Spaces:
Running
Running
thibaud frere
commited on
Commit
·
0390c56
1
Parent(s):
f9074bc
update doc
Browse files
app/astro.config.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import remarkFootnotes from 'remark-footnotes';
|
|
| 8 |
import rehypeSlug from 'rehype-slug';
|
| 9 |
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
| 10 |
import rehypeCitation from 'rehype-citation';
|
| 11 |
-
import
|
| 12 |
import rehypeReferencesAndFootnotes from './plugins/rehype/post-citation.mjs';
|
| 13 |
import remarkIgnoreCitationsInCode from './plugins/remark/ignore-citations-in-code.mjs';
|
| 14 |
import rehypeRestoreAtInCode from './plugins/rehype/restore-at-in-code.mjs';
|
|
@@ -56,7 +56,7 @@ export default defineConfig({
|
|
| 56 |
}],
|
| 57 |
rehypeReferencesAndFootnotes,
|
| 58 |
rehypeRestoreAtInCode,
|
| 59 |
-
|
| 60 |
rehypeWrapTables
|
| 61 |
]
|
| 62 |
}
|
|
|
|
| 8 |
import rehypeSlug from 'rehype-slug';
|
| 9 |
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
| 10 |
import rehypeCitation from 'rehype-citation';
|
| 11 |
+
import rehypeCodeCopy from './plugins/rehype/code-copy.mjs';
|
| 12 |
import rehypeReferencesAndFootnotes from './plugins/rehype/post-citation.mjs';
|
| 13 |
import remarkIgnoreCitationsInCode from './plugins/remark/ignore-citations-in-code.mjs';
|
| 14 |
import rehypeRestoreAtInCode from './plugins/rehype/restore-at-in-code.mjs';
|
|
|
|
| 56 |
}],
|
| 57 |
rehypeReferencesAndFootnotes,
|
| 58 |
rehypeRestoreAtInCode,
|
| 59 |
+
rehypeCodeCopy,
|
| 60 |
rehypeWrapTables
|
| 61 |
]
|
| 62 |
}
|
app/plugins/rehype/{code-copy-and-label.mjs → code-copy.mjs}
RENAMED
|
@@ -1,46 +1,14 @@
|
|
| 1 |
-
// Minimal rehype plugin to wrap code blocks with a copy button
|
| 2 |
// Exported as a standalone module to keep astro.config.mjs lean
|
| 3 |
-
export default function
|
| 4 |
return (tree) => {
|
| 5 |
// Walk the tree; lightweight visitor to find <pre><code>
|
| 6 |
const visit = (node, parent) => {
|
| 7 |
if (!node || typeof node !== 'object') return;
|
| 8 |
const children = Array.isArray(node.children) ? node.children : [];
|
| 9 |
if (node.tagName === 'pre' && children.some(c => c.tagName === 'code')) {
|
| 10 |
-
// Find code child
|
| 11 |
const code = children.find(c => c.tagName === 'code');
|
| 12 |
-
const collectClasses = (val) => Array.isArray(val) ? val.map(String) : (typeof val === 'string' ? String(val).split(/\s+/) : []);
|
| 13 |
-
const fromClass = (names) => {
|
| 14 |
-
const hit = names.find((n) => /^language-/.test(String(n)));
|
| 15 |
-
return hit ? String(hit).replace(/^language-/, '') : '';
|
| 16 |
-
};
|
| 17 |
-
const codeClasses = collectClasses(code?.properties?.className);
|
| 18 |
-
const preClasses = collectClasses(node?.properties?.className);
|
| 19 |
-
const candidates = [
|
| 20 |
-
code?.properties?.['data-language'],
|
| 21 |
-
fromClass(codeClasses),
|
| 22 |
-
node?.properties?.['data-language'],
|
| 23 |
-
fromClass(preClasses),
|
| 24 |
-
];
|
| 25 |
-
let lang = candidates.find(Boolean) || '';
|
| 26 |
-
const lower = String(lang).toLowerCase();
|
| 27 |
-
const toExt = (s) => {
|
| 28 |
-
switch (String(s).toLowerCase()) {
|
| 29 |
-
case 'typescript': case 'ts': return 'ts';
|
| 30 |
-
case 'tsx': return 'tsx';
|
| 31 |
-
case 'javascript': case 'js': case 'node': return 'js';
|
| 32 |
-
case 'jsx': return 'jsx';
|
| 33 |
-
case 'python': case 'py': return 'py';
|
| 34 |
-
case 'bash': case 'shell': case 'sh': return 'sh';
|
| 35 |
-
case 'markdown': case 'md': return 'md';
|
| 36 |
-
case 'yaml': case 'yml': return 'yml';
|
| 37 |
-
case 'html': return 'html';
|
| 38 |
-
case 'css': return 'css';
|
| 39 |
-
case 'json': return 'json';
|
| 40 |
-
default: return lower || '';
|
| 41 |
-
}
|
| 42 |
-
};
|
| 43 |
-
const ext = toExt(lower);
|
| 44 |
// Determine if single-line block: prefer Shiki lines, then text content
|
| 45 |
const countLinesFromShiki = () => {
|
| 46 |
const isLineEl = (el) => el && el.type === 'element' && el.tagName === 'span' && Array.isArray(el.properties?.className) && el.properties.className.includes('line');
|
|
@@ -85,16 +53,11 @@ export default function rehypeCodeCopyAndLabel() {
|
|
| 85 |
node.__forceSingle = true;
|
| 86 |
}
|
| 87 |
}
|
| 88 |
-
// Ensure CSS-only label works: set data-language on <code> and <pre>, and wrapper
|
| 89 |
-
code.properties = code.properties || {};
|
| 90 |
-
if (ext) code.properties['data-language'] = ext;
|
| 91 |
-
node.properties = node.properties || {};
|
| 92 |
-
if (ext) node.properties['data-language'] = ext;
|
| 93 |
// Replace <pre> with wrapper div.code-card containing button + pre
|
| 94 |
const wrapper = {
|
| 95 |
type: 'element',
|
| 96 |
tagName: 'div',
|
| 97 |
-
properties: { className: ['code-card'].concat((isSingleLine || node.__forceSingle) ? ['no-copy'] : [])
|
| 98 |
children: (isSingleLine || node.__forceSingle) ? [ node ] : [
|
| 99 |
{
|
| 100 |
type: 'element',
|
|
@@ -127,3 +90,5 @@ export default function rehypeCodeCopyAndLabel() {
|
|
| 127 |
}
|
| 128 |
|
| 129 |
|
|
|
|
|
|
|
|
|
| 1 |
+
// Minimal rehype plugin to wrap code blocks with a copy button
|
| 2 |
// Exported as a standalone module to keep astro.config.mjs lean
|
| 3 |
+
export default function rehypeCodeCopy() {
|
| 4 |
return (tree) => {
|
| 5 |
// Walk the tree; lightweight visitor to find <pre><code>
|
| 6 |
const visit = (node, parent) => {
|
| 7 |
if (!node || typeof node !== 'object') return;
|
| 8 |
const children = Array.isArray(node.children) ? node.children : [];
|
| 9 |
if (node.tagName === 'pre' && children.some(c => c.tagName === 'code')) {
|
| 10 |
+
// Find code child
|
| 11 |
const code = children.find(c => c.tagName === 'code');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
// Determine if single-line block: prefer Shiki lines, then text content
|
| 13 |
const countLinesFromShiki = () => {
|
| 14 |
const isLineEl = (el) => el && el.type === 'element' && el.tagName === 'span' && Array.isArray(el.properties?.className) && el.properties.className.includes('line');
|
|
|
|
| 53 |
node.__forceSingle = true;
|
| 54 |
}
|
| 55 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
// Replace <pre> with wrapper div.code-card containing button + pre
|
| 57 |
const wrapper = {
|
| 58 |
type: 'element',
|
| 59 |
tagName: 'div',
|
| 60 |
+
properties: { className: ['code-card'].concat((isSingleLine || node.__forceSingle) ? ['no-copy'] : []) },
|
| 61 |
children: (isSingleLine || node.__forceSingle) ? [ node ] : [
|
| 62 |
{
|
| 63 |
type: 'element',
|
|
|
|
| 90 |
}
|
| 91 |
|
| 92 |
|
| 93 |
+
|
| 94 |
+
|
app/src/content/article.mdx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
| 1 |
---
|
| 2 |
title: "Bringing paper to life:\n A modern template for\n scientific writing
|
| 3 |
"
|
| 4 |
-
subtitle: "
|
| 5 |
-
description: "
|
| 6 |
authors:
|
| 7 |
- name: "Thibaud Frere"
|
| 8 |
url: "https://huggingface.co/tfrere"
|
|
|
|
| 1 |
---
|
| 2 |
title: "Bringing paper to life:\n A modern template for\n scientific writing
|
| 3 |
"
|
| 4 |
+
subtitle: "Ready‑to‑publish workflow so you can focus on ideas rather than infrastructure."
|
| 5 |
+
description: "Ready‑to‑publish workflow so you can focus on ideas rather than infrastructure."
|
| 6 |
authors:
|
| 7 |
- name: "Thibaud Frere"
|
| 8 |
url: "https://huggingface.co/tfrere"
|
app/src/content/chapters/introduction.mdx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
| 1 |
import Sidenote from "../../components/Sidenote.astro";
|
| 2 |
|
| 3 |
<Sidenote>
|
| 4 |
-
Welcome to this
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
It offers a **ready‑to‑publish**, **all‑in‑one** workflow so you can focus on **ideas** rather than **infrastructure**.
|
| 7 |
<Fragment slot="aside">
|
| 8 |
Reading time: 20–25 minutes.
|
| 9 |
</Fragment>
|
|
@@ -26,7 +27,7 @@ import Sidenote from "../../components/Sidenote.astro";
|
|
| 26 |
<span className="tag">Dataviz color palettes</span>
|
| 27 |
<span className="tag">Optimized images</span>
|
| 28 |
<span className="tag">Lightweight bundle</span>
|
| 29 |
-
<span className="tag">SEO
|
| 30 |
<span className="tag">Automatic build</span>
|
| 31 |
<span className="tag">Automatic PDF export</span>
|
| 32 |
<span className="tag">Dark theme</span>
|
|
@@ -38,26 +39,23 @@ import Sidenote from "../../components/Sidenote.astro";
|
|
| 38 |
</Sidenote>
|
| 39 |
|
| 40 |
## Introduction
|
| 41 |
-
The web
|
| 42 |
-
|
| 43 |
-
If you write technical blogs or research notes, you’ll benefit from a **ready‑to‑publish workflow** with clear notation, optimized media, and **automatic builds**.
|
| 44 |
|
| 45 |
### Who is this for
|
| 46 |
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
- Researchers/engineers with live figures.
|
| 50 |
-
- Educators crafting explorable explanations.
|
| 51 |
|
| 52 |
-
|
|
|
|
| 53 |
|
|
|
|
| 54 |
|
| 55 |
### Inspired by Distill
|
| 56 |
|
| 57 |
<Sidenote>
|
| 58 |
-
This project draws strong inspiration from [**Distill**](https://distill.pub) (2016–2021), which championed clear
|
| 59 |
|
| 60 |
-
To give you a sense of what inspired this template, here is a short, curated list of well‑designed and often interactive works from Distill:
|
| 61 |
|
| 62 |
- [Growing Neural Cellular Automata](https://distill.pub/2020/growing-ca/)
|
| 63 |
- [Activation Atlas](https://distill.pub/2019/activation-atlas/)
|
|
|
|
| 1 |
import Sidenote from "../../components/Sidenote.astro";
|
| 2 |
|
| 3 |
<Sidenote>
|
| 4 |
+
Welcome to this single‑page **research article template**. It helps you publish **clear**, **modern**, and **interactive technical writing** with **minimal setup**.
|
| 5 |
+
|
| 6 |
+
Grounded in up to date good practices in web dev, it favors **interactive explanations**, **clear notation**, and **inspectable examples** over static snapshots.
|
| 7 |
|
|
|
|
| 8 |
<Fragment slot="aside">
|
| 9 |
Reading time: 20–25 minutes.
|
| 10 |
</Fragment>
|
|
|
|
| 27 |
<span className="tag">Dataviz color palettes</span>
|
| 28 |
<span className="tag">Optimized images</span>
|
| 29 |
<span className="tag">Lightweight bundle</span>
|
| 30 |
+
<span className="tag">SEO friendly</span>
|
| 31 |
<span className="tag">Automatic build</span>
|
| 32 |
<span className="tag">Automatic PDF export</span>
|
| 33 |
<span className="tag">Dark theme</span>
|
|
|
|
| 39 |
</Sidenote>
|
| 40 |
|
| 41 |
## Introduction
|
| 42 |
+
The web offers what static PDFs can’t: **interactive diagrams**, **progressive notation**, and **exploratory views** that show how ideas behave. This template treats **interactive artifacts**—figures, math, code, and inspectable experiments—as **first‑class** alongside prose, helping readers **build intuition** instead of skimming results.
|
|
|
|
|
|
|
| 43 |
|
| 44 |
### Who is this for
|
| 45 |
|
| 46 |
+
Ideal for anyone creating **web‑native** and **interactive** content with **minimal setup**:
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
+
- For **scientists** writing modern web‑native papers
|
| 49 |
+
- For **educators** building explorable lessons.
|
| 50 |
|
| 51 |
+
This is not a CMS or a multi‑page blog—it's a **focused**, **single‑page**, **MDX‑first** workflow.
|
| 52 |
|
| 53 |
### Inspired by Distill
|
| 54 |
|
| 55 |
<Sidenote>
|
| 56 |
+
This project draws strong inspiration from [**Distill**](https://distill.pub) (2016–2021), which championed **clear**, **web‑native** scholarship and set a **high standard** for **interactive scientific communication**.
|
| 57 |
|
| 58 |
+
To give you a sense of what inspired this template, here is a short, curated list of **well‑designed** and often **interactive** works from Distill:
|
| 59 |
|
| 60 |
- [Growing Neural Cellular Automata](https://distill.pub/2020/growing-ca/)
|
| 61 |
- [Activation Atlas](https://distill.pub/2019/activation-atlas/)
|
app/src/pages/index.astro
CHANGED
|
@@ -186,7 +186,7 @@ const licence = (articleFM as any)?.licence ?? (articleFM as any)?.license ?? (a
|
|
| 186 |
</script>
|
| 187 |
|
| 188 |
<script>
|
| 189 |
-
// Delegate copy clicks for code blocks injected by
|
| 190 |
document.addEventListener('click', async (e) => {
|
| 191 |
const target = e.target instanceof Element ? e.target : null;
|
| 192 |
const btn = target ? target.closest('.code-copy') : null;
|
|
|
|
| 186 |
</script>
|
| 187 |
|
| 188 |
<script>
|
| 189 |
+
// Delegate copy clicks for code blocks injected by rehypeCodeCopy
|
| 190 |
document.addEventListener('click', async (e) => {
|
| 191 |
const target = e.target instanceof Element ? e.target : null;
|
| 192 |
const btn = target ? target.closest('.code-copy') : null;
|
app/src/styles/components/_code.css
CHANGED
|
@@ -174,9 +174,7 @@ section.content-grid pre code {
|
|
| 174 |
} */
|
| 175 |
|
| 176 |
/* Fallback if Shiki uses data-lang instead of data-language */
|
| 177 |
-
.astro-code[data-lang]::after { content: attr(data-lang); }
|
| 178 |
-
|
| 179 |
-
/* Normalize to extensions for common languages */
|
| 180 |
.astro-code[data-language="typescript"]::after { content: "ts"; }
|
| 181 |
.astro-code[data-language="tsx"]::after { content: "tsx"; }
|
| 182 |
.astro-code[data-language="javascript"]::after,
|
|
@@ -191,7 +189,7 @@ section.content-grid pre code {
|
|
| 191 |
.astro-code[data-language="yml"]::after { content: "yml"; }
|
| 192 |
.astro-code[data-language="html"]::after { content: "html"; }
|
| 193 |
.astro-code[data-language="css"]::after { content: "css"; }
|
| 194 |
-
.astro-code[data-language="json"]::after { content: "json"; }
|
| 195 |
|
| 196 |
/* In Accordions, keep same bottom-right placement */
|
| 197 |
.accordion .astro-code::after { right: 0; bottom: 0; }
|
|
|
|
| 174 |
} */
|
| 175 |
|
| 176 |
/* Fallback if Shiki uses data-lang instead of data-language */
|
| 177 |
+
/* .astro-code[data-lang]::after { content: attr(data-lang); }
|
|
|
|
|
|
|
| 178 |
.astro-code[data-language="typescript"]::after { content: "ts"; }
|
| 179 |
.astro-code[data-language="tsx"]::after { content: "tsx"; }
|
| 180 |
.astro-code[data-language="javascript"]::after,
|
|
|
|
| 189 |
.astro-code[data-language="yml"]::after { content: "yml"; }
|
| 190 |
.astro-code[data-language="html"]::after { content: "html"; }
|
| 191 |
.astro-code[data-language="css"]::after { content: "css"; }
|
| 192 |
+
.astro-code[data-language="json"]::after { content: "json"; } */
|
| 193 |
|
| 194 |
/* In Accordions, keep same bottom-right placement */
|
| 195 |
.accordion .astro-code::after { right: 0; bottom: 0; }
|
app/src/styles/components/_tag.css
CHANGED
|
@@ -4,10 +4,10 @@
|
|
| 4 |
display: inline-flex;
|
| 5 |
align-items: center;
|
| 6 |
gap: 6px;
|
| 7 |
-
padding:
|
| 8 |
font-size: 12px;
|
| 9 |
line-height: 1;
|
| 10 |
-
border-radius:
|
| 11 |
background: var(--surface-bg);
|
| 12 |
border: 1px solid var(--border-color);
|
| 13 |
color: var(--text-color);
|
|
|
|
| 4 |
display: inline-flex;
|
| 5 |
align-items: center;
|
| 6 |
gap: 6px;
|
| 7 |
+
padding: 8px 12px;
|
| 8 |
font-size: 12px;
|
| 9 |
line-height: 1;
|
| 10 |
+
border-radius: var(--button-radius);
|
| 11 |
background: var(--surface-bg);
|
| 12 |
border: 1px solid var(--border-color);
|
| 13 |
color: var(--text-color);
|