diff --git a/.gitattributes b/.gitattributes index 92a6ee9c9fd87119961153d41085604d243c78fb..7b662b2c5d7eb87165852e4e21881650ffe40d6a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,14 +1,14 @@ -# Images et médias *.png filter=lfs diff=lfs merge=lfs -text *.jpg filter=lfs diff=lfs merge=lfs -text *.jpeg filter=lfs diff=lfs merge=lfs -text *.gif filter=lfs diff=lfs merge=lfs -text - -# Vidéos et audio +*.mp3 filter=lfs diff=lfs merge=lfs -text *.mp4 filter=lfs diff=lfs merge=lfs -text *.mov filter=lfs diff=lfs merge=lfs -text *.avi filter=lfs diff=lfs merge=lfs -text -*.mp3 filter=lfs diff=lfs merge=lfs -text *.wav filter=lfs diff=lfs merge=lfs -text - -*.pdf filter=lfs diff=lfs merge=lfs -text \ No newline at end of file +*.csv filter=lfs diff=lfs merge=lfs -text +*.json filter=lfs diff=lfs merge=lfs -text +# the package and package lock should not be tracked +package.json -filter -diff -merge text +package-lock.json -filter -diff -merge text \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000000000000000000000000000000000000..5837b2b57b8d319f7a12c1b0ff413044b7792f33 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,118 @@ +# Changelog + +All notable changes to the Research Article Template will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Initial open source release +- Comprehensive documentation +- Contributing guidelines +- License file + +## [1.0.0] - 2024-12-19 + +### Added +- **Core Features**: + - Markdown/MDX-based writing system + - KaTeX mathematical notation support + - Syntax highlighting for code blocks + - Academic citations with BibTeX integration + - Footnotes and sidenotes system + - Auto-generated table of contents + - Interactive Mermaid diagrams + - Plotly.js and D3.js integration + - HTML embed support + - Gradio app embedding + - Dataviz color palettes + - Image optimization + - SEO-friendly structure + - Automatic PDF export + - Dark/light theme toggle + - Mobile-responsive design + - LaTeX import functionality + - Template synchronization system + +- **Components**: + - Figure component with captions + - MultiFigure for image galleries + - Note component with variants + - Quote component + - Accordion for collapsible content + - Sidenote component + - Table of Contents + - Theme Toggle + - HTML Embed + - Raw HTML support + - SEO component + - Hero section + - Footer + - Full-width and wide layouts + +- **Build System**: + - Astro 4.10.0 integration + - PostCSS with custom media queries + - Automatic compression + - Docker support + - Nginx configuration + - Git LFS support + +- **Scripts**: + - PDF export functionality + - LaTeX to MDX conversion + - Template synchronization + - Font SVG generation + - TrackIO data generation + +- **Documentation**: + - Getting started guide + - Writing best practices + - Component reference + - LaTeX conversion guide + - Interactive examples + +### Technical Details +- **Framework**: Astro 4.10.0 +- **Styling**: PostCSS with custom properties +- **Math**: KaTeX 0.16.22 +- **Charts**: Plotly.js 3.1.0, D3.js 7.9.0 +- **Diagrams**: Mermaid 11.10.1 +- **Node.js**: >=20.0.0 +- **License**: CC-BY-4.0 + +### Browser Support +- Chrome (latest) +- Firefox (latest) +- Safari (latest) +- Edge (latest) + +--- + +## Version History + +- **1.0.0**: Initial stable release with full feature set +- **0.0.1**: Development version (pre-release) + +## Migration Guide + +### From 0.0.1 to 1.0.0 + +This is the first stable release. No breaking changes from the development version. + +### Updating Your Project + +Use the template synchronization system to update: + +```bash +npm run sync:template -- --dry-run # Preview changes +npm run sync:template # Apply updates +``` + +## Support + +- **Documentation**: [Hugging Face Space](https://huggingface.co/spaces/tfrere/research-article-template) +- **Issues**: [Community Discussions](https://huggingface.co/spaces/tfrere/research-article-template/discussions) +- **Contact**: [@tfrere](https://huggingface.co/tfrere) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..a4573b5d9abcd9e9ba35095677d0443b157298ec --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,196 @@ +# Contributing to Research Article Template + +Thank you for your interest in contributing to the Research Article Template! This document provides guidelines and information for contributors. + +## 🤝 How to Contribute + +### Reporting Issues + +Before creating an issue, please: +1. **Search existing issues** to avoid duplicates +2. **Use the issue template** when available +3. **Provide detailed information**: + - Clear description of the problem + - Steps to reproduce + - Expected vs actual behavior + - Environment details (OS, Node.js version, browser) + - Screenshots if applicable + +### Suggesting Features + +We welcome feature suggestions! Please: +1. **Check existing discussions** first +2. **Describe the use case** clearly +3. **Explain the benefits** for the community +4. **Consider implementation complexity** + +### Code Contributions + +#### Getting Started + +1. **Fork the repository** on Hugging Face +2. **Clone your fork**: + ```bash + git clone git@hf.co:spaces//research-article-template + cd research-article-template + ``` +3. **Install dependencies**: + ```bash + cd app + npm install + ``` +4. **Create a feature branch**: + ```bash + git checkout -b feature/your-feature-name + ``` + +#### Development Workflow + +1. **Make your changes** following our coding standards +2. **Test thoroughly**: + ```bash + npm run dev # Test locally + npm run build # Ensure build works + ``` +3. **Update documentation** if needed +4. **Commit with clear messages**: + ```bash + git commit -m "feat: add new component for interactive charts" + ``` + +#### Pull Request Process + +1. **Push your branch**: + ```bash + git push origin feature/your-feature-name + ``` +2. **Create a Pull Request** with: + - Clear title and description + - Reference related issues + - Screenshots for UI changes + - Testing instructions + +## 📋 Coding Standards + +### Code Style + +- **Use Prettier** for consistent formatting +- **Follow existing patterns** in the codebase +- **Write clear, self-documenting code** +- **Add comments** for complex logic +- **Use meaningful variable names** + +### File Organization + +- **Components**: Place in `src/components/` +- **Styles**: Use CSS modules or component-scoped styles +- **Assets**: Organize in `src/content/assets/` +- **Documentation**: Update relevant `.mdx` files + +### Commit Message Format + +We follow [Conventional Commits](https://www.conventionalcommits.org/): + +``` +type(scope): description + +feat: add new interactive chart component +fix: resolve mobile layout issues +docs: update installation instructions +style: improve button hover states +refactor: simplify component structure +test: add unit tests for utility functions +``` + +**Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore` + +## 🧪 Testing + +### Manual Testing + +Before submitting: +- [ ] Test on different screen sizes +- [ ] Verify dark/light theme compatibility +- [ ] Check browser compatibility (Chrome, Firefox, Safari) +- [ ] Test with different content types +- [ ] Ensure accessibility standards + +### Automated Testing + +```bash +# Run build to catch errors +npm run build + +# Test PDF export +npm run export:pdf + +# Test LaTeX conversion +npm run latex:convert +``` + +## 📚 Documentation + +### Writing Guidelines + +- **Use clear, concise language** +- **Provide examples** for complex features +- **Include screenshots** for UI changes +- **Update both English content and code comments** + +### Documentation Structure + +- **README.md**: Project overview and quick start +- **CONTRIBUTING.md**: This file +- **Content files**: In `src/content/chapters/demo/` +- **Component docs**: Inline comments and examples + +## 🎯 Areas for Contribution + +### High Priority + +- **Bug fixes** and stability improvements +- **Accessibility enhancements** +- **Mobile responsiveness** +- **Performance optimizations** +- **Documentation improvements** + +### Feature Ideas + +- **New interactive components** +- **Additional export formats** +- **Enhanced LaTeX import** +- **Theme customization** +- **Plugin system** + +### Community + +- **Answer questions** in discussions +- **Share examples** of your work +- **Write tutorials** and guides +- **Help with translations** + +## 🚫 What Not to Contribute + +- **Breaking changes** without discussion +- **Major architectural changes** without approval +- **Dependencies** that significantly increase bundle size +- **Features** that don't align with the project's goals + +## 📞 Getting Help + +- **Discussions**: [Community tab](https://huggingface.co/spaces/tfrere/research-article-template/discussions) +- **Issues**: [Report bugs](https://huggingface.co/spaces/tfrere/research-article-template/discussions?status=open&type=issue) +- **Contact**: [@tfrere](https://huggingface.co/tfrere) on Hugging Face + +## 📄 License + +By contributing, you agree that your contributions will be licensed under the same [CC-BY-4.0 license](LICENSE) that covers the project. + +## 🙏 Recognition + +Contributors will be: +- **Listed in acknowledgments** (if desired) +- **Mentioned in release notes** for significant contributions +- **Credited** in relevant documentation + +Thank you for helping make scientific writing more accessible and interactive! 🎉 diff --git a/Dockerfile b/Dockerfile index 67c6e813bddb036e8afb74f7055d42c8c1b5e416..74a68280c9fa5bbd41b71f134c6020db86a2394f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Use an official Node runtime as the base image for building the application # Build with Playwright (browsers and deps ready) -FROM mcr.microsoft.com/playwright:v1.56.0-jammy AS build +FROM mcr.microsoft.com/playwright:v1.55.0-jammy AS build # Install git, git-lfs, and dependencies for Pandoc (only if ENABLE_LATEX_CONVERSION=true) RUN apt-get update && apt-get install -y git git-lfs wget && apt-get clean @@ -24,7 +24,7 @@ RUN npm install COPY app/ . # Conditionally convert LaTeX to MDX if ENABLE_LATEX_CONVERSION=true -ARG ENABLE_LATEX_CONVERSION=false +ARG ENABLE_LATEX_CONVERSION=true RUN if [ "$ENABLE_LATEX_CONVERSION" = "true" ]; then \ echo "🔄 LaTeX importer enabled - running latex:convert..."; \ npm run latex:convert; \ @@ -32,10 +32,6 @@ RUN if [ "$ENABLE_LATEX_CONVERSION" = "true" ]; then \ echo "⏭️ LaTeX importer disabled - skipping..."; \ fi -# Pre-install notion-importer dependencies (for runtime import) -# Note: Notion import is done at RUNTIME (not build time) to access secrets -RUN cd scripts/notion-importer && npm install && cd ../.. - # Ensure `public/data` is a real directory with real files (not a symlink) # This handles the case where `public/data` is a symlink in the repo, which # would be broken inside the container after COPY. @@ -46,32 +42,30 @@ RUN set -e; \ mkdir -p public/data; \ cp -a src/content/assets/data/. public/data/ -# Build the application (with minimal placeholder content) +# Build the application RUN npm run build # Generate the PDF (light theme, full wait) RUN npm run export:pdf -- --theme=light --wait=full -# Generate LaTeX export -RUN npm run export:latex +# Use an official Nginx runtime as the base image for serving the application +FROM nginx:alpine -# Install nginx in the build stage (we'll use this image as final to keep Node.js) -RUN apt-get update && apt-get install -y nginx && apt-get clean && rm -rf /var/lib/apt/lists/* +# Copy the built application from the build stage +COPY --from=build /app/dist /usr/share/nginx/html -# Copy nginx configuration +# Copy a custom Nginx configuration file COPY nginx.conf /etc/nginx/nginx.conf -# Copy entrypoint script -COPY entrypoint.sh /entrypoint.sh -RUN chmod +x /entrypoint.sh - # Create necessary directories and set permissions -RUN mkdir -p /var/cache/nginx /var/run /var/log/nginx /var/lib/nginx/body && \ - chmod -R 777 /var/cache/nginx /var/run /var/log/nginx /var/lib/nginx /etc/nginx/nginx.conf && \ - chmod -R 777 /app +RUN mkdir -p /var/cache/nginx /var/run /var/log/nginx && \ + chmod -R 777 /var/cache/nginx /var/run /var/log/nginx /etc/nginx/nginx.conf + +# Switch to non-root user +USER nginx # Expose port 8080 EXPOSE 8080 -# Use entrypoint script that handles Notion import if enabled -ENTRYPOINT ["/entrypoint.sh"] +# Command to run the application +CMD ["nginx", "-g", "daemon off;"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..b267a53137822114e4c0bcef2e6383aaf52a70f1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,33 @@ +Creative Commons Attribution 4.0 International License + +Copyright (c) 2024 Thibaud Frere + +This work is licensed under the Creative Commons Attribution 4.0 International License. +To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/ +or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. + +You are free to: + + Share — copy and redistribute the material in any medium or format + Adapt — remix, transform, and build upon the material for any purpose, even commercially. + +The licensor cannot revoke these freedoms as long as you follow the license terms. + +Under the following terms: + + Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. You may do so in any reasonable manner, but not in any way that suggests the licensor endorses you or your use. + + No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. + +Notices: + + You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation. + + No warranties are given. The license may not give you all of the permissions necessary for your intended use. For example, other rights such as publicity, privacy, or moral rights may limit how you use the material. + +--- + +For the source code and technical implementation: +- The source code is available at: https://huggingface.co/spaces/tfrere/research-article-template +- Third-party figures and assets are excluded from this license and marked in their captions +- Dependencies and third-party libraries maintain their respective licenses diff --git a/app/astro.config.mjs b/app/astro.config.mjs index 9b35ebc6f6aeb852e7d6293fbc20b557b855810e..a00150600b0ca51f142971b8611048f559bc41f9 100644 --- a/app/astro.config.mjs +++ b/app/astro.config.mjs @@ -56,11 +56,13 @@ export default defineConfig({ rehypePlugins: [ rehypeSlug, [rehypeAutolinkHeadings, { behavior: 'wrap' }], - [rehypeKatex, { trust: true }], + [rehypeKatex, { + trust: true, + }], [rehypeCitation, { bibliography: 'src/content/bibliography.bib', linkCitations: true, - csl: "apa" + csl: "apa", }], rehypeReferencesAndFootnotes, rehypeRestoreAtInCode, diff --git a/app/package.json b/app/package.json index 71350c44d5b6f0784f561d2a8f0b452fa2e18453..6bace040eb0c3d6f08ce003cb889c1ec046bface 100644 Binary files a/app/package.json and b/app/package.json differ diff --git a/app/public/scripts/color-palettes.js b/app/public/scripts/color-palettes.js index 7dd3223cfb8a3b6cf5b65121ef1e4eacf33232dd..370b1f464142e0d9280855b18f8f636db810ea6e 100644 --- a/app/public/scripts/color-palettes.js +++ b/app/public/scripts/color-palettes.js @@ -46,63 +46,95 @@ return { r, g, b: b3 }; }; const oklchToOklab = (L, C, hDeg) => { const h = (hDeg * Math.PI) / 180; return { L, a: C * Math.cos(h), b: C * Math.sin(h) }; }; - const oklabToOklch = (L, a, b) => { const C = Math.sqrt(a*a + b*b); let h = Math.atan2(b, a) * 180 / Math.PI; if (h < 0) h += 360; return { L, C, h }; }; + const oklabToOklch = (L, a, b) => { const C = Math.sqrt(a * a + b * b); let h = Math.atan2(b, a) * 180 / Math.PI; if (h < 0) h += 360; return { L, C, h }; }; const clamp01 = (x) => Math.min(1, Math.max(0, x)); const isInGamut = ({ r, g, b }) => r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1; - const toHex = ({ r, g, b }) => { const R = Math.round(clamp01(r)*255), G = Math.round(clamp01(g)*255), B = Math.round(clamp01(b)*255); const h = (n) => n.toString(16).padStart(2,'0'); return `#${h(R)}${h(G)}${h(B)}`.toUpperCase(); }; - const oklchToHexSafe = (L, C, h) => { let c = C; for (let i=0;i<12;i++){ const { a, b } = oklchToOklab(L,c,h); const rgb = oklabToRgb(L,a,b); if (isInGamut(rgb)) return toHex(rgb); c = Math.max(0, c-0.02);} return toHex(oklabToRgb(L,0,0)); }; - const parseCssColorToRgb = (css) => { try { const el = document.createElement('span'); el.style.color = css; document.body.appendChild(el); const cs = getComputedStyle(el).color; document.body.removeChild(el); const m = cs.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (!m) return null; return { r: Number(m[1])/255, g: Number(m[2])/255, b: Number(m[3])/255 }; } catch { return null; } }; + const toHex = ({ r, g, b }) => { + const R = Math.round(clamp01(r) * 255), G = Math.round(clamp01(g) * 255), B = Math.round(clamp01(b) * 255); + const h = (n) => n.toString(16).padStart(2, '0'); + return `#${h(R)}${h(G)}${h(B)}`.toUpperCase(); + }; + const oklchToHexSafe = (L, C, h) => { let c = C; for (let i = 0; i < 12; i++) { const { a, b } = oklchToOklab(L, c, h); const rgb = oklabToRgb(L, a, b); if (isInGamut(rgb)) return toHex(rgb); c = Math.max(0, c - 0.02); } return toHex(oklabToRgb(L, 0, 0)); }; + const parseCssColorToRgb = (css) => { try { const el = document.createElement('span'); el.style.color = css; document.body.appendChild(el); const cs = getComputedStyle(el).color; document.body.removeChild(el); const m = cs.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (!m) return null; return { r: Number(m[1]) / 255, g: Number(m[2]) / 255, b: Number(m[3]) / 255 }; } catch { return null; } }; - const getPrimaryHex = () => { + // Get primary color in OKLCH format to preserve precision + const getPrimaryOKLCH = () => { const css = getCssVar('--primary-color'); - if (!css) return '#E889AB'; - if (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(css)) return css.toUpperCase(); + if (!css) return null; + + // For OKLCH colors, return the exact values without conversion + if (css.includes('oklch')) { + const oklchMatch = css.match(/oklch\(([^)]+)\)/); + if (oklchMatch) { + const values = oklchMatch[1].split(/\s+/).map(v => parseFloat(v.trim())); + if (values.length >= 3) { + const [L, C, h] = values; + return { L, C, h }; + } + } + } + + // For non-OKLCH colors, convert to OKLCH for consistency const rgb = parseCssColorToRgb(css); - if (rgb) return toHex(rgb); - return '#E889AB'; + if (rgb) { + const { L, a, b } = rgbToOklab(rgb.r, rgb.g, rgb.b); + const { C, h } = oklabToOklch(L, a, b); + return { L, C, h }; + } + return null; + }; + + // Keep getPrimaryHex for backward compatibility, but now it converts from OKLCH + const getPrimaryHex = () => { + const oklch = getPrimaryOKLCH(); + if (!oklch) return null; + + const { a, b } = oklchToOklab(oklch.L, oklch.C, oklch.h); + const rgb = oklabToRgb(oklch.L, a, b); + return toHex(rgb); }; // No count management via CSS anymore; counts are passed directly to the API const generators = { - categorical: (baseHex, count) => { - const parseHex = (h) => { const s = h.replace('#',''); const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s; return { r: parseInt(v.slice(0,2),16)/255, g: parseInt(v.slice(2,4),16)/255, b: parseInt(v.slice(4,6),16)/255 }; }; - const { r, g, b } = parseHex(baseHex); - const { L, a, b: bb } = rgbToOklab(r,g,b); - const { C, h } = oklabToOklch(L,a,bb); + categorical: (baseOKLCH, count) => { + const { L, C, h } = baseOKLCH; const L0 = Math.min(0.85, Math.max(0.4, L)); const C0 = Math.min(0.35, Math.max(0.1, C || 0.2)); const total = Math.max(1, Math.min(12, count || 8)); const hueStep = 360 / total; const results = []; - for (let i=0;i { - const parseHex = (h) => { const s = h.replace('#',''); const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s; return { r: parseInt(v.slice(0,2),16)/255, g: parseInt(v.slice(2,4),16)/255, b: parseInt(v.slice(4,6),16)/255 }; }; - const { r, g, b } = parseHex(baseHex); - const { L, a, b: bb } = rgbToOklab(r,g,b); - const { C, h } = oklabToOklch(L,a,bb); + sequential: (baseOKLCH, count) => { + const { L, C, h } = baseOKLCH; const total = Math.max(1, Math.min(12, count || 8)); const startL = Math.max(0.25, L - 0.18); const endL = Math.min(0.92, L + 0.18); const cBase = Math.min(0.33, Math.max(0.08, C * 0.9 + 0.06)); const out = []; - for (let i=0;i { - const parseHex = (h) => { const s = h.replace('#',''); const v = s.length===3 ? s.split('').map(ch=>ch+ch).join('') : s; return { r: parseInt(v.slice(0,2),16)/255, g: parseInt(v.slice(2,4),16)/255, b: parseInt(v.slice(4,6),16)/255 }; }; - const { r, g, b } = parseHex(baseHex); - const baseLab = rgbToOklab(r,g,b); - const baseLch = oklabToOklch(baseLab.L, baseLab.a, baseLab.b); + diverging: (baseOKLCH, count) => { + const { L, C, h } = baseOKLCH; const total = Math.max(1, Math.min(12, count || 8)); // Left endpoint: EXACT primary color (no darkening) - const leftLab = baseLab; + const leftLab = oklchToOklab(L, C, h); // Right endpoint: complement with same L and similar C (clamped safe) - const compH = (baseLch.h + 180) % 360; - const cSafe = Math.min(0.35, Math.max(0.08, baseLch.C)); - const rightLab = oklchToOklab(baseLab.L, cSafe, compH); + const compH = (h + 180) % 360; + const cSafe = Math.min(0.35, Math.max(0.08, C)); + const rightLab = oklchToOklab(L, cSafe, compH); const whiteLab = { L: 0.98, a: 0, b: 0 }; // center near‑white const hexFromOKLab = (L, a, b) => toHex(oklabToRgb(L, a, b)); @@ -152,18 +184,22 @@ let lastSignature = ''; const updatePalettes = () => { - const primary = getPrimaryHex(); - const signature = `${primary}`; + const primaryOKLCH = getPrimaryOKLCH(); + const primaryHex = getPrimaryHex(); + const signature = `${primaryOKLCH?.L},${primaryOKLCH?.C},${primaryOKLCH?.h}`; if (signature === lastSignature) return; lastSignature = signature; - try { document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary } })); } catch {} + try { document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { } }; const bootstrap = () => { + // Initial setup - only run once on page load updatePalettes(); + + // Observer will handle all subsequent changes const mo = new MutationObserver(() => updatePalettes()); mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] }); - setInterval(updatePalettes, 400); + // Utility: choose high-contrast (or softened) text style against an arbitrary background color const pickTextStyleForBackground = (bgCss, opts = {}) => { const cssRoot = document.documentElement; @@ -175,13 +211,13 @@ if (!rgb) return null; return rgb; // already 0..1 }; - const mixRgb01 = (a, b, t) => ({ r: a.r*(1-t)+b.r*t, g: a.g*(1-t)+b.g*t, b: a.b*(1-t)+b.b*t }); + const mixRgb01 = (a, b, t) => ({ r: a.r * (1 - t) + b.r * t, g: a.g * (1 - t) + b.g * t, b: a.b * (1 - t) + b.b * t }); const relLum = (rgb) => { const f = (u) => srgbToLinear(u); - return 0.2126*f(rgb.r) + 0.7152*f(rgb.g) + 0.0722*f(rgb.b); + return 0.2126 * f(rgb.r) + 0.7152 * f(rgb.g) + 0.0722 * f(rgb.b); }; const contrast = (fg, bg) => { - const L1 = relLum(fg), L2 = relLum(bg); const a = Math.max(L1,L2), b = Math.min(L1,L2); + const L1 = relLum(fg), L2 = relLum(bg); const a = Math.max(L1, L2), b = Math.min(L1, L2); return (a + 0.05) / (b + 0.05); }; try { @@ -193,7 +229,7 @@ .filter(x => !!x.rgb); // Pick the max contrast let best = candidates[0]; let bestCR = contrast(best.rgb, bg); - for (let i=1;i bestCR) { best = candidates[i]; bestCR = cr; } } @@ -206,7 +242,7 @@ finalRgb = mixRgb01(best.rgb, mutedRgb, blend); } const haloStrength = Math.min(1, Math.max(0, Number(opts.haloStrength == null ? 0.5 : opts.haloStrength))); - const stroke = (best.css === '#000' || best.css.toLowerCase() === 'black') ? `rgba(255,255,255,${0.30 + 0.40*haloStrength})` : `rgba(0,0,0,${0.30 + 0.30*haloStrength})`; + const stroke = (best.css === '#000' || best.css.toLowerCase() === 'black') ? `rgba(255,255,255,${0.30 + 0.40 * haloStrength})` : `rgba(0,0,0,${0.30 + 0.30 * haloStrength})`; return { fill: toHex(finalRgb), stroke, strokeWidth: (opts.haloWidth == null ? 1 : Number(opts.haloWidth)) }; } catch { return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 }; @@ -214,14 +250,16 @@ }; window.ColorPalettes = { refresh: updatePalettes, - notify: () => { try { const primary = getPrimaryHex(); document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary } })); } catch {} }, + notify: () => { try { const primaryOKLCH = getPrimaryOKLCH(); const primaryHex = getPrimaryHex(); document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { } }, getPrimary: () => getPrimaryHex(), + getPrimaryOKLCH: () => getPrimaryOKLCH(), getColors: (key, count = 6) => { - const primary = getPrimaryHex(); + const primaryOKLCH = getPrimaryOKLCH(); + if (!primaryOKLCH) return []; const total = Math.max(1, Math.min(12, Number(count) || 6)); - if (key === 'categorical') return generators.categorical(primary, total); - if (key === 'sequential') return generators.sequential(primary, total); - if (key === 'diverging') return generators.diverging(primary, total); + if (key === 'categorical') return generators.categorical(primaryOKLCH, total); + if (key === 'sequential') return generators.sequential(primaryOKLCH, total); + if (key === 'diverging') return generators.diverging(primaryOKLCH, total); return []; }, getTextStyleForBackground: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {}), diff --git a/app/scripts/notion-importer/.notion-to-md/media/27877f1c-9c9d-804d-9c82-f7b3905578ff_media.json b/app/scripts/notion-importer/.notion-to-md/media/27877f1c-9c9d-804d-9c82-f7b3905578ff_media.json index 06b2ccebde122dee8e0ef4572c734a4c1b075421..1d87adc7d1c81ba31bf41cd3b5a94975be29c81e 100644 --- a/app/scripts/notion-importer/.notion-to-md/media/27877f1c-9c9d-804d-9c82-f7b3905578ff_media.json +++ b/app/scripts/notion-importer/.notion-to-md/media/27877f1c-9c9d-804d-9c82-f7b3905578ff_media.json @@ -1,198 +1,3 @@ -{ - "pageId": "27877f1c-9c9d-804d-9c82-f7b3905578ff", - "lastUpdated": "2025-10-08T13:00:26.065Z", - "mediaEntries": { - "27877f1c-9c9d-8078-b6da-c7a4c67c8f35": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/69940e63-ba02-4d6b-bc79-81dcab16590b/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466RQAHHWD2%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQCja3KUJzl59DJB4he%2BZc4EIAdSKZH6PHGkvSc5iTCA5gIhAK1O3OugR1Ap%2Fhuos6U7RbD2KxvfMHmfQXSU6K7IuEKgKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igw0pXUjiFqmEZtM2z8q3AMIdeJHjnKFMXfrzdsbRQMk5j3lsQc7tLMnmEPmHH%2FmX7sZrT4UjUkaKoeSyH%2Bg7wFpYmgvN77x8nGQ0nEQtZAPw2%2F3EH4nPTYF53xRmvyn7%2FMwloMmw7eBz7SIZ3Xk0aPYpbtVRoPUesefx7Jvddzmxu0Q%2FQpw0%2FrRCkLPJAnMdsR8fY3n8s8TBv7cVHHWkjpqgeaNKOD7O7Ej%2FNkVahgG%2BVi%2B8DlcuBG8FNk2EzDNroum71dNW6RzTO3ju65V5xHwLgmRkZRTKS%2FHeolCb01h51d%2BhJY03OORmJCMcOEYzPWg48LVGlFl%2FOA6OAhR%2FQ0eWs%2B1ZzKAz7HOSK6gHOhmcCj%2Bbve0%2FVaH2P708k33m5SQqjZQmfmO0JUCNn2WRPK9uYfSy64QFqpzeEXgAGiaNMQCrWjrLpO1TNcK457IQ4ofuJ9bt3be947z17Fa2pKdcescLFE7GdVtV5L4Nps5%2FoHCl0X1BxCPjpxN8zLRIQZtHFRloU8K6ebOrPFIb4bkjxfC9VidpyS0vnZ1DhFt8ssI1sOwrzRn5Kf%2FRltkKxvJPAp829yFUsrA35VhLZLllGpsdLzGVZS1GWkNqf0UxA5BZhdb9t4qPmp1WbBIufTnxcFuHKdUMxsdfTCxpZnHBjqkASugrWSTfsJxQU%2BvD6fKuut0yaNhG1Zscr6q%2FLX6mZAcKzCrMYWhsql27FMH5lEFZTsvBfNoX2h1ie3MPQw%2B67C4yNLYGIvBL%2BFPBfaw6qimvk48x5JDZBNsxN4fEMufvUM1%2FbemWiZZJSl8V5Q5h0NMxBmZFJkachxllAPWTTVPbSArY%2FYyX3vpKDGMi1fYZYT4IjNycyQDb6nSEKlqbnOjUosj&X-Amz-Signature=0e7a6d2cf89a76128b1e4ecd8e12053deb0a5e802a15e3e8c3d7979e2131fa09&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8078-b6da-c7a4c67c8f35.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8078-b6da-c7a4c67c8f35.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:35.892Z", - "updatedAt": "2025-10-08T13:00:25.689Z" - }, - "27877f1c-9c9d-8014-834f-d700b623256b": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/a11fbd23-10f8-4485-ad35-f3de9d480449/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466RQAHHWD2%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQCja3KUJzl59DJB4he%2BZc4EIAdSKZH6PHGkvSc5iTCA5gIhAK1O3OugR1Ap%2Fhuos6U7RbD2KxvfMHmfQXSU6K7IuEKgKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igw0pXUjiFqmEZtM2z8q3AMIdeJHjnKFMXfrzdsbRQMk5j3lsQc7tLMnmEPmHH%2FmX7sZrT4UjUkaKoeSyH%2Bg7wFpYmgvN77x8nGQ0nEQtZAPw2%2F3EH4nPTYF53xRmvyn7%2FMwloMmw7eBz7SIZ3Xk0aPYpbtVRoPUesefx7Jvddzmxu0Q%2FQpw0%2FrRCkLPJAnMdsR8fY3n8s8TBv7cVHHWkjpqgeaNKOD7O7Ej%2FNkVahgG%2BVi%2B8DlcuBG8FNk2EzDNroum71dNW6RzTO3ju65V5xHwLgmRkZRTKS%2FHeolCb01h51d%2BhJY03OORmJCMcOEYzPWg48LVGlFl%2FOA6OAhR%2FQ0eWs%2B1ZzKAz7HOSK6gHOhmcCj%2Bbve0%2FVaH2P708k33m5SQqjZQmfmO0JUCNn2WRPK9uYfSy64QFqpzeEXgAGiaNMQCrWjrLpO1TNcK457IQ4ofuJ9bt3be947z17Fa2pKdcescLFE7GdVtV5L4Nps5%2FoHCl0X1BxCPjpxN8zLRIQZtHFRloU8K6ebOrPFIb4bkjxfC9VidpyS0vnZ1DhFt8ssI1sOwrzRn5Kf%2FRltkKxvJPAp829yFUsrA35VhLZLllGpsdLzGVZS1GWkNqf0UxA5BZhdb9t4qPmp1WbBIufTnxcFuHKdUMxsdfTCxpZnHBjqkASugrWSTfsJxQU%2BvD6fKuut0yaNhG1Zscr6q%2FLX6mZAcKzCrMYWhsql27FMH5lEFZTsvBfNoX2h1ie3MPQw%2B67C4yNLYGIvBL%2BFPBfaw6qimvk48x5JDZBNsxN4fEMufvUM1%2FbemWiZZJSl8V5Q5h0NMxBmZFJkachxllAPWTTVPbSArY%2FYyX3vpKDGMi1fYZYT4IjNycyQDb6nSEKlqbnOjUosj&X-Amz-Signature=9145e81d85d35d531ffb6bbabad25d5846aa559613d371bc2880f6d02529b799&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8014-834f-d700b623256b.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8014-834f-d700b623256b.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:35.899Z", - "updatedAt": "2025-10-08T13:00:25.703Z" - }, - "27877f1c-9c9d-808d-9c6d-fae817ac8868": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/9b549e26-b527-4ad5-b1ad-3ed049747090/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466X3VEST5Q%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130020Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQC4Th0nobtVjj0GnPkA6aNua8I5iyMPP5wm3FPgOcNrpQIhAM6Cd1AyvJOWLCdgyh%2F9xxRIzhQ8ZEYJPtDoTzf9dVzsKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igwbq%2Fmpsk4PhTeoeU8q3ANUdDY8Un1V3sp1KnXAZQmxQggeyUGmjkp%2BsBgML%2Bh3jLesSMzdlQPFwlTBbqh74KOfU7NmNfHFLiHqWLrpeLClymtDa9BGRQAnfNGuT%2BUXXxp5dSoeOsbUJInqYhW4FsjUBRNw%2FSAslpIJKfTrmcN81%2FIUQNc0TRtVmiiXKPFUCNfWErOAQlec%2B6mNZCJYY1EbzUg1tPFNYYLpoiNb45%2F0t1VlsyeG6WlkA2KFXybguL2BmlWms%2BC%2BDeANHe2r%2Bq0h9JVtz%2BYBB3CzzQdeerNfGtyml%2BTz5DiBvv0h9F5FNcqJ4Uf1CbLGaRVIlCXLHam3xV3Fk%2FpYYQerVcK6t6YWWeRO0x92Uj1TyF1H4aCPO%2BuAjxDdY0WJfdLROdRvwH69zTdBYb26Lo7h2T2MnY%2FjJ4UNGPf0CWoGMVU9BJCO6%2Bm7RySlYPw1Axogb7clhfZAUYsM%2FhBI01n5OLo2mIl1dMcIxnZNc%2FFxp3njz6Jpth%2Fu0KOe8X%2FPTgM9S58jNUVTWHYd3bEm54SfyUlEIwmSVHkdHAvZw8QfhIC8DAJULbBkKHe6nRKxvGqo1UZW2OY0%2BKpOu7kPBwFl13lcqjMeSy%2F%2BvpZDgwHonNSKYiYRrlffBfL%2BUFWdiijebzDHpJnHBjqkAfpE6DXG5ducvq1TeQISVLspLOKXhLJ0Jn1auWbgWOv02xnmSsxVrzmbEGcfsyZArqlrtxdg9N9yH30onEToRB0OnUGFrpiazcXfmdl7JS0Cn3mL4FiEHdFC%2B2lUkrwFbKVX0NhXsjQRmkeUUtT7dXPTDyzpPIxoyghxvJsm6OswiSouBbg4NB6EQaFjgxRw9KEg79jjKbCgyYvpKNs56YVN9kvk&X-Amz-Signature=b9e650906f6978d3d01caebf5329c1571bfa97d53d88a3c571eaaa7b618970d2&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-808d-9c6d-fae817ac8868.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-808d-9c6d-fae817ac8868.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:35.943Z", - "updatedAt": "2025-10-08T13:00:25.727Z" - }, - "27877f1c-9c9d-8075-ae2e-dc24fe9296ca": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/943ada15-c18b-4434-ac96-cca2440119db/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466X3VEST5Q%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130020Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQC4Th0nobtVjj0GnPkA6aNua8I5iyMPP5wm3FPgOcNrpQIhAM6Cd1AyvJOWLCdgyh%2F9xxRIzhQ8ZEYJPtDoTzf9dVzsKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igwbq%2Fmpsk4PhTeoeU8q3ANUdDY8Un1V3sp1KnXAZQmxQggeyUGmjkp%2BsBgML%2Bh3jLesSMzdlQPFwlTBbqh74KOfU7NmNfHFLiHqWLrpeLClymtDa9BGRQAnfNGuT%2BUXXxp5dSoeOsbUJInqYhW4FsjUBRNw%2FSAslpIJKfTrmcN81%2FIUQNc0TRtVmiiXKPFUCNfWErOAQlec%2B6mNZCJYY1EbzUg1tPFNYYLpoiNb45%2F0t1VlsyeG6WlkA2KFXybguL2BmlWms%2BC%2BDeANHe2r%2Bq0h9JVtz%2BYBB3CzzQdeerNfGtyml%2BTz5DiBvv0h9F5FNcqJ4Uf1CbLGaRVIlCXLHam3xV3Fk%2FpYYQerVcK6t6YWWeRO0x92Uj1TyF1H4aCPO%2BuAjxDdY0WJfdLROdRvwH69zTdBYb26Lo7h2T2MnY%2FjJ4UNGPf0CWoGMVU9BJCO6%2Bm7RySlYPw1Axogb7clhfZAUYsM%2FhBI01n5OLo2mIl1dMcIxnZNc%2FFxp3njz6Jpth%2Fu0KOe8X%2FPTgM9S58jNUVTWHYd3bEm54SfyUlEIwmSVHkdHAvZw8QfhIC8DAJULbBkKHe6nRKxvGqo1UZW2OY0%2BKpOu7kPBwFl13lcqjMeSy%2F%2BvpZDgwHonNSKYiYRrlffBfL%2BUFWdiijebzDHpJnHBjqkAfpE6DXG5ducvq1TeQISVLspLOKXhLJ0Jn1auWbgWOv02xnmSsxVrzmbEGcfsyZArqlrtxdg9N9yH30onEToRB0OnUGFrpiazcXfmdl7JS0Cn3mL4FiEHdFC%2B2lUkrwFbKVX0NhXsjQRmkeUUtT7dXPTDyzpPIxoyghxvJsm6OswiSouBbg4NB6EQaFjgxRw9KEg79jjKbCgyYvpKNs56YVN9kvk&X-Amz-Signature=f6c6aeafa758351188cf92575d7adbac6d4438d61891b377861cceebe4699c16&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8075-ae2e-dc24fe9296ca.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8075-ae2e-dc24fe9296ca.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:36.095Z", - "updatedAt": "2025-10-08T13:00:25.926Z" - }, - "27877f1c-9c9d-801d-841a-e35011491566": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/3a912fb2-01eb-46d2-9687-41ffbb5b0446/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466Y2XCJFFW%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQDGr1WaO6mypVW00YGoLx4XK5HeMm7tg4Wxi4tRLT0sBQIhAJZB9sfEWXoL6QQ8WnamF77mePTGmWqiZjYqIQ1Ba5kMKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgyvfgauHIg52DwLqjMq3AOxwogx8uxNPXYdZuG3Giz0pcRPcVYGSzx%2FsCHi3wTJAjMOnK7oq5bOcT%2Fl2UzIP2n1RWhZZWYAbDJFjDsGnnxxjP1dA4W%2BG2JxtQjoyKDV24YvvyoKp%2B8c%2BVuzeJYc%2FAp0wja9UZjjmB6We9vy73l%2F0lzOEnuDWGvA3Wz7HN72nCAxRWfFw3VXoLc12NJA7jMXJpMTJ0kQhR54IefMuCgXQRkP9ThY944aJBcJDVVW2oPCliR0sNKWOwxmzYLfqTaCfVYnYWij0W2PFYWTl8O8Fz%2F6tJQeBzIzYZkQH%2B%2FID9QHo2H91PyVglygKK5nFfSUGmu5%2BlBHNZhF0tQ%2BkkSiJBO%2Fzpzh1l3tJ5%2FSDTS3OkCq9CgQS1LlOZ%2FWVGRa0dO%2BPKuLt2kSE3g%2BdENrh0uHMXL16JnD8tihVBQjDVDteb9ty3wYric3oz7UIdTbcFFhBvLNK2iaTTxgVVe8Iwec1Yh5%2Br70PVEtQ2RGpeJRNF%2FTCdNajTXZecTA4gfj3Dep1uEbLH6wJzKpoYff5ewJZaQ5NqIKjBbwZlP0ZsdmirA3vCFTiBuA%2FtxyQutgfCfqtxDzRYtRLdYtuXHnOJX19k0dW1qKTx%2FvJHjGhd%2BF7DDuSjbWdZfyMNBtvTCCpZnHBjqkAXWtzxhDxy981YikbANxxcBAJxQM7hXL4Lmn9JZH3hI2Z46Dd1xpYDzpq%2Bv0TjPTSsilTsyySr4C8rXZuw4BTrdyjlduhH34%2FNp16EC1u7PlP2iNue5eMeLdERldacspGGFk9eApo%2FSCPEUJdHN%2B514Zr0nBCSnGWEy9najlnY9KFSWwfrPpbEz0Z2MLkADQVvGJ4N1Aa7S4MU9m9WAxOA%2BBxMyX&X-Amz-Signature=fdbb11308e72b72824529e50a6386bf06785d0576e5c27d6cf6f609380614733&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-801d-841a-e35011491566.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-801d-841a-e35011491566.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:36.121Z", - "updatedAt": "2025-10-08T13:00:25.898Z" - }, - "27877f1c-9c9d-8048-9b7e-db4fa7485915": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/58651851-13c8-4e99-92e3-857c8e3d305b/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466X3VEST5Q%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130020Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQC4Th0nobtVjj0GnPkA6aNua8I5iyMPP5wm3FPgOcNrpQIhAM6Cd1AyvJOWLCdgyh%2F9xxRIzhQ8ZEYJPtDoTzf9dVzsKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igwbq%2Fmpsk4PhTeoeU8q3ANUdDY8Un1V3sp1KnXAZQmxQggeyUGmjkp%2BsBgML%2Bh3jLesSMzdlQPFwlTBbqh74KOfU7NmNfHFLiHqWLrpeLClymtDa9BGRQAnfNGuT%2BUXXxp5dSoeOsbUJInqYhW4FsjUBRNw%2FSAslpIJKfTrmcN81%2FIUQNc0TRtVmiiXKPFUCNfWErOAQlec%2B6mNZCJYY1EbzUg1tPFNYYLpoiNb45%2F0t1VlsyeG6WlkA2KFXybguL2BmlWms%2BC%2BDeANHe2r%2Bq0h9JVtz%2BYBB3CzzQdeerNfGtyml%2BTz5DiBvv0h9F5FNcqJ4Uf1CbLGaRVIlCXLHam3xV3Fk%2FpYYQerVcK6t6YWWeRO0x92Uj1TyF1H4aCPO%2BuAjxDdY0WJfdLROdRvwH69zTdBYb26Lo7h2T2MnY%2FjJ4UNGPf0CWoGMVU9BJCO6%2Bm7RySlYPw1Axogb7clhfZAUYsM%2FhBI01n5OLo2mIl1dMcIxnZNc%2FFxp3njz6Jpth%2Fu0KOe8X%2FPTgM9S58jNUVTWHYd3bEm54SfyUlEIwmSVHkdHAvZw8QfhIC8DAJULbBkKHe6nRKxvGqo1UZW2OY0%2BKpOu7kPBwFl13lcqjMeSy%2F%2BvpZDgwHonNSKYiYRrlffBfL%2BUFWdiijebzDHpJnHBjqkAfpE6DXG5ducvq1TeQISVLspLOKXhLJ0Jn1auWbgWOv02xnmSsxVrzmbEGcfsyZArqlrtxdg9N9yH30onEToRB0OnUGFrpiazcXfmdl7JS0Cn3mL4FiEHdFC%2B2lUkrwFbKVX0NhXsjQRmkeUUtT7dXPTDyzpPIxoyghxvJsm6OswiSouBbg4NB6EQaFjgxRw9KEg79jjKbCgyYvpKNs56YVN9kvk&X-Amz-Signature=f13847b038a80822deb40d3e0873722dc40cb227be4a6edfdb70d27be0794729&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8048-9b7e-db4fa7485915.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8048-9b7e-db4fa7485915.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:36.136Z", - "updatedAt": "2025-10-08T13:00:25.917Z" - }, - "27877f1c-9c9d-804d-bd0a-e0b1c15e504f": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/3fbc7e6c-8ec0-4ef5-9161-7aef75930323/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466RQAHHWD2%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130014Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQCja3KUJzl59DJB4he%2BZc4EIAdSKZH6PHGkvSc5iTCA5gIhAK1O3OugR1Ap%2Fhuos6U7RbD2KxvfMHmfQXSU6K7IuEKgKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igw0pXUjiFqmEZtM2z8q3AMIdeJHjnKFMXfrzdsbRQMk5j3lsQc7tLMnmEPmHH%2FmX7sZrT4UjUkaKoeSyH%2Bg7wFpYmgvN77x8nGQ0nEQtZAPw2%2F3EH4nPTYF53xRmvyn7%2FMwloMmw7eBz7SIZ3Xk0aPYpbtVRoPUesefx7Jvddzmxu0Q%2FQpw0%2FrRCkLPJAnMdsR8fY3n8s8TBv7cVHHWkjpqgeaNKOD7O7Ej%2FNkVahgG%2BVi%2B8DlcuBG8FNk2EzDNroum71dNW6RzTO3ju65V5xHwLgmRkZRTKS%2FHeolCb01h51d%2BhJY03OORmJCMcOEYzPWg48LVGlFl%2FOA6OAhR%2FQ0eWs%2B1ZzKAz7HOSK6gHOhmcCj%2Bbve0%2FVaH2P708k33m5SQqjZQmfmO0JUCNn2WRPK9uYfSy64QFqpzeEXgAGiaNMQCrWjrLpO1TNcK457IQ4ofuJ9bt3be947z17Fa2pKdcescLFE7GdVtV5L4Nps5%2FoHCl0X1BxCPjpxN8zLRIQZtHFRloU8K6ebOrPFIb4bkjxfC9VidpyS0vnZ1DhFt8ssI1sOwrzRn5Kf%2FRltkKxvJPAp829yFUsrA35VhLZLllGpsdLzGVZS1GWkNqf0UxA5BZhdb9t4qPmp1WbBIufTnxcFuHKdUMxsdfTCxpZnHBjqkASugrWSTfsJxQU%2BvD6fKuut0yaNhG1Zscr6q%2FLX6mZAcKzCrMYWhsql27FMH5lEFZTsvBfNoX2h1ie3MPQw%2B67C4yNLYGIvBL%2BFPBfaw6qimvk48x5JDZBNsxN4fEMufvUM1%2FbemWiZZJSl8V5Q5h0NMxBmZFJkachxllAPWTTVPbSArY%2FYyX3vpKDGMi1fYZYT4IjNycyQDb6nSEKlqbnOjUosj&X-Amz-Signature=fd7ef992dc4f3a290a5f04a0a640fe2828b5a33487682c4956913fe674c9c017&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-804d-bd0a-e0b1c15e504f.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-804d-bd0a-e0b1c15e504f.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:36.244Z", - "updatedAt": "2025-10-08T13:00:26.009Z" - }, - "27877f1c-9c9d-80b9-8cfb-f0a6aaaa8760": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/d3899f55-47ba-45b0-be0f-9abc5bb4f247/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466RQAHHWD2%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQCja3KUJzl59DJB4he%2BZc4EIAdSKZH6PHGkvSc5iTCA5gIhAK1O3OugR1Ap%2Fhuos6U7RbD2KxvfMHmfQXSU6K7IuEKgKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igw0pXUjiFqmEZtM2z8q3AMIdeJHjnKFMXfrzdsbRQMk5j3lsQc7tLMnmEPmHH%2FmX7sZrT4UjUkaKoeSyH%2Bg7wFpYmgvN77x8nGQ0nEQtZAPw2%2F3EH4nPTYF53xRmvyn7%2FMwloMmw7eBz7SIZ3Xk0aPYpbtVRoPUesefx7Jvddzmxu0Q%2FQpw0%2FrRCkLPJAnMdsR8fY3n8s8TBv7cVHHWkjpqgeaNKOD7O7Ej%2FNkVahgG%2BVi%2B8DlcuBG8FNk2EzDNroum71dNW6RzTO3ju65V5xHwLgmRkZRTKS%2FHeolCb01h51d%2BhJY03OORmJCMcOEYzPWg48LVGlFl%2FOA6OAhR%2FQ0eWs%2B1ZzKAz7HOSK6gHOhmcCj%2Bbve0%2FVaH2P708k33m5SQqjZQmfmO0JUCNn2WRPK9uYfSy64QFqpzeEXgAGiaNMQCrWjrLpO1TNcK457IQ4ofuJ9bt3be947z17Fa2pKdcescLFE7GdVtV5L4Nps5%2FoHCl0X1BxCPjpxN8zLRIQZtHFRloU8K6ebOrPFIb4bkjxfC9VidpyS0vnZ1DhFt8ssI1sOwrzRn5Kf%2FRltkKxvJPAp829yFUsrA35VhLZLllGpsdLzGVZS1GWkNqf0UxA5BZhdb9t4qPmp1WbBIufTnxcFuHKdUMxsdfTCxpZnHBjqkASugrWSTfsJxQU%2BvD6fKuut0yaNhG1Zscr6q%2FLX6mZAcKzCrMYWhsql27FMH5lEFZTsvBfNoX2h1ie3MPQw%2B67C4yNLYGIvBL%2BFPBfaw6qimvk48x5JDZBNsxN4fEMufvUM1%2FbemWiZZJSl8V5Q5h0NMxBmZFJkachxllAPWTTVPbSArY%2FYyX3vpKDGMi1fYZYT4IjNycyQDb6nSEKlqbnOjUosj&X-Amz-Signature=0e702a4db1c4dabdd69b83b59535958cd39a3e2906332aa607e61864797bca59&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80b9-8cfb-f0a6aaaa8760.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80b9-8cfb-f0a6aaaa8760.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:36.247Z", - "updatedAt": "2025-10-08T13:00:26.000Z" - }, - "27877f1c-9c9d-80aa-b968-c54c9fe7e5d7": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/1f240ffd-b4d7-422a-9a81-0aabda1f6c16/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466Y2XCJFFW%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQDGr1WaO6mypVW00YGoLx4XK5HeMm7tg4Wxi4tRLT0sBQIhAJZB9sfEWXoL6QQ8WnamF77mePTGmWqiZjYqIQ1Ba5kMKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgyvfgauHIg52DwLqjMq3AOxwogx8uxNPXYdZuG3Giz0pcRPcVYGSzx%2FsCHi3wTJAjMOnK7oq5bOcT%2Fl2UzIP2n1RWhZZWYAbDJFjDsGnnxxjP1dA4W%2BG2JxtQjoyKDV24YvvyoKp%2B8c%2BVuzeJYc%2FAp0wja9UZjjmB6We9vy73l%2F0lzOEnuDWGvA3Wz7HN72nCAxRWfFw3VXoLc12NJA7jMXJpMTJ0kQhR54IefMuCgXQRkP9ThY944aJBcJDVVW2oPCliR0sNKWOwxmzYLfqTaCfVYnYWij0W2PFYWTl8O8Fz%2F6tJQeBzIzYZkQH%2B%2FID9QHo2H91PyVglygKK5nFfSUGmu5%2BlBHNZhF0tQ%2BkkSiJBO%2Fzpzh1l3tJ5%2FSDTS3OkCq9CgQS1LlOZ%2FWVGRa0dO%2BPKuLt2kSE3g%2BdENrh0uHMXL16JnD8tihVBQjDVDteb9ty3wYric3oz7UIdTbcFFhBvLNK2iaTTxgVVe8Iwec1Yh5%2Br70PVEtQ2RGpeJRNF%2FTCdNajTXZecTA4gfj3Dep1uEbLH6wJzKpoYff5ewJZaQ5NqIKjBbwZlP0ZsdmirA3vCFTiBuA%2FtxyQutgfCfqtxDzRYtRLdYtuXHnOJX19k0dW1qKTx%2FvJHjGhd%2BF7DDuSjbWdZfyMNBtvTCCpZnHBjqkAXWtzxhDxy981YikbANxxcBAJxQM7hXL4Lmn9JZH3hI2Z46Dd1xpYDzpq%2Bv0TjPTSsilTsyySr4C8rXZuw4BTrdyjlduhH34%2FNp16EC1u7PlP2iNue5eMeLdERldacspGGFk9eApo%2FSCPEUJdHN%2B514Zr0nBCSnGWEy9najlnY9KFSWwfrPpbEz0Z2MLkADQVvGJ4N1Aa7S4MU9m9WAxOA%2BBxMyX&X-Amz-Signature=c353bf2bf0d4ef95c66531df5c779035a90add76e2c286e9e26d41c1a6526eb9&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80aa-b968-c54c9fe7e5d7.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80aa-b968-c54c9fe7e5d7.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:36.247Z", - "updatedAt": "2025-10-08T13:00:26.061Z" - }, - "27877f1c-9c9d-8013-b668-f14bd1ac0ec0": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/cb01b334-89a5-436f-a141-b7c310d25b57/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466Y2XCJFFW%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQDGr1WaO6mypVW00YGoLx4XK5HeMm7tg4Wxi4tRLT0sBQIhAJZB9sfEWXoL6QQ8WnamF77mePTGmWqiZjYqIQ1Ba5kMKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgyvfgauHIg52DwLqjMq3AOxwogx8uxNPXYdZuG3Giz0pcRPcVYGSzx%2FsCHi3wTJAjMOnK7oq5bOcT%2Fl2UzIP2n1RWhZZWYAbDJFjDsGnnxxjP1dA4W%2BG2JxtQjoyKDV24YvvyoKp%2B8c%2BVuzeJYc%2FAp0wja9UZjjmB6We9vy73l%2F0lzOEnuDWGvA3Wz7HN72nCAxRWfFw3VXoLc12NJA7jMXJpMTJ0kQhR54IefMuCgXQRkP9ThY944aJBcJDVVW2oPCliR0sNKWOwxmzYLfqTaCfVYnYWij0W2PFYWTl8O8Fz%2F6tJQeBzIzYZkQH%2B%2FID9QHo2H91PyVglygKK5nFfSUGmu5%2BlBHNZhF0tQ%2BkkSiJBO%2Fzpzh1l3tJ5%2FSDTS3OkCq9CgQS1LlOZ%2FWVGRa0dO%2BPKuLt2kSE3g%2BdENrh0uHMXL16JnD8tihVBQjDVDteb9ty3wYric3oz7UIdTbcFFhBvLNK2iaTTxgVVe8Iwec1Yh5%2Br70PVEtQ2RGpeJRNF%2FTCdNajTXZecTA4gfj3Dep1uEbLH6wJzKpoYff5ewJZaQ5NqIKjBbwZlP0ZsdmirA3vCFTiBuA%2FtxyQutgfCfqtxDzRYtRLdYtuXHnOJX19k0dW1qKTx%2FvJHjGhd%2BF7DDuSjbWdZfyMNBtvTCCpZnHBjqkAXWtzxhDxy981YikbANxxcBAJxQM7hXL4Lmn9JZH3hI2Z46Dd1xpYDzpq%2Bv0TjPTSsilTsyySr4C8rXZuw4BTrdyjlduhH34%2FNp16EC1u7PlP2iNue5eMeLdERldacspGGFk9eApo%2FSCPEUJdHN%2B514Zr0nBCSnGWEy9najlnY9KFSWwfrPpbEz0Z2MLkADQVvGJ4N1Aa7S4MU9m9WAxOA%2BBxMyX&X-Amz-Signature=ff0a0599c70fd9f202d55e4a7e887de8d8727724550d0e9faf6df28e0e9b7696&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8013-b668-f14bd1ac0ec0.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8013-b668-f14bd1ac0ec0.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:36.250Z", - "updatedAt": "2025-10-08T13:00:26.056Z" - }, - "27877f1c-9c9d-80e9-b729-dbd328930bed": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/9d723e06-252f-44e3-844e-1410fa92667a/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466RQAHHWD2%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQCja3KUJzl59DJB4he%2BZc4EIAdSKZH6PHGkvSc5iTCA5gIhAK1O3OugR1Ap%2Fhuos6U7RbD2KxvfMHmfQXSU6K7IuEKgKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igw0pXUjiFqmEZtM2z8q3AMIdeJHjnKFMXfrzdsbRQMk5j3lsQc7tLMnmEPmHH%2FmX7sZrT4UjUkaKoeSyH%2Bg7wFpYmgvN77x8nGQ0nEQtZAPw2%2F3EH4nPTYF53xRmvyn7%2FMwloMmw7eBz7SIZ3Xk0aPYpbtVRoPUesefx7Jvddzmxu0Q%2FQpw0%2FrRCkLPJAnMdsR8fY3n8s8TBv7cVHHWkjpqgeaNKOD7O7Ej%2FNkVahgG%2BVi%2B8DlcuBG8FNk2EzDNroum71dNW6RzTO3ju65V5xHwLgmRkZRTKS%2FHeolCb01h51d%2BhJY03OORmJCMcOEYzPWg48LVGlFl%2FOA6OAhR%2FQ0eWs%2B1ZzKAz7HOSK6gHOhmcCj%2Bbve0%2FVaH2P708k33m5SQqjZQmfmO0JUCNn2WRPK9uYfSy64QFqpzeEXgAGiaNMQCrWjrLpO1TNcK457IQ4ofuJ9bt3be947z17Fa2pKdcescLFE7GdVtV5L4Nps5%2FoHCl0X1BxCPjpxN8zLRIQZtHFRloU8K6ebOrPFIb4bkjxfC9VidpyS0vnZ1DhFt8ssI1sOwrzRn5Kf%2FRltkKxvJPAp829yFUsrA35VhLZLllGpsdLzGVZS1GWkNqf0UxA5BZhdb9t4qPmp1WbBIufTnxcFuHKdUMxsdfTCxpZnHBjqkASugrWSTfsJxQU%2BvD6fKuut0yaNhG1Zscr6q%2FLX6mZAcKzCrMYWhsql27FMH5lEFZTsvBfNoX2h1ie3MPQw%2B67C4yNLYGIvBL%2BFPBfaw6qimvk48x5JDZBNsxN4fEMufvUM1%2FbemWiZZJSl8V5Q5h0NMxBmZFJkachxllAPWTTVPbSArY%2FYyX3vpKDGMi1fYZYT4IjNycyQDb6nSEKlqbnOjUosj&X-Amz-Signature=4faf0d241e315ba80e5f56bb4cb410229c15e429fcab990fd1ebc2b17e2e4388&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80e9-b729-dbd328930bed.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80e9-b729-dbd328930bed.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:36.251Z", - "updatedAt": "2025-10-08T13:00:26.048Z" - }, - "27877f1c-9c9d-80a9-b4d0-f2129716632d": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/137c2c51-6f89-437f-ba45-3a844c747b7a/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466W5FHSQK6%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130018Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJGMEQCIAhuEUuJdQLuRcUIqWrlF%2FxJVRmDyl6u61kVLhtjlfZUAiBV%2Fg6BRYZGv8gjl2KUiGhGUm6oydw9R%2BGbL4YBC8orxyqIBAi9%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAAaDDYzNzQyMzE4MzgwNSIM3uBXzGO9Ig0qNGY4KtwD8SFvKEBtLvytudTAIznpwZ0LlbKJs50UtNCuHXZMaMCWVzh3bh5s1YUDv9F8QzpxpmwoEHNA0zdieOQ0RTayznMP%2FQonTMVpq110QUSpjw0oiOQAObBO%2FZhOW3eVIvyERdtG8hJ%2FFdtOx68HiwShjXQNIx%2FTLP%2FyKsamFu97bNFKLLbXaKEQL%2FPeYBMfUh1FOikwzUsWtnX6xkIyDZlzyvR5XfiR93IkhPKAe3tRogdfHErVfaMhWD4CLC3CotugLqYLChLu2FEgjljFpEHc5u5od5FnVy9AUpcA9X21VVFEM5ZdQ2e3RAB6wIMQvIaYTHHCnFFfouA8nRqcEVzWysN3qwh2ilXM%2FkIfZN7fRIxbBSPLyUIVcJpZNwbzs8d5bs3qvzM9G8FkzDvtYML3JOdEzF4LuxAAJJLAqhTTQ1PhnB4lalWW%2BzGBluxdvxfidxr7w8b74x1A6sr2RRg204TCYmbv8epUSs%2BRfkjbpQb25T59MrJnDDkvR3vwM0uRiYOcx7BIJoJQ9NABbDhUdQivPKSx%2FWfUI4qzy2Qu3ylcwrudYUrkObXxW8EpCrpCr3q7hWCPkKfvLhioYojN04XKEoAr9eJNJKYu8%2BGtS2%2FHsFqf94Soxz4NYa8wsaWZxwY6pgEneyRps7aPn1nFrZvbD3DUmmNVnG7Ot90eZ9AeaSCgE0a8LPo2EBJqYduCTOFx0ZS31WSpymmuKKyjsI94sx8u%2F5Mmvyc%2F6g6F%2FSGga8Z7F5NIPVciGCTsa2iZFLblK4LGKDfvx5RgoyuAatccTNlNYca%2F5w9iQ1JXOu83l39OW1O54Xr5rX96kIiak60h5LxJgIHU3zeBdD4UeGH39LaRoJyMksdX&X-Amz-Signature=3e45baae308e0004d8e62bc03d6e459d6310e19ae4935db52491727bc24231cb&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80a9-b4d0-f2129716632d.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80a9-b4d0-f2129716632d.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:36.257Z", - "updatedAt": "2025-10-08T13:00:26.048Z" - }, - "27877f1c-9c9d-80b6-be07-e8646502f82a": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/843a4edd-435d-45ca-8212-1af31305fb3a/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466RQAHHWD2%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQCja3KUJzl59DJB4he%2BZc4EIAdSKZH6PHGkvSc5iTCA5gIhAK1O3OugR1Ap%2Fhuos6U7RbD2KxvfMHmfQXSU6K7IuEKgKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igw0pXUjiFqmEZtM2z8q3AMIdeJHjnKFMXfrzdsbRQMk5j3lsQc7tLMnmEPmHH%2FmX7sZrT4UjUkaKoeSyH%2Bg7wFpYmgvN77x8nGQ0nEQtZAPw2%2F3EH4nPTYF53xRmvyn7%2FMwloMmw7eBz7SIZ3Xk0aPYpbtVRoPUesefx7Jvddzmxu0Q%2FQpw0%2FrRCkLPJAnMdsR8fY3n8s8TBv7cVHHWkjpqgeaNKOD7O7Ej%2FNkVahgG%2BVi%2B8DlcuBG8FNk2EzDNroum71dNW6RzTO3ju65V5xHwLgmRkZRTKS%2FHeolCb01h51d%2BhJY03OORmJCMcOEYzPWg48LVGlFl%2FOA6OAhR%2FQ0eWs%2B1ZzKAz7HOSK6gHOhmcCj%2Bbve0%2FVaH2P708k33m5SQqjZQmfmO0JUCNn2WRPK9uYfSy64QFqpzeEXgAGiaNMQCrWjrLpO1TNcK457IQ4ofuJ9bt3be947z17Fa2pKdcescLFE7GdVtV5L4Nps5%2FoHCl0X1BxCPjpxN8zLRIQZtHFRloU8K6ebOrPFIb4bkjxfC9VidpyS0vnZ1DhFt8ssI1sOwrzRn5Kf%2FRltkKxvJPAp829yFUsrA35VhLZLllGpsdLzGVZS1GWkNqf0UxA5BZhdb9t4qPmp1WbBIufTnxcFuHKdUMxsdfTCxpZnHBjqkASugrWSTfsJxQU%2BvD6fKuut0yaNhG1Zscr6q%2FLX6mZAcKzCrMYWhsql27FMH5lEFZTsvBfNoX2h1ie3MPQw%2B67C4yNLYGIvBL%2BFPBfaw6qimvk48x5JDZBNsxN4fEMufvUM1%2FbemWiZZJSl8V5Q5h0NMxBmZFJkachxllAPWTTVPbSArY%2FYyX3vpKDGMi1fYZYT4IjNycyQDb6nSEKlqbnOjUosj&X-Amz-Signature=6b41be5cc09cf3fae5304c63a3291650e24c6ad9285ba21202c90a186c8be390&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80b6-be07-e8646502f82a.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80b6-be07-e8646502f82a.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:36.274Z", - "updatedAt": "2025-10-08T13:00:25.995Z" - }, - "27877f1c-9c9d-808f-b712-c7c608da3fc6": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/33327526-41c3-4e9c-b4ee-c7cc3c23c7ca/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466Y2XCJFFW%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130015Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQDGr1WaO6mypVW00YGoLx4XK5HeMm7tg4Wxi4tRLT0sBQIhAJZB9sfEWXoL6QQ8WnamF77mePTGmWqiZjYqIQ1Ba5kMKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1IgyvfgauHIg52DwLqjMq3AOxwogx8uxNPXYdZuG3Giz0pcRPcVYGSzx%2FsCHi3wTJAjMOnK7oq5bOcT%2Fl2UzIP2n1RWhZZWYAbDJFjDsGnnxxjP1dA4W%2BG2JxtQjoyKDV24YvvyoKp%2B8c%2BVuzeJYc%2FAp0wja9UZjjmB6We9vy73l%2F0lzOEnuDWGvA3Wz7HN72nCAxRWfFw3VXoLc12NJA7jMXJpMTJ0kQhR54IefMuCgXQRkP9ThY944aJBcJDVVW2oPCliR0sNKWOwxmzYLfqTaCfVYnYWij0W2PFYWTl8O8Fz%2F6tJQeBzIzYZkQH%2B%2FID9QHo2H91PyVglygKK5nFfSUGmu5%2BlBHNZhF0tQ%2BkkSiJBO%2Fzpzh1l3tJ5%2FSDTS3OkCq9CgQS1LlOZ%2FWVGRa0dO%2BPKuLt2kSE3g%2BdENrh0uHMXL16JnD8tihVBQjDVDteb9ty3wYric3oz7UIdTbcFFhBvLNK2iaTTxgVVe8Iwec1Yh5%2Br70PVEtQ2RGpeJRNF%2FTCdNajTXZecTA4gfj3Dep1uEbLH6wJzKpoYff5ewJZaQ5NqIKjBbwZlP0ZsdmirA3vCFTiBuA%2FtxyQutgfCfqtxDzRYtRLdYtuXHnOJX19k0dW1qKTx%2FvJHjGhd%2BF7DDuSjbWdZfyMNBtvTCCpZnHBjqkAXWtzxhDxy981YikbANxxcBAJxQM7hXL4Lmn9JZH3hI2Z46Dd1xpYDzpq%2Bv0TjPTSsilTsyySr4C8rXZuw4BTrdyjlduhH34%2FNp16EC1u7PlP2iNue5eMeLdERldacspGGFk9eApo%2FSCPEUJdHN%2B514Zr0nBCSnGWEy9najlnY9KFSWwfrPpbEz0Z2MLkADQVvGJ4N1Aa7S4MU9m9WAxOA%2BBxMyX&X-Amz-Signature=68518895319bebfef33cc5c5786ea73b408d55f058fe1b07a6e057f3167389dc&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-808f-b712-c7c608da3fc6.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-808f-b712-c7c608da3fc6.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:36.274Z", - "updatedAt": "2025-10-08T13:00:25.863Z" - }, - "27877f1c-9c9d-8031-ac8d-c5678af1bdd5": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/16831579-d887-4894-b36a-d4df8a134653/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466X3VEST5Q%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130020Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQC4Th0nobtVjj0GnPkA6aNua8I5iyMPP5wm3FPgOcNrpQIhAM6Cd1AyvJOWLCdgyh%2F9xxRIzhQ8ZEYJPtDoTzf9dVzsKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igwbq%2Fmpsk4PhTeoeU8q3ANUdDY8Un1V3sp1KnXAZQmxQggeyUGmjkp%2BsBgML%2Bh3jLesSMzdlQPFwlTBbqh74KOfU7NmNfHFLiHqWLrpeLClymtDa9BGRQAnfNGuT%2BUXXxp5dSoeOsbUJInqYhW4FsjUBRNw%2FSAslpIJKfTrmcN81%2FIUQNc0TRtVmiiXKPFUCNfWErOAQlec%2B6mNZCJYY1EbzUg1tPFNYYLpoiNb45%2F0t1VlsyeG6WlkA2KFXybguL2BmlWms%2BC%2BDeANHe2r%2Bq0h9JVtz%2BYBB3CzzQdeerNfGtyml%2BTz5DiBvv0h9F5FNcqJ4Uf1CbLGaRVIlCXLHam3xV3Fk%2FpYYQerVcK6t6YWWeRO0x92Uj1TyF1H4aCPO%2BuAjxDdY0WJfdLROdRvwH69zTdBYb26Lo7h2T2MnY%2FjJ4UNGPf0CWoGMVU9BJCO6%2Bm7RySlYPw1Axogb7clhfZAUYsM%2FhBI01n5OLo2mIl1dMcIxnZNc%2FFxp3njz6Jpth%2Fu0KOe8X%2FPTgM9S58jNUVTWHYd3bEm54SfyUlEIwmSVHkdHAvZw8QfhIC8DAJULbBkKHe6nRKxvGqo1UZW2OY0%2BKpOu7kPBwFl13lcqjMeSy%2F%2BvpZDgwHonNSKYiYRrlffBfL%2BUFWdiijebzDHpJnHBjqkAfpE6DXG5ducvq1TeQISVLspLOKXhLJ0Jn1auWbgWOv02xnmSsxVrzmbEGcfsyZArqlrtxdg9N9yH30onEToRB0OnUGFrpiazcXfmdl7JS0Cn3mL4FiEHdFC%2B2lUkrwFbKVX0NhXsjQRmkeUUtT7dXPTDyzpPIxoyghxvJsm6OswiSouBbg4NB6EQaFjgxRw9KEg79jjKbCgyYvpKNs56YVN9kvk&X-Amz-Signature=0e07c27075a366f43cf237c80660a72ba43bdbad3486e7f1f9e3a12afdfcc7f0&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8031-ac8d-c5678af1bdd5.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-8031-ac8d-c5678af1bdd5.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:36.276Z", - "updatedAt": "2025-10-08T13:00:25.920Z" - }, - "27877f1c-9c9d-80e7-a500-fb79cebde7e3": { - "mediaInfo": { - "type": "DOWNLOAD", - "originalUrl": "https://prod-files-secure.s3.us-west-2.amazonaws.com/6fe77f1c-9c9d-81b1-b174-00031f2bc702/0458ed42-9805-4c8f-9d7e-c675474a626e/image.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=ASIAZI2LB466X3VEST5Q%2F20251008%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20251008T130020Z&X-Amz-Expires=3600&X-Amz-Security-Token=IQoJb3JpZ2luX2VjECQaCXVzLXdlc3QtMiJIMEYCIQC4Th0nobtVjj0GnPkA6aNua8I5iyMPP5wm3FPgOcNrpQIhAM6Cd1AyvJOWLCdgyh%2F9xxRIzhQ8ZEYJPtDoTzf9dVzsKogECL3%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEQABoMNjM3NDIzMTgzODA1Igwbq%2Fmpsk4PhTeoeU8q3ANUdDY8Un1V3sp1KnXAZQmxQggeyUGmjkp%2BsBgML%2Bh3jLesSMzdlQPFwlTBbqh74KOfU7NmNfHFLiHqWLrpeLClymtDa9BGRQAnfNGuT%2BUXXxp5dSoeOsbUJInqYhW4FsjUBRNw%2FSAslpIJKfTrmcN81%2FIUQNc0TRtVmiiXKPFUCNfWErOAQlec%2B6mNZCJYY1EbzUg1tPFNYYLpoiNb45%2F0t1VlsyeG6WlkA2KFXybguL2BmlWms%2BC%2BDeANHe2r%2Bq0h9JVtz%2BYBB3CzzQdeerNfGtyml%2BTz5DiBvv0h9F5FNcqJ4Uf1CbLGaRVIlCXLHam3xV3Fk%2FpYYQerVcK6t6YWWeRO0x92Uj1TyF1H4aCPO%2BuAjxDdY0WJfdLROdRvwH69zTdBYb26Lo7h2T2MnY%2FjJ4UNGPf0CWoGMVU9BJCO6%2Bm7RySlYPw1Axogb7clhfZAUYsM%2FhBI01n5OLo2mIl1dMcIxnZNc%2FFxp3njz6Jpth%2Fu0KOe8X%2FPTgM9S58jNUVTWHYd3bEm54SfyUlEIwmSVHkdHAvZw8QfhIC8DAJULbBkKHe6nRKxvGqo1UZW2OY0%2BKpOu7kPBwFl13lcqjMeSy%2F%2BvpZDgwHonNSKYiYRrlffBfL%2BUFWdiijebzDHpJnHBjqkAfpE6DXG5ducvq1TeQISVLspLOKXhLJ0Jn1auWbgWOv02xnmSsxVrzmbEGcfsyZArqlrtxdg9N9yH30onEToRB0OnUGFrpiazcXfmdl7JS0Cn3mL4FiEHdFC%2B2lUkrwFbKVX0NhXsjQRmkeUUtT7dXPTDyzpPIxoyghxvJsm6OswiSouBbg4NB6EQaFjgxRw9KEg79jjKbCgyYvpKNs56YVN9kvk&X-Amz-Signature=e43c1f35e9930e144c0b2b6098457be4848a723301fd78f3ff27d94f87ccbebb&X-Amz-SignedHeaders=host&x-amz-checksum-mode=ENABLED&x-id=GetObject", - "localPath": "/Users/thibaudfrere/Documents/work-projects/huggingface/research-article-template/app/scripts/notion-importer/output/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80e7-a500-fb79cebde7e3.png", - "sourceType": "block", - "transformedPath": "/media/27877f1c9c9d804d9c82f7b3905578ff/image_27877f1c-9c9d-80e7-a500-fb79cebde7e3.png" - }, - "lastEdited": "2025-09-24T09:22:00.000Z", - "createdAt": "2025-09-24T09:36:36.448Z", - "updatedAt": "2025-10-08T13:00:26.065Z" - } - } -} \ No newline at end of file +version https://git-lfs.github.com/spec/v1 +oid sha256:c282e7bcb40ee2caafda422b3614d996d6023dbb4bbe10f96521348ee151aeb0 +size 36969 diff --git a/app/scripts/notion-importer/README.md b/app/scripts/notion-importer/README.md index 2eff386123fece21f7dde934ea0b7ec2f7e329ee..96f8ea895862954f0fa0801e2815b8627029374f 100644 --- a/app/scripts/notion-importer/README.md +++ b/app/scripts/notion-importer/README.md @@ -4,25 +4,6 @@ Complete Notion to MDX (Markdown + JSX) importer optimized for Astro with advanc ## 🚀 Quick Start -### Method 1: Using NOTION_PAGE_ID (Recommended) - -```bash -# Install dependencies -npm install - -# Setup environment variables -cp env.example .env -# Edit .env with your Notion token and page ID - -# Complete Notion → MDX conversion (fetches title/slug automatically) -NOTION_TOKEN=secret_xxx NOTION_PAGE_ID=abc123 node index.mjs - -# Or use .env file -node index.mjs -``` - -### Method 2: Using pages.json (Legacy) - ```bash # Install dependencies npm install @@ -31,18 +12,7 @@ npm install cp env.example .env # Edit .env with your Notion token -# Configure pages in input/pages.json -# { -# "pages": [ -# { -# "id": "your-page-id", -# "title": "Title", -# "slug": "slug" -# } -# ] -# } - -# Complete Notion → MDX conversion +# Complete Notion → MDX conversion with all features node index.mjs # For step-by-step debugging @@ -73,7 +43,7 @@ notion-importer/ ### 🎯 **Advanced Media Handling** - **Local download**: Automatic download of all Notion media (images, files, PDFs) - **Path transformation**: Smart path conversion for web accessibility -- **Image components**: Automatic conversion to Astro `Image` components with zoom/download +- **Figure components**: Automatic conversion to Astro `Figure` components with zoom/download - **Media organization**: Structured media storage by page ID ### 🧮 **Interactive Components** @@ -121,7 +91,7 @@ published: "2024-01-15" tableOfContentsAutoCollapse: true --- -import Image from '../components/Image.astro'; +import Figure from '../components/Figure.astro'; import Note from '../components/Note.astro'; import gettingStartedImage from './media/getting-started/image1.png'; diff --git a/app/scripts/notion-importer/env.example b/app/scripts/notion-importer/env.example index 7b89b420f3d18d11035486c98019d406ab813599..725a31ad38a4d577b01fcdbe91bc279dcb5a46f6 100644 --- a/app/scripts/notion-importer/env.example +++ b/app/scripts/notion-importer/env.example @@ -1,2 +1,72 @@ -NOTION_TOKEN=ntn_xxx -NOTION_PAGE_ID=xxx +# Notion to MDX Toolkit - Environment Variables +# Copy this file to .env and fill in your actual values + +# =========================================== +# NOTION API CONFIGURATION +# =========================================== + +# Your Notion Integration Token +# Get this from: https://www.notion.so/my-integrations +# Format: secret_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +NOTION_TOKEN=secret_your_notion_integration_token_here + +# =========================================== +# OPTIONAL CONFIGURATION +# =========================================== + +# Custom output directory (optional) +# Default: ./output +# OUTPUT_DIR=./my-custom-output + +# Custom input configuration file (optional) +# Default: ./input/pages.json +# INPUT_CONFIG=./my-pages.json + +# =========================================== +# USAGE EXAMPLES +# =========================================== + +# 1. Basic usage: +# NOTION_TOKEN=secret_xxx node index.mjs + +# 2. With custom paths: +# NOTION_TOKEN=secret_xxx OUTPUT_DIR=./converted node index.mjs + +# 3. Test access to a page: +# NOTION_TOKEN=secret_xxx node test-access.mjs + +# =========================================== +# SETUP INSTRUCTIONS +# =========================================== + +# 1. Create a Notion integration: +# - Go to https://www.notion.so/my-integrations +# - Click "New integration" +# - Give it a name (e.g., "MDX Converter") +# - Select your workspace +# - Click "Submit" +# - Copy the "Internal Integration Token" + +# 2. Share your Notion pages with the integration: +# - Open your Notion page +# - Click "Share" (top right) +# - Click "Invite" +# - Search for your integration name +# - Select it and give "Can read content" permission +# - Click "Invite" + +# 3. Configure your pages in input/pages.json: +# { +# "pages": [ +# { +# "id": "your-notion-page-id", +# "title": "Page Title", +# "slug": "page-slug" +# } +# ] +# } + +# 4. Run the conversion: +# cp env.example .env +# # Edit .env with your actual token +# node index.mjs --clean diff --git a/app/scripts/notion-importer/index.mjs b/app/scripts/notion-importer/index.mjs index a32413d09d72c6cc0d0b2bec3595da3e9b88f28b..4364cf4b7020ae35c6ccb8a1af7576062597b658 100644 --- a/app/scripts/notion-importer/index.mjs +++ b/app/scripts/notion-importer/index.mjs @@ -6,10 +6,9 @@ import { fileURLToPath } from 'url'; import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs'; import { convertNotionToMarkdown } from './notion-converter.mjs'; import { convertToMdx } from './mdx-converter.mjs'; -import { Client } from '@notionhq/client'; -// Load environment variables from .env file (but don't override existing ones) -config({ override: false }); +// Load environment variables from .env file +config(); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -29,8 +28,7 @@ function parseArgs() { clean: false, notionOnly: false, mdxOnly: false, - token: process.env.NOTION_TOKEN, - pageId: process.env.NOTION_PAGE_ID + token: process.env.NOTION_TOKEN }; for (const arg of args) { @@ -40,8 +38,6 @@ function parseArgs() { config.output = arg.split('=')[1]; } else if (arg.startsWith('--token=')) { config.token = arg.split('=')[1]; - } else if (arg.startsWith('--page-id=')) { - config.pageId = arg.split('=')[1]; } else if (arg === '--clean') { config.clean = true; } else if (arg === '--notion-only') { @@ -127,54 +123,6 @@ function readPagesConfig(inputFile) { } } -/** - * Create a temporary pages.json from NOTION_PAGE_ID environment variable - * Extracts title and generates slug from the Notion page - */ -async function createPagesConfigFromEnv(pageId, token, outputPath) { - try { - console.log('🔍 Fetching page info from Notion API...'); - const notion = new Client({ auth: token }); - const page = await notion.pages.retrieve({ page_id: pageId }); - - // Extract title - let title = 'Article'; - if (page.properties.title && page.properties.title.title && page.properties.title.title.length > 0) { - title = page.properties.title.title[0].plain_text; - } else if (page.properties.Name && page.properties.Name.title && page.properties.Name.title.length > 0) { - title = page.properties.Name.title[0].plain_text; - } - - // Generate slug from title - const slug = title - .toLowerCase() - .replace(/[^\w\s-]/g, '') - .replace(/\s+/g, '-') - .replace(/-+/g, '-') - .trim(); - - console.log(` ✅ Found page: "${title}" (slug: ${slug})`); - - // Create pages config - const pagesConfig = { - pages: [{ - id: pageId, - title: title, - slug: slug - }] - }; - - // Write to temporary file - writeFileSync(outputPath, JSON.stringify(pagesConfig, null, 4)); - console.log(` ✅ Created temporary pages config`); - - return pagesConfig; - } catch (error) { - console.error(`❌ Error fetching page from Notion: ${error.message}`); - throw error; - } -} - function copyToAstroContent(outputDir) { console.log('📋 Copying MDX files to Astro content directory...'); @@ -188,9 +136,7 @@ function copyToAstroContent(outputDir) { const mdxFiles = files.filter(file => file.endsWith('.mdx')); if (mdxFiles.length > 0) { const mdxFile = join(outputDir, mdxFiles[0]); // Take the first MDX file - // Read and write instead of copy to avoid EPERM issues - const mdxContent = readFileSync(mdxFile, 'utf8'); - writeFileSync(ASTRO_CONTENT_PATH, mdxContent); + copyFileSync(mdxFile, ASTRO_CONTENT_PATH); console.log(` ✅ Copied MDX to ${ASTRO_CONTENT_PATH}`); } @@ -253,24 +199,6 @@ async function main() { console.log('========================'); try { - // Prepare input config file - let inputConfigFile = config.input; - let pageIdFromEnv = null; - - // If NOTION_PAGE_ID is provided via env var, create temporary pages.json - if (config.pageId && config.token) { - console.log('✨ Using NOTION_PAGE_ID from environment variable'); - const tempConfigPath = join(config.output, '.temp-pages.json'); - ensureDirectory(config.output); - await createPagesConfigFromEnv(config.pageId, config.token, tempConfigPath); - inputConfigFile = tempConfigPath; - pageIdFromEnv = config.pageId; - } else if (!existsSync(config.input)) { - console.error(`❌ No NOTION_PAGE_ID environment variable and no pages.json found at: ${config.input}`); - console.log('💡 Either set NOTION_PAGE_ID env var or create input/pages.json'); - process.exit(1); - } - if (config.clean) { console.log('🧹 Cleaning output directory...'); await cleanDirectory(config.output); @@ -285,7 +213,7 @@ async function main() { } else if (config.notionOnly) { // Only convert Notion to Markdown console.log('📄 Notion conversion only mode'); - await convertNotionToMarkdown(inputConfigFile, config.output, config.token); + await convertNotionToMarkdown(config.input, config.output, config.token); } else { // Full workflow @@ -293,13 +221,13 @@ async function main() { // Step 1: Convert Notion to Markdown console.log('\n📄 Step 1: Converting Notion pages to Markdown...'); - await convertNotionToMarkdown(inputConfigFile, config.output, config.token); + await convertNotionToMarkdown(config.input, config.output, config.token); // Step 2: Convert Markdown to MDX with Notion metadata console.log('\n📝 Step 2: Converting Markdown to MDX...'); - const pagesConfig = readPagesConfig(inputConfigFile); + const pagesConfig = readPagesConfig(config.input); const firstPage = pagesConfig.pages && pagesConfig.pages.length > 0 ? pagesConfig.pages[0] : null; - const pageId = pageIdFromEnv || (firstPage ? firstPage.id : null); + const pageId = firstPage ? firstPage.id : null; await convertToMdx(config.output, config.output, pageId, config.token); // Step 3: Copy to Astro content directory diff --git a/app/scripts/notion-importer/input/pages.json b/app/scripts/notion-importer/input/pages.json index 7ac9e48ad5ab396f0c6dff6997a748ebf26cb10b..d043e3a1081cb57d6605c813415ae02f847db229 100644 --- a/app/scripts/notion-importer/input/pages.json +++ b/app/scripts/notion-importer/input/pages.json @@ -1,9 +1,3 @@ -{ - "pages": [ - { - "id": "27877f1c9c9d804d9c82f7b3905578ff", - "title": "The Smol Training Guide", - "slug": "smol-training-guide" - } - ] -} \ No newline at end of file +version https://git-lfs.github.com/spec/v1 +oid sha256:2d51fba4ce9b05562f5df611a150e3cd702b487d2e608441318336556e0f248a +size 188 diff --git a/app/scripts/notion-importer/mdx-converter.mjs b/app/scripts/notion-importer/mdx-converter.mjs index bfdc791236a9ea97a48e1b2bc646cb62903e3be5..90709786565805ef0922914f13c5240541bcfa19 100644 --- a/app/scripts/notion-importer/mdx-converter.mjs +++ b/app/scripts/notion-importer/mdx-converter.mjs @@ -122,12 +122,12 @@ function addComponentImports(content) { } /** - * Transform Notion images to Image components + * Transform Notion images to Figure components * @param {string} content - MDX content - * @returns {string} - Content with Image components + * @returns {string} - Content with Figure components */ function transformImages(content) { - console.log(' 🖼️ Transforming images to Image components...'); + console.log(' 🖼️ Transforming images to Figure components...'); let hasImages = false; @@ -163,8 +163,8 @@ function transformImages(content) { : cleaned; }; - // Create Image component with import - const createImageComponent = (src, alt = '', caption = '') => { + // Create Figure component with import + const createFigureComponent = (src, alt = '', caption = '') => { const cleanSrc = cleanSrcPath(src); // Skip PDF URLs and external URLs - they should remain as links only @@ -177,7 +177,7 @@ function transformImages(content) { const varName = generateImageVarName(cleanSrc); imageImports.set(cleanSrc, varName); - usedComponents.add('Image'); + usedComponents.add('Figure'); const props = []; props.push(`src={${varName}}`); @@ -187,30 +187,30 @@ function transformImages(content) { if (alt) props.push(`alt="${alt}"`); if (caption) props.push(`caption={'${caption}'}`); - return ``; + return ``; }; // Transform markdown images: ![alt](src) content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (match, alt, src) => { const cleanSrc = cleanSrcPath(src); - const cleanAlt = cleanAltText(alt || 'Image'); + const cleanAlt = cleanAltText(alt || 'Figure'); hasImages = true; - return createImageComponent(cleanSrc, cleanAlt); + return createFigureComponent(cleanSrc, cleanAlt); }); // Transform images with captions (Notion sometimes adds captions as separate text) content = content.replace(/!\[([^\]]*)\]\(([^)]+)\)\s*\n\s*([^\n]+)/g, (match, alt, src, caption) => { const cleanSrc = cleanSrcPath(src); - const cleanAlt = cleanAltText(alt || 'Image'); + const cleanAlt = cleanAltText(alt || 'Figure'); const cleanCap = cleanCaption(caption); hasImages = true; - return createImageComponent(cleanSrc, cleanAlt, cleanCap); + return createFigureComponent(cleanSrc, cleanAlt, cleanCap); }); if (hasImages) { - console.log(' ✅ Image components with imports will be created'); + console.log(' ✅ Figure components with imports will be created'); } return content; diff --git a/app/scripts/notion-importer/notion-converter.mjs b/app/scripts/notion-importer/notion-converter.mjs index 89e7729751e9d647fdf91abcd2dfd89ef6f19730..80f08100fe21f26e0bd96b0b24f27c5858f335bb 100644 --- a/app/scripts/notion-importer/notion-converter.mjs +++ b/app/scripts/notion-importer/notion-converter.mjs @@ -10,8 +10,8 @@ import { fileURLToPath } from 'url'; import { postProcessMarkdown } from './post-processor.mjs'; import { createCustomCodeRenderer } from './custom-code-renderer.mjs'; -// Load environment variables from .env file (but don't override existing ones) -config({ override: false }); +// Load environment variables from .env file +config(); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); diff --git a/app/scripts/sync-template.mjs b/app/scripts/sync-template.mjs new file mode 100644 index 0000000000000000000000000000000000000000..949702ed5f27f5b1c673641bfb2bbde0e3bb0457 --- /dev/null +++ b/app/scripts/sync-template.mjs @@ -0,0 +1,361 @@ +#!/usr/bin/env node + +/** + * Template synchronization script for research-article-template + * + * This script: + * 1. Clones or updates the template repo in a temporary directory + * 2. Copies all files EXCEPT those in ./src/content which contain specific content + * 3. Preserves important local configuration files + * 4. Creates backups of files that will be overwritten + * + * Usage: npm run sync:template [--dry-run] [--backup] [--force] + */ + +import { execSync } from 'child_process'; +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const APP_ROOT = path.resolve(__dirname, '..'); +const PROJECT_ROOT = path.resolve(APP_ROOT, '..'); +const TEMP_DIR = path.join(PROJECT_ROOT, '.temp-template-sync'); +const TEMPLATE_REPO = 'https://huggingface.co/spaces/tfrere/research-article-template'; + +// Files and directories to PRESERVE (do not overwrite) +const PRESERVE_PATHS = [ + // Project-specific content + 'app/src/content', + + // Public data (symlink to our data) - CRITICAL: preserve this symlink + 'app/public/data', + + // Local configuration + 'app/package-lock.json', + 'app/node_modules', + + // Project-specific scripts (preserve our sync script) + 'app/scripts/sync-template.mjs', + + // Project configuration files + 'README.md', + 'tools', + + // Backup and temporary files + '.backup-*', + '.temp-*', + + // Git + '.git', + '.gitignore' +]; + +// Files to handle with caution (require confirmation) +const SENSITIVE_FILES = [ + 'app/package.json', + 'app/astro.config.mjs', + 'Dockerfile', + 'nginx.conf' +]; + +const args = process.argv.slice(2); +const isDryRun = args.includes('--dry-run'); +const shouldBackup = args.includes('--backup'); // Disabled by default, use --backup to enable +const isForce = args.includes('--force'); + +console.log('🔄 Template synchronization script for research-article-template'); +console.log(`📁 Working directory: ${PROJECT_ROOT}`); +console.log(`🎯 Template source: ${TEMPLATE_REPO}`); +if (isDryRun) console.log('🔍 DRY-RUN mode enabled - no files will be modified'); +if (shouldBackup) console.log('💾 Backup enabled'); +if (!shouldBackup) console.log('🚫 Backup disabled (use --backup to enable)'); +console.log(''); + +async function executeCommand(command, options = {}) { + try { + if (isDryRun && !options.allowInDryRun) { + console.log(`[DRY-RUN] Command: ${command}`); + return ''; + } + console.log(`$ ${command}`); + const result = execSync(command, { + encoding: 'utf8', + cwd: options.cwd || PROJECT_ROOT, + stdio: options.quiet ? 'pipe' : 'inherit' + }); + return result; + } catch (error) { + console.error(`❌ Error during execution: ${command}`); + console.error(error.message); + throw error; + } +} + +async function pathExists(filePath) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +async function isPathPreserved(relativePath) { + return PRESERVE_PATHS.some(preserve => + relativePath === preserve || + relativePath.startsWith(preserve + '/') + ); +} + +async function createBackup(filePath) { + if (!shouldBackup || isDryRun) return; + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupPath = `${filePath}.backup-${timestamp}`; + + try { + await fs.copyFile(filePath, backupPath); + console.log(`💾 Backup created: ${path.relative(PROJECT_ROOT, backupPath)}`); + } catch (error) { + console.warn(`⚠️ Unable to create backup for ${filePath}: ${error.message}`); + } +} + +async function syncFile(sourcePath, targetPath) { + const relativeTarget = path.relative(PROJECT_ROOT, targetPath); + + // Check if the file should be preserved + if (await isPathPreserved(relativeTarget)) { + console.log(`🔒 PRESERVED: ${relativeTarget}`); + return; + } + + // Check if it's a sensitive file + if (SENSITIVE_FILES.includes(relativeTarget)) { + if (!isForce) { + console.log(`⚠️ SENSITIVE (ignored): ${relativeTarget} (use --force to overwrite)`); + return; + } else { + console.log(`⚠️ SENSITIVE (forced): ${relativeTarget}`); + } + } + + // Check if target file is a symbolic link to preserve + if (await pathExists(targetPath)) { + try { + const targetStats = await fs.lstat(targetPath); + if (targetStats.isSymbolicLink()) { + console.log(`🔗 SYMLINK TARGET (preserved): ${relativeTarget}`); + return; + } + } catch (error) { + console.warn(`⚠️ Impossible de vérifier ${targetPath}: ${error.message}`); + } + } + + // Create backup if file already exists (and is not a symbolic link) + if (await pathExists(targetPath)) { + try { + const stats = await fs.lstat(targetPath); + if (!stats.isSymbolicLink()) { + await createBackup(targetPath); + } + } catch (error) { + console.warn(`⚠️ Impossible de vérifier ${targetPath}: ${error.message}`); + } + } + + if (isDryRun) { + console.log(`[DRY-RUN] COPY: ${relativeTarget}`); + return; + } + + // Assurer que le répertoire parent existe + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + + // Check if source is a symbolic link + try { + const sourceStats = await fs.lstat(sourcePath); + if (sourceStats.isSymbolicLink()) { + console.log(`🔗 SYMLINK SOURCE (ignored): ${relativeTarget}`); + return; + } + } catch (error) { + console.warn(`⚠️ Unable to check source ${sourcePath}: ${error.message}`); + return; + } + + // Remove target file if it exists (to handle symbolic links) + if (await pathExists(targetPath)) { + await fs.rm(targetPath, { recursive: true, force: true }); + } + + // Copier le fichier + await fs.copyFile(sourcePath, targetPath); + console.log(`✅ COPIED: ${relativeTarget}`); +} + +async function syncDirectory(sourceDir, targetDir) { + const items = await fs.readdir(sourceDir, { withFileTypes: true }); + + for (const item of items) { + const sourcePath = path.join(sourceDir, item.name); + const targetPath = path.join(targetDir, item.name); + const relativeTarget = path.relative(PROJECT_ROOT, targetPath); + + if (await isPathPreserved(relativeTarget)) { + console.log(`🔒 DOSSIER PRÉSERVÉ: ${relativeTarget}/`); + continue; + } + + if (item.isDirectory()) { + if (!isDryRun) { + await fs.mkdir(targetPath, { recursive: true }); + } + await syncDirectory(sourcePath, targetPath); + } else { + await syncFile(sourcePath, targetPath); + } + } +} + +async function cloneOrUpdateTemplate() { + console.log('📥 Fetching template...'); + + // Nettoyer le dossier temporaire s'il existe + if (await pathExists(TEMP_DIR)) { + await fs.rm(TEMP_DIR, { recursive: true, force: true }); + if (isDryRun) { + console.log(`[DRY-RUN] Suppression: ${TEMP_DIR}`); + } + } + + // Clone template repo (even in dry-run to be able to compare) + await executeCommand(`git clone ${TEMPLATE_REPO} "${TEMP_DIR}"`, { allowInDryRun: true }); + + return TEMP_DIR; +} + +async function ensureDataSymlink() { + const dataSymlinkPath = path.join(APP_ROOT, 'public', 'data'); + const dataSourcePath = path.join(APP_ROOT, 'src', 'content', 'assets', 'data'); + + // Check if symlink exists and is correct + if (await pathExists(dataSymlinkPath)) { + try { + const stats = await fs.lstat(dataSymlinkPath); + if (stats.isSymbolicLink()) { + const target = await fs.readlink(dataSymlinkPath); + const expectedTarget = path.relative(path.dirname(dataSymlinkPath), dataSourcePath); + if (target === expectedTarget) { + console.log('🔗 Data symlink is correct'); + return; + } else { + console.log(`⚠️ Data symlink points to wrong target: ${target} (expected: ${expectedTarget})`); + } + } else { + console.log('⚠️ app/public/data exists but is not a symlink'); + } + } catch (error) { + console.log(`⚠️ Error checking symlink: ${error.message}`); + } + } + + // Recreate symlink + if (!isDryRun) { + if (await pathExists(dataSymlinkPath)) { + await fs.rm(dataSymlinkPath, { recursive: true, force: true }); + } + await fs.symlink(path.relative(path.dirname(dataSymlinkPath), dataSourcePath), dataSymlinkPath); + console.log('✅ Data symlink recreated'); + } else { + console.log('[DRY-RUN] Would recreate data symlink'); + } +} + +async function showSummary(templateDir) { + console.log('\n📊 SYNCHRONIZATION SUMMARY'); + console.log('================================'); + + console.log('\n🔒 Preserved files/directories:'); + for (const preserve of PRESERVE_PATHS) { + const fullPath = path.join(PROJECT_ROOT, preserve); + if (await pathExists(fullPath)) { + console.log(` ✓ ${preserve}`); + } else { + console.log(` - ${preserve} (n'existe pas)`); + } + } + + console.log('\n⚠️ Sensitive files (require --force):'); + for (const sensitive of SENSITIVE_FILES) { + const fullPath = path.join(PROJECT_ROOT, sensitive); + if (await pathExists(fullPath)) { + console.log(` ! ${sensitive}`); + } + } + + if (isDryRun) { + console.log('\n🔍 To execute for real: npm run sync:template'); + console.log('🔧 To force sensitive files: npm run sync:template -- --force'); + } +} + +async function cleanup() { + console.log('\n🧹 Cleaning up...'); + if (await pathExists(TEMP_DIR)) { + if (!isDryRun) { + await fs.rm(TEMP_DIR, { recursive: true, force: true }); + } + console.log(`🗑️ Temporary directory removed: ${TEMP_DIR}`); + } +} + +async function main() { + try { + // Verify we're in the correct directory + const packageJsonPath = path.join(APP_ROOT, 'package.json'); + if (!(await pathExists(packageJsonPath))) { + throw new Error(`Package.json not found in ${APP_ROOT}. Are you in the correct directory?`); + } + + // Clone the template + const templateDir = await cloneOrUpdateTemplate(); + + // Synchroniser + console.log('\n🔄 Synchronisation en cours...'); + await syncDirectory(templateDir, PROJECT_ROOT); + + // S'assurer que le lien symbolique des données est correct + console.log('\n🔗 Vérification du lien symbolique des données...'); + await ensureDataSymlink(); + + // Afficher le résumé + await showSummary(templateDir); + + console.log('\n✅ Synchronization completed!'); + + } catch (error) { + console.error('\n❌ Error during synchronization:'); + console.error(error.message); + process.exit(1); + } finally { + await cleanup(); + } +} + +// Signal handling to clean up on interruption +process.on('SIGINT', async () => { + console.log('\n\n⚠️ Interruption detected, cleaning up...'); + await cleanup(); + process.exit(1); +}); + +process.on('SIGTERM', async () => { + console.log('\n\n⚠️ Shutdown requested, cleaning up...'); + await cleanup(); + process.exit(1); +}); + +main(); diff --git a/app/src/components/Glossary.astro b/app/src/components/Glossary.astro new file mode 100644 index 0000000000000000000000000000000000000000..9023fe5b824da357632216d161721d897d42d871 --- /dev/null +++ b/app/src/components/Glossary.astro @@ -0,0 +1,336 @@ +--- +interface Props { + /** The word or term to define */ + term: string; + /** The definition of the term */ + definition: string; + /** Optional CSS class to apply to the term */ + class?: string; + /** Optional style to apply to the term */ + style?: string; + /** Tooltip position (top, bottom, left, right) */ + position?: "top" | "bottom" | "left" | "right"; + /** Delay before showing tooltip in ms */ + delay?: number; + /** Disable tooltip on mobile */ + disableOnMobile?: boolean; +} + +const { + term, + definition, + class: className = "", + style: inlineStyle = "", + position = "top", + delay = 300, + disableOnMobile = false, +} = Astro.props as Props; + +// Generate a unique ID for this component +const tooltipId = `glossary-${Math.random().toString(36).slice(2)}`; +--- + +
+ + {term} + + + +
+ + + + diff --git a/app/src/components/Hero.astro b/app/src/components/Hero.astro index a8b947f7c908f2d59ca657e57ea264bfdb61da9d..8042aa2054b683ac157d8bee70014f29571f4524 100644 --- a/app/src/components/Hero.astro +++ b/app/src/components/Hero.astro @@ -12,6 +12,7 @@ interface Props { affiliation?: string; // legacy single affiliation published?: string; doi?: string; + pdfProOnly?: boolean; // Gate PDF download to Pro users only } const { @@ -23,6 +24,7 @@ const { affiliation, published, doi, + pdfProOnly = false, } = Astro.props as Props; type Author = { name: string; url?: string; affiliationIndices?: number[] }; @@ -111,12 +113,11 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`; shouldShowAffiliationSupers && Array.isArray(a.affiliationIndices) && a.affiliationIndices.length ? ( - {a.affiliationIndices.join(",")} + {a.affiliationIndices.join(", ")} ) : null; return (
  • - {a.url ? {a.name} : a.name} - {supers} + {a.url ? {a.name} : a.name}{supers}{i < normalizedAuthors.length - 1 && }
  • ); })} @@ -126,7 +127,7 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`; } { Array.isArray(affiliations) && affiliations.length > 0 && ( -
    +

    Affiliation{affiliations.length > 1 ? "s" : ""}

    {hasMultipleAffiliations ? (
      @@ -162,7 +163,7 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`; } { (!affiliations || affiliations.length === 0) && affiliation && ( -
      +

      Affiliation

      {affiliation}

      @@ -183,26 +184,170 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
      )} -->
      -

      PDF

      -

      - - Download PDF - -

      +
      +

      PDF

      + + +
      +
      +

      Checking access...

      + + +
    + + diff --git a/app/src/components/HtmlEmbed.astro b/app/src/components/HtmlEmbed.astro index 2dab719abe71a218bc542e50c4859e3bcea4f580..91d4762c51e9d8e50eeda957e9787656aee7f59a 100644 --- a/app/src/components/HtmlEmbed.astro +++ b/app/src/components/HtmlEmbed.astro @@ -20,12 +20,15 @@ const html = resolveFragment(src); const mountId = `frag-${Math.random().toString(36).slice(2)}`; const dataAttr = Array.isArray(data) ? JSON.stringify(data) : (typeof data === 'string' ? data : undefined); const configAttr = typeof config === 'string' ? config : (config != null ? JSON.stringify(config) : undefined); + +// Apply the ID to the HTML content if provided +const htmlWithId = id && html ? html.replace(/
    ]*>/, `
    `) : html; --- { html ? (
    {title &&
    {title}
    }
    -
    +
    {desc &&
    }
    @@ -70,7 +73,6 @@ const configAttr = typeof config === 'string' ? config : (config != null ? JSON. .html-embed { margin: 0 0 var(--block-spacing-y); z-index: var(--z-elevated); position: relative; - } .html-embed__title { text-align: left; @@ -83,12 +85,14 @@ const configAttr = typeof config === 'string' ? config : (config != null ? JSON. position: relative; display: block; width: 100%; + background: var(--page-bg); + z-index: var(--z-elevated); } .html-embed__card { background: var(--code-bg); border: 1px solid var(--border-color); border-radius: 10px; - padding: 8px; + padding: 24px; z-index: calc(var(--z-elevated) + 1); position: relative; } @@ -108,6 +112,7 @@ const configAttr = typeof config === 'string' ? config : (config != null ? JSON. z-index: var(--z-elevated); display: block; width: 100%; + background: var(--page-bg); } /* Plotly – fragments & controls */ .html-embed__card svg text { fill: var(--text-color); } diff --git a/app/src/components/Image.astro b/app/src/components/Image.astro new file mode 100644 index 0000000000000000000000000000000000000000..164ec1f59c8046bdc2ca174a52062540b2ca9183 --- /dev/null +++ b/app/src/components/Image.astro @@ -0,0 +1,508 @@ +--- +// @ts-ignore - types provided by Astro at runtime +import { Image as AstroImage } from "astro:assets"; + +interface Props { + /** Source image imported via astro:assets */ + src: any; + /** Alt text for accessibility */ + alt: string; + /** Optional HTML string caption (use slot caption for rich content) */ + caption?: string; + /** Optional class to apply on the
    wrapper when caption is used */ + figureClass?: string; + /** Enable medium-zoom behavior on this image */ + zoomable?: boolean; + /** Show a download button overlay and enable download flow */ + downloadable?: boolean; + /** Optional explicit file name to use on download */ + downloadName?: string; + /** Optional explicit source URL to download instead of currentSrc */ + downloadSrc?: string; + /** Optional link that wraps the image (not the caption) */ + linkHref?: string; + /** Optional target for the link (default: _blank when linkHref provided) */ + linkTarget?: string; + /** Optional rel for the link (default: noopener noreferrer when linkHref provided) */ + linkRel?: string; + /** Make the image span full width */ + fullWidth?: boolean; + /** Any additional attributes should be forwarded to the underlying */ + [key: string]: any; +} + +const { + caption, + figureClass, + zoomable, + downloadable, + downloadName, + downloadSrc, + linkHref, + linkTarget, + linkRel, + fullWidth, + ...imgProps +} = Astro.props as Props; +const hasCaptionSlot = Astro.slots.has("caption"); +const hasCaption = + hasCaptionSlot || (typeof caption === "string" && caption.length > 0); +const hasTitle = Astro.slots.has("title"); +const uid = `ri_${Math.random().toString(36).slice(2)}`; +const dataZoomable = + zoomable === true || (imgProps as any)["data-zoomable"] ? "1" : undefined; +const dataDownloadable = + downloadable === true || (imgProps as any)["data-downloadable"] + ? "1" + : undefined; +const hasLink = typeof linkHref === "string" && linkHref.length > 0; +const resolvedTarget = hasLink ? linkTarget || "_blank" : undefined; +const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined; +--- + +
    + { + hasCaption ? ( +
    + {dataDownloadable ? ( + + {hasLink ? ( + + + + ) : ( + + )} + + + ) : hasLink ? ( + + + + ) : ( + + )} +
    + {hasCaptionSlot ? ( + + ) : ( + caption && + )} +
    +
    + ) : dataDownloadable ? ( + + {hasLink ? ( + + + + ) : ( + + )} + + + ) : hasLink ? ( + + + + ) : ( + + ) + } +
    + + + + diff --git a/app/src/components/Quote.astro b/app/src/components/Quote.astro new file mode 100644 index 0000000000000000000000000000000000000000..f75c29e93ce0319da6aa20535eaad95e0bf7dff1 --- /dev/null +++ b/app/src/components/Quote.astro @@ -0,0 +1,124 @@ +--- +interface Props { + source?: string; +} + +const { source } = Astro.props; +--- + +
    +
    + +
    + { + source && ( +
    + +
    + ) + } +
    + + diff --git a/app/src/components/Reference.astro b/app/src/components/Reference.astro new file mode 100644 index 0000000000000000000000000000000000000000..5061143077e518465f1cf287a34dadcfcab3ce9b --- /dev/null +++ b/app/src/components/Reference.astro @@ -0,0 +1,45 @@ +--- +interface Props { + /** ID unique pour la référence */ + id: string; + /** Légende HTML pour la référence */ + caption: string; +} + +const { id, caption } = Astro.props as Props; +--- + +
    +
    +
    + +
    +
    +
    +
    + + diff --git a/app/src/components/Sidenote.astro b/app/src/components/Sidenote.astro index 10631f567038d60b410c199a8c8ba12196e30eb9..8a017fd88d8f458f82c7020afdee7f688bfa0430 100644 --- a/app/src/components/Sidenote.astro +++ b/app/src/components/Sidenote.astro @@ -1,37 +1,99 @@ --- + --- -
    -
    + +
    +
    -
    + diff --git a/app/src/components/Stack.astro b/app/src/components/Stack.astro new file mode 100644 index 0000000000000000000000000000000000000000..783682a4883321d163b38af30232dc3dc66047a3 --- /dev/null +++ b/app/src/components/Stack.astro @@ -0,0 +1,161 @@ +--- +interface Props { + /** Layout mode: number of columns or 'auto' for responsive */ + layout?: "2-column" | "3-column" | "4-column" | "auto"; + /** Gap between items - can be a predefined size or custom value (e.g., "2rem", "20px", "1.5em") */ + gap?: "small" | "medium" | "large" | string; + /** Optional class to apply on the wrapper */ + class?: string; + /** Optional ID for the stack */ + id?: string; +} + +const { + layout = "2-column", + gap = "medium", + class: className, + id, +} = Astro.props as Props; + +// Generate flex properties based on layout +const getFlexProperties = () => { + switch (layout) { + case "2-column": + return { flexBasis: "50%", maxWidth: "50%" }; + case "3-column": + return { flexBasis: "33.333%", maxWidth: "33.333%" }; + case "4-column": + return { flexBasis: "25%", maxWidth: "25%" }; + case "auto": + return { flexBasis: "auto", maxWidth: "none" }; + default: + // By default, all children on one line with equal width + return { flexBasis: "auto", maxWidth: "none" }; + } +}; + +const getGapSize = () => { + // If it's a predefined size, return the corresponding value + switch (gap) { + case "small": + return "0.5rem"; + case "medium": + return "1rem"; + case "large": + return "1.5rem"; + default: + // If it's a custom value, return it as-is (e.g., "2rem", "20px", "1.5em") + return gap; + } +}; + +const flexProps = getFlexProperties(); +const gapSize = getGapSize(); +--- + +
    + +
    + + diff --git a/app/src/components/demo/ColorPicker.astro b/app/src/components/demo/ColorPicker.astro new file mode 100644 index 0000000000000000000000000000000000000000..c26e355ff005d3453246acb1db96cbc234e6c065 --- /dev/null +++ b/app/src/components/demo/ColorPicker.astro @@ -0,0 +1,633 @@ +--- + +--- + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    Hue
    +
    +
    +
    +
    214°
    +
    +
    +
    +
    + diff --git a/app/src/components/demo/Palettes.astro b/app/src/components/demo/Palettes.astro new file mode 100644 index 0000000000000000000000000000000000000000..2aabeda6ffa7b0af45799e105b04e4da5eb60060 --- /dev/null +++ b/app/src/components/demo/Palettes.astro @@ -0,0 +1,596 @@ +--- +const rootId = `palettes-${Math.random().toString(36).slice(2)}`; +--- + +
    + +
    +
    + + +
    +
    +
    + + 8 +
    +
    + +
    +
    +
    +
    +
    + +
    +
    + diff --git a/app/src/components/trackio/Trackio.svelte b/app/src/components/trackio/Trackio.svelte index 63805331926bfff1ab7bc59b140d2c276cad02be..309e945f002482503aaed8c39e43337dbd0b07d1 100644 --- a/app/src/components/trackio/Trackio.svelte +++ b/app/src/components/trackio/Trackio.svelte @@ -1,14 +1,20 @@
    - { const run = e?.detail?.name; if (!run) return; ghostRun(run); }} on:legend-leave={() => { clearGhost(); }} /> + { + const run = e?.detail?.name; + if (!run) return; + ghostRun(run); + }} + on:legend-leave={() => { + clearGhost(); + }} + />
    {#each cellsDef as c, i} - colorsByRun[name] || '#999'} + colorsByRun[name] || "#999"} {hostEl} currentIndex={i} onOpenModal={openModal} @@ -467,9 +638,17 @@
    @@ -477,7 +656,7 @@ - - diff --git a/app/src/components/trackio/TrackioWrapper.astro b/app/src/components/trackio/TrackioWrapper.astro new file mode 100644 index 0000000000000000000000000000000000000000..f8ac691cf261f0aa5fae6a3df2e5f8193853191a --- /dev/null +++ b/app/src/components/trackio/TrackioWrapper.astro @@ -0,0 +1,510 @@ +--- +// TrackioWrapper.astro +import Trackio from "./Trackio.svelte"; +--- + + + + + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + + + +
    +
    + +
    + +
    +
    + + + + diff --git a/app/src/components/trackio/core/adaptive-sampler.js b/app/src/components/trackio/core/adaptive-sampler.js index 7672290d94feb92f1ebe2a745c0807fa2073ea74..2b8275252dedc578ac317061f95bffc6b53950ad 100644 --- a/app/src/components/trackio/core/adaptive-sampler.js +++ b/app/src/components/trackio/core/adaptive-sampler.js @@ -7,24 +7,24 @@ export class AdaptiveSampler { constructor(options = {}) { this.options = { - maxPoints: 400, // Seuil pour déclencher le sampling - targetPoints: 200, // Nombre cible de points après sampling - preserveFeatures: true, // Préserver les pics/vallées importantes + maxPoints: 400, // Threshold to trigger sampling + targetPoints: 200, // Target number of points after sampling + preserveFeatures: true, // Preserve important peaks/valleys adaptiveStrategy: 'smart', // 'uniform', 'smart', 'lod' - smoothingWindow: 3, // Fenêtre pour détection des features + smoothingWindow: 3, // Window for feature detection ...options }; } /** - * Détermine si le sampling est nécessaire + * Determine if sampling is necessary */ needsSampling(dataLength) { return dataLength > this.options.maxPoints; } /** - * Point d'entrée principal pour le sampling + * Main entry point for sampling */ sampleSeries(data, strategy = null) { if (!Array.isArray(data) || data.length === 0) { @@ -234,11 +234,11 @@ export class AdaptiveSampler { const index = Math.floor(logProgress * (totalLength - 1)); indices.push(Math.max(1, Math.min(totalLength - 2, index))); } - return [...new Set(indices)]; // Supprimer les doublons + return [...new Set(indices)]; // Remove duplicates } /** - * Échantillonnage basé sur la variation locale + * Sampling based on local variation */ sampleByVariation(data, targetPoints) { const variations = []; @@ -290,7 +290,7 @@ export class AdaptiveSampler { * Reconstruit les données complètes pour une zone spécifique (pour le zoom) */ getFullDataForRange(originalData, samplingInfo, startStep, endStep) { - // Cette méthode permettrait de récupérer plus de détails + // This method would allow recovering more details // quand l'utilisateur zoom sur une zone spécifique const startIdx = originalData.findIndex(d => d.step >= startStep); const endIdx = originalData.findIndex(d => d.step > endStep); diff --git a/app/src/content/embeds/banner.html b/app/src/content/embeds/banner.html new file mode 100644 index 0000000000000000000000000000000000000000..17b25a53ee9a6ea99f38a5de9d467d6c92478ba6 --- /dev/null +++ b/app/src/content/embeds/banner.html @@ -0,0 +1,633 @@ +
    + + diff --git a/app/src/content/embeds2/d3-bar.html b/app/src/content/embeds/d3-bar.html similarity index 100% rename from app/src/content/embeds2/d3-bar.html rename to app/src/content/embeds/d3-bar.html diff --git a/app/src/content/embeds2/d3-benchmark.html b/app/src/content/embeds/d3-benchmark.html similarity index 100% rename from app/src/content/embeds2/d3-benchmark.html rename to app/src/content/embeds/d3-benchmark.html diff --git a/app/src/content/embeds2/d3-confusion-matrix.html b/app/src/content/embeds/d3-confusion-matrix.html similarity index 100% rename from app/src/content/embeds2/d3-confusion-matrix.html rename to app/src/content/embeds/d3-confusion-matrix.html diff --git a/app/src/content/embeds2/d3-evals-after-fix.html b/app/src/content/embeds/d3-evals-after-fix.html similarity index 100% rename from app/src/content/embeds2/d3-evals-after-fix.html rename to app/src/content/embeds/d3-evals-after-fix.html diff --git a/app/src/content/embeds2/d3-evals-tpbug.html b/app/src/content/embeds/d3-evals-tpbug.html similarity index 100% rename from app/src/content/embeds2/d3-evals-tpbug.html rename to app/src/content/embeds/d3-evals-tpbug.html diff --git a/app/src/content/embeds2/d3-line-quad.html b/app/src/content/embeds/d3-line-quad.html similarity index 100% rename from app/src/content/embeds2/d3-line-quad.html rename to app/src/content/embeds/d3-line-quad.html diff --git a/app/src/content/embeds2/d3-line.html b/app/src/content/embeds/d3-line.html similarity index 100% rename from app/src/content/embeds2/d3-line.html rename to app/src/content/embeds/d3-line.html diff --git a/app/src/content/embeds2/d3-matrix.html b/app/src/content/embeds/d3-matrix.html similarity index 100% rename from app/src/content/embeds2/d3-matrix.html rename to app/src/content/embeds/d3-matrix.html diff --git a/app/src/content/embeds2/d3-neural-network.html b/app/src/content/embeds/d3-neural-network.html similarity index 100% rename from app/src/content/embeds2/d3-neural-network.html rename to app/src/content/embeds/d3-neural-network.html diff --git a/app/src/content/embeds2/d3-pie-quad.html b/app/src/content/embeds/d3-pie-quad.html similarity index 100% rename from app/src/content/embeds2/d3-pie-quad.html rename to app/src/content/embeds/d3-pie-quad.html diff --git a/app/src/content/embeds2/d3-pie.html b/app/src/content/embeds/d3-pie.html similarity index 100% rename from app/src/content/embeds2/d3-pie.html rename to app/src/content/embeds/d3-pie.html diff --git a/app/src/content/embeds2/d3-scatter.html b/app/src/content/embeds/d3-scatter.html similarity index 100% rename from app/src/content/embeds2/d3-scatter.html rename to app/src/content/embeds/d3-scatter.html diff --git a/app/src/content/embeds2/demo/color-picker.html b/app/src/content/embeds/demo/color-picker.html similarity index 100% rename from app/src/content/embeds2/demo/color-picker.html rename to app/src/content/embeds/demo/color-picker.html diff --git a/app/src/content/embeds2/demo/content-structure.html b/app/src/content/embeds/demo/content-structure.html similarity index 100% rename from app/src/content/embeds2/demo/content-structure.html rename to app/src/content/embeds/demo/content-structure.html diff --git a/app/src/content/embeds2/demo/palettes.html b/app/src/content/embeds/demo/palettes.html similarity index 100% rename from app/src/content/embeds2/demo/palettes.html rename to app/src/content/embeds/demo/palettes.html diff --git a/app/src/content/embeds2/original_embeds/plotly/banner.py b/app/src/content/embeds/original_embeds/plotly/banner.py similarity index 100% rename from app/src/content/embeds2/original_embeds/plotly/banner.py rename to app/src/content/embeds/original_embeds/plotly/banner.py diff --git a/app/src/content/embeds2/original_embeds/plotly/bar.py b/app/src/content/embeds/original_embeds/plotly/bar.py similarity index 100% rename from app/src/content/embeds2/original_embeds/plotly/bar.py rename to app/src/content/embeds/original_embeds/plotly/bar.py diff --git a/app/src/content/embeds2/original_embeds/plotly/heatmap.py b/app/src/content/embeds/original_embeds/plotly/heatmap.py similarity index 100% rename from app/src/content/embeds2/original_embeds/plotly/heatmap.py rename to app/src/content/embeds/original_embeds/plotly/heatmap.py diff --git a/app/src/content/embeds2/original_embeds/plotly/line.py b/app/src/content/embeds/original_embeds/plotly/line.py similarity index 100% rename from app/src/content/embeds2/original_embeds/plotly/line.py rename to app/src/content/embeds/original_embeds/plotly/line.py diff --git a/app/src/content/embeds2/original_embeds/plotly/poetry.lock b/app/src/content/embeds/original_embeds/plotly/poetry.lock similarity index 100% rename from app/src/content/embeds2/original_embeds/plotly/poetry.lock rename to app/src/content/embeds/original_embeds/plotly/poetry.lock diff --git a/app/src/content/embeds2/original_embeds/plotly/pyproject.toml b/app/src/content/embeds/original_embeds/plotly/pyproject.toml similarity index 100% rename from app/src/content/embeds2/original_embeds/plotly/pyproject.toml rename to app/src/content/embeds/original_embeds/plotly/pyproject.toml diff --git a/app/src/content/embeds2/plotly-line.html b/app/src/content/embeds/plotly-line.html similarity index 100% rename from app/src/content/embeds2/plotly-line.html rename to app/src/content/embeds/plotly-line.html diff --git a/app/src/content/embeds2/throughput-debug-1node.html b/app/src/content/embeds/throughput-debug-1node.html similarity index 100% rename from app/src/content/embeds2/throughput-debug-1node.html rename to app/src/content/embeds/throughput-debug-1node.html diff --git a/app/src/content/embeds2/throughput-drops-comparison.html b/app/src/content/embeds/throughput-drops-comparison.html similarity index 100% rename from app/src/content/embeds2/throughput-drops-comparison.html rename to app/src/content/embeds/throughput-drops-comparison.html diff --git a/app/src/content/embeds2/throughput-weka-drops.html b/app/src/content/embeds/throughput-weka-drops.html similarity index 100% rename from app/src/content/embeds2/throughput-weka-drops.html rename to app/src/content/embeds/throughput-weka-drops.html diff --git a/app/src/content/embeds2/vibe-code-d3-embeds-directives.md b/app/src/content/embeds/vibe-code-d3-embeds-directives.md similarity index 100% rename from app/src/content/embeds2/vibe-code-d3-embeds-directives.md rename to app/src/content/embeds/vibe-code-d3-embeds-directives.md diff --git a/app/src/content/embeds2/banner.html b/app/src/content/embeds2/banner.html deleted file mode 100644 index 1ad415ed788791dcb7eec078aea3fda6d4d8907c..0000000000000000000000000000000000000000 --- a/app/src/content/embeds2/banner.html +++ /dev/null @@ -1,267 +0,0 @@ -
    - - diff --git a/app/src/pages/index.astro b/app/src/pages/index.astro index 38919e60d34ff5a1ef49f1ef7e13681c5a0c10bf..b33dbdadb36f28852c8f1608d72934eed1bba015 100644 --- a/app/src/pages/index.astro +++ b/app/src/pages/index.astro @@ -1,48 +1,57 @@ --- -import * as ArticleMod from '../content/article.mdx'; +import * as ArticleMod from "../content/article.mdx"; -import Hero from '../components/Hero.astro'; -import Footer from '../components/Footer.astro'; -import ThemeToggle from '../components/ThemeToggle.astro'; -import Seo from '../components/Seo.astro'; -import TableOfContents from '../components/TableOfContents.astro'; +import Hero from "../components/Hero.astro"; +import Footer from "../components/Footer.astro"; +import ThemeToggle from "../components/ThemeToggle.astro"; +import Seo from "../components/Seo.astro"; +import TableOfContents from "../components/TableOfContents.astro"; // Default OG image served from public/ -const ogDefaultUrl = '/thumb.auto.jpg'; -import 'katex/dist/katex.min.css'; -import '../styles/global.css'; +const ogDefaultUrl = "/thumb.auto.jpg"; +import "katex/dist/katex.min.css"; +import "../styles/global.css"; const articleFM = (ArticleMod as any).frontmatter ?? {}; const Article = (ArticleMod as any).default; -const docTitle = articleFM?.title ?? 'Untitled article'; +const docTitle = articleFM?.title ?? "Untitled article"; // Allow explicit line breaks in the title via "\n" or YAML newlines -const docTitleHtml = (articleFM?.title ?? 'Untitled article') - .replace(/\\n/g, '
    ') - .replace(/\n/g, '
    '); -const subtitle = articleFM?.subtitle ?? ''; -const description = articleFM?.description ?? ''; +const docTitleHtml = (articleFM?.title ?? "Untitled article") + .replace(/\\n/g, "
    ") + .replace(/\n/g, "
    "); +const subtitle = articleFM?.subtitle ?? ""; +const description = articleFM?.description ?? ""; // Accept authors as string[] or array of objects { name, url, affiliations? } const rawAuthors = (articleFM as any)?.authors ?? []; type Affiliation = { id: number; name: string; url?: string }; type Author = { name: string; url?: string; affiliationIndices?: number[] }; // Normalize affiliations from frontmatter: supports strings or objects { id?, name, url? } -const rawAffils = (articleFM as any)?.affiliations ?? (articleFM as any)?.affiliation ?? []; +const rawAffils = + (articleFM as any)?.affiliations ?? (articleFM as any)?.affiliation ?? []; const normalizedAffiliations: Affiliation[] = (() => { const seen: Map = new Map(); const list: Affiliation[] = []; const pushUnique = (name: string, url?: string) => { - const key = `${String(name).trim()}|${url ? String(url).trim() : ''}`; + const key = `${String(name).trim()}|${url ? String(url).trim() : ""}`; if (seen.has(key)) return seen.get(key)!; const id = list.length + 1; - list.push({ id, name: String(name).trim(), url: url ? String(url) : undefined }); + list.push({ + id, + name: String(name).trim(), + url: url ? String(url) : undefined, + }); seen.set(key, id); return id; }; - const input = Array.isArray(rawAffils) ? rawAffils : (rawAffils ? [rawAffils] : []); + const input = Array.isArray(rawAffils) + ? rawAffils + : rawAffils + ? [rawAffils] + : []; for (const a of input) { - if (typeof a === 'string') { + if (typeof a === "string") { pushUnique(a); - } else if (a && typeof a === 'object') { - const name = a.name ?? a.label ?? a.text ?? a.affiliation ?? ''; + } else if (a && typeof a === "object") { + const name = a.name ?? a.label ?? a.text ?? a.affiliation ?? ""; if (!String(name).trim()) continue; const url = a.url || a.link; // Respect provided numeric id for display stability if present and sequential; otherwise reassign @@ -55,53 +64,69 @@ const normalizedAffiliations: Affiliation[] = (() => { // Helper: ensure an affiliation exists and return its id const ensureAffiliation = (val: any): number | undefined => { if (val == null) return undefined; - if (typeof val === 'number' && Number.isFinite(val) && val > 0) { + if (typeof val === "number" && Number.isFinite(val) && val > 0) { return Math.floor(val); } - const name = typeof val === 'string' ? val : (val?.name ?? val?.label ?? val?.text ?? val?.affiliation); + const name = + typeof val === "string" + ? val + : (val?.name ?? val?.label ?? val?.text ?? val?.affiliation); if (!name || !String(name).trim()) return undefined; - const existing = normalizedAffiliations.find(a => a.name === String(name).trim()); + const existing = normalizedAffiliations.find( + (a) => a.name === String(name).trim(), + ); if (existing) return existing.id; const id = normalizedAffiliations.length + 1; - normalizedAffiliations.push({ id, name: String(name).trim(), url: val?.url || val?.link }); + normalizedAffiliations.push({ + id, + name: String(name).trim(), + url: val?.url || val?.link, + }); return id; }; // Normalize authors and map affiliations -> indices (Distill-like) -const normalizedAuthors: Author[] = (Array.isArray(rawAuthors) ? rawAuthors : []) +const normalizedAuthors: Author[] = ( + Array.isArray(rawAuthors) ? rawAuthors : [] +) .map((a: any) => { - if (typeof a === 'string') { + if (typeof a === "string") { return { name: a } as Author; } - const name = String(a?.name || '').trim(); + const name = String(a?.name || "").trim(); const url = a?.url || a?.link; let indices: number[] | undefined = undefined; const raw = a?.affiliations ?? a?.affiliation ?? a?.affils; if (raw != null) { const entries = Array.isArray(raw) ? raw : [raw]; - const ids = entries.map(ensureAffiliation).filter((x): x is number => typeof x === 'number'); + const ids = entries + .map(ensureAffiliation) + .filter((x): x is number => typeof x === "number"); const unique = Array.from(new Set(ids)).sort((x, y) => x - y); if (unique.length) indices = unique; } return { name, url, affiliationIndices: indices } as Author; }) .filter((a: Author) => a.name && a.name.trim().length > 0); -const authorNames: string[] = normalizedAuthors.map(a => a.name); +const authorNames: string[] = normalizedAuthors.map((a) => a.name); const published = articleFM?.published ?? undefined; const tags = articleFM?.tags ?? []; // Prefer seoThumbImage from frontmatter if provided const fmOg = articleFM?.seoThumbImage as string | undefined; -const imageAbs: string = fmOg && fmOg.startsWith('http') - ? fmOg - : (Astro.site ? new URL((fmOg ?? ogDefaultUrl), Astro.site).toString() : (fmOg ?? ogDefaultUrl)); +const imageAbs: string = + fmOg && fmOg.startsWith("http") + ? fmOg + : Astro.site + ? new URL(fmOg ?? ogDefaultUrl, Astro.site).toString() + : (fmOg ?? ogDefaultUrl); // ---- Build citation text & BibTeX from frontmatter ---- -const stripHtml = (text: string) => String(text || '').replace(/<[^>]*>/g, ''); -const rawTitle = articleFM?.title ?? 'Untitled article'; +const stripHtml = (text: string) => String(text || "").replace(/<[^>]*>/g, ""); +const rawTitle = articleFM?.title ?? "Untitled article"; const titleFlat = stripHtml(String(rawTitle)) - .replace(/\\n/g, ' ') - .replace(/\n/g, ' ') - .replace(/\s+/g, ' ') + .replace(/\\n/g, " ") + .replace(/\n/g, " ") + .replace(/\s+/g, " ") .trim(); const extractYear = (val: string | undefined): number | undefined => { if (!val) return undefined; @@ -112,105 +137,204 @@ const extractYear = (val: string | undefined): number | undefined => { }; const year = extractYear(published); -const citationAuthorsText = authorNames.join(', '); -const citationText = `${citationAuthorsText}${year ? ` (${year})` : ''}. "${titleFlat}".`; - -const authorsBib = authorNames.join(' and '); -const keyAuthor = (authorNames[0] || 'article').split(/\s+/).slice(-1)[0].toLowerCase(); -const keyTitle = titleFlat.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '').slice(0, 24); -const bibKey = `${keyAuthor}${year ?? ''}_${keyTitle}`; -const doi = (ArticleMod as any)?.frontmatter?.doi ? String((ArticleMod as any).frontmatter.doi) : undefined; -const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBib}},\n ${year ? `year={${year}},\n ` : ''}${doi ? `doi={${doi}}` : ''}\n}`; +const citationAuthorsText = authorNames.join(", "); +const citationText = `${citationAuthorsText}${year ? ` (${year})` : ""}. "${titleFlat}".`; + +const authorsBib = authorNames.join(" and "); +const keyAuthor = (authorNames[0] || "article") + .split(/\s+/) + .slice(-1)[0] + .toLowerCase(); +const keyTitle = titleFlat + .toLowerCase() + .replace(/[^a-z0-9]+/g, "_") + .replace(/^_|_$/g, "") + .slice(0, 24); +const bibKey = `${keyAuthor}${year ?? ""}_${keyTitle}`; +const doi = (ArticleMod as any)?.frontmatter?.doi + ? String((ArticleMod as any).frontmatter.doi) + : undefined; +const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBib}},\n ${year ? `year={${year}},\n ` : ""}${doi ? `doi={${doi}}` : ""}\n}`; const envCollapse = false; const tableOfContentAutoCollapse = Boolean( - (articleFM as any)?.tableOfContentAutoCollapse ?? (articleFM as any)?.tableOfContentsAutoCollapse ?? envCollapse + (articleFM as any)?.tableOfContentAutoCollapse ?? + (articleFM as any)?.tableOfContentsAutoCollapse ?? + envCollapse, ); // Licence note (HTML allowed) -const licence = (articleFM as any)?.licence ?? (articleFM as any)?.license ?? (articleFM as any)?.licenseNote; +const licence = + (articleFM as any)?.licence ?? + (articleFM as any)?.license ?? + (articleFM as any)?.licenseNote; --- - + + - + - + - + - + + - +
    - +
    -