Molbap HF Staff commited on
Commit
e903a32
·
1 Parent(s): a1ed00a

push a bunch of updates

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +1 -0
  2. .gitignore +38 -0
  3. CHANGELOG.md +118 -0
  4. CONTRIBUTING.md +196 -0
  5. Dockerfile +71 -0
  6. LICENSE +33 -0
  7. README.md +115 -5
  8. app/.astro/astro/content.d.ts +12 -3
  9. app/astro.config.mjs +14 -0
  10. app/dist/_astro/index.BzKj3Iki.css +0 -1
  11. app/dist/index.html +0 -0
  12. app/dist/index.html.gz +2 -2
  13. app/package.json +1 -1
  14. app/public/data +1 -0
  15. app/public/image/Bloatedness_visualizer copy.png +3 -0
  16. app/public/image/Bloatedness_visualizer.png +3 -0
  17. app/public/image/Jaccard_similarity_plot.png +3 -0
  18. app/public/image/big_picture_zoomout.png +3 -0
  19. app/public/image/classic_encoders.png +3 -0
  20. app/{dist/_astro/index.BzKj3Iki.css.gz → public/image/cluster_wave2vec2.png} +2 -2
  21. app/public/image/detr_island.png +3 -0
  22. app/public/image/fast_image_processors copy.png +3 -0
  23. app/public/image/fast_image_processors.png +3 -0
  24. app/public/image/graph_modular_related_models.png +3 -0
  25. app/public/image/hf-logo.svg +8 -0
  26. app/public/image/llama_center.png +3 -0
  27. app/public/image/llama_glm_attn.png +3 -0
  28. app/public/image/model_debugger copy.png +3 -0
  29. app/public/image/model_debugger.png +3 -0
  30. app/public/image/modular_candidates.png +3 -0
  31. app/public/image/popular_models_barplot.png +3 -0
  32. app/public/image/still_graph_bloat.png +3 -0
  33. app/public/image/timeline_llava.png +3 -0
  34. app/public/scripts/color-palettes.js +274 -0
  35. app/scripts/export-latex.mjs +318 -0
  36. app/scripts/export-pdf.mjs +483 -0
  37. app/scripts/generate-trackio-data.mjs +196 -0
  38. app/scripts/jitter-trackio-data.mjs +129 -0
  39. app/scripts/latex-importer/README.md +169 -0
  40. app/scripts/latex-importer/bib-cleaner.mjs +104 -0
  41. app/scripts/latex-importer/filters/equation-ids.lua +134 -0
  42. app/scripts/latex-importer/index.mjs +138 -0
  43. app/scripts/latex-importer/latex-converter.mjs +330 -0
  44. app/scripts/latex-importer/mdx-converter.mjs +896 -0
  45. app/scripts/latex-importer/metadata-extractor.mjs +170 -0
  46. app/scripts/latex-importer/package-lock.json +1272 -0
  47. app/scripts/latex-importer/package.json +33 -0
  48. app/scripts/latex-importer/post-processor.mjs +439 -0
  49. app/scripts/latex-importer/reference-preprocessor.mjs +239 -0
  50. app/scripts/notion-importer/.cursorignore +1 -0
.gitattributes CHANGED
@@ -5,6 +5,7 @@
5
  *.ckpt filter=lfs diff=lfs merge=lfs -text
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
 
8
  *.h5 filter=lfs diff=lfs merge=lfs -text
9
  *.joblib filter=lfs diff=lfs merge=lfs -text
10
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
 
5
  *.ckpt filter=lfs diff=lfs merge=lfs -text
6
  *.ftz filter=lfs diff=lfs merge=lfs -text
7
  *.gz filter=lfs diff=lfs merge=lfs -text
8
+ dist/**/*.gz -filter -diff -merge
9
  *.h5 filter=lfs diff=lfs merge=lfs -text
10
  *.joblib filter=lfs diff=lfs merge=lfs -text
11
  *.lfs.* filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -1 +1,39 @@
1
  node_modules
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  node_modules
2
+ # Python
3
+ __pycache__
4
+ *.py[cod]
5
+ *.so
6
+ .Python
7
+ env/
8
+ venv/
9
+ *.egg-info/
10
+ dist/
11
+ build/
12
+ *.egg
13
+ .idea/
14
+ .vscode/
15
+ .astro/
16
+ .claude/
17
+ *.swp
18
+ .DS_Store
19
+ # Node
20
+ node_modules/
21
+ *.log
22
+ *.env
23
+ *.cache
24
+
25
+ app/scripts/latex-to-mdx/output/
26
+ app/src/content/embeds/typography/generated
27
+
28
+ # PDF export
29
+ app/public/*.pdf
30
+ app/public/*.png
31
+ app/public/*.jpg
32
+ app/public/data/**/*
33
+
34
+ .astro/
35
+
36
+ # Template sync temporary directories
37
+ .template-sync/
38
+ .temp-*/
39
+ .backup-*/
CHANGELOG.md ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Changelog
2
+
3
+ All notable changes to the Research Article Template will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+ - Initial open source release
12
+ - Comprehensive documentation
13
+ - Contributing guidelines
14
+ - License file
15
+
16
+ ## [1.0.0] - 2024-12-19
17
+
18
+ ### Added
19
+ - **Core Features**:
20
+ - Markdown/MDX-based writing system
21
+ - KaTeX mathematical notation support
22
+ - Syntax highlighting for code blocks
23
+ - Academic citations with BibTeX integration
24
+ - Footnotes and sidenotes system
25
+ - Auto-generated table of contents
26
+ - Interactive Mermaid diagrams
27
+ - Plotly.js and D3.js integration
28
+ - HTML embed support
29
+ - Gradio app embedding
30
+ - Dataviz color palettes
31
+ - Image optimization
32
+ - SEO-friendly structure
33
+ - Automatic PDF export
34
+ - Dark/light theme toggle
35
+ - Mobile-responsive design
36
+ - LaTeX import functionality
37
+ - Template synchronization system
38
+
39
+ - **Components**:
40
+ - Figure component with captions
41
+ - MultiFigure for image galleries
42
+ - Note component with variants
43
+ - Quote component
44
+ - Accordion for collapsible content
45
+ - Sidenote component
46
+ - Table of Contents
47
+ - Theme Toggle
48
+ - HTML Embed
49
+ - Raw HTML support
50
+ - SEO component
51
+ - Hero section
52
+ - Footer
53
+ - Full-width and wide layouts
54
+
55
+ - **Build System**:
56
+ - Astro 4.10.0 integration
57
+ - PostCSS with custom media queries
58
+ - Automatic compression
59
+ - Docker support
60
+ - Nginx configuration
61
+ - Git LFS support
62
+
63
+ - **Scripts**:
64
+ - PDF export functionality
65
+ - LaTeX to MDX conversion
66
+ - Template synchronization
67
+ - Font SVG generation
68
+ - TrackIO data generation
69
+
70
+ - **Documentation**:
71
+ - Getting started guide
72
+ - Writing best practices
73
+ - Component reference
74
+ - LaTeX conversion guide
75
+ - Interactive examples
76
+
77
+ ### Technical Details
78
+ - **Framework**: Astro 4.10.0
79
+ - **Styling**: PostCSS with custom properties
80
+ - **Math**: KaTeX 0.16.22
81
+ - **Charts**: Plotly.js 3.1.0, D3.js 7.9.0
82
+ - **Diagrams**: Mermaid 11.10.1
83
+ - **Node.js**: >=20.0.0
84
+ - **License**: CC-BY-4.0
85
+
86
+ ### Browser Support
87
+ - Chrome (latest)
88
+ - Firefox (latest)
89
+ - Safari (latest)
90
+ - Edge (latest)
91
+
92
+ ---
93
+
94
+ ## Version History
95
+
96
+ - **1.0.0**: Initial stable release with full feature set
97
+ - **0.0.1**: Development version (pre-release)
98
+
99
+ ## Migration Guide
100
+
101
+ ### From 0.0.1 to 1.0.0
102
+
103
+ This is the first stable release. No breaking changes from the development version.
104
+
105
+ ### Updating Your Project
106
+
107
+ Use the template synchronization system to update:
108
+
109
+ ```bash
110
+ npm run sync:template -- --dry-run # Preview changes
111
+ npm run sync:template # Apply updates
112
+ ```
113
+
114
+ ## Support
115
+
116
+ - **Documentation**: [Hugging Face Space](https://huggingface.co/spaces/tfrere/research-article-template)
117
+ - **Issues**: [Community Discussions](https://huggingface.co/spaces/tfrere/research-article-template/discussions)
118
+ - **Contact**: [@tfrere](https://huggingface.co/tfrere)
CONTRIBUTING.md ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing to Research Article Template
2
+
3
+ Thank you for your interest in contributing to the Research Article Template! This document provides guidelines and information for contributors.
4
+
5
+ ## 🤝 How to Contribute
6
+
7
+ ### Reporting Issues
8
+
9
+ Before creating an issue, please:
10
+ 1. **Search existing issues** to avoid duplicates
11
+ 2. **Use the issue template** when available
12
+ 3. **Provide detailed information**:
13
+ - Clear description of the problem
14
+ - Steps to reproduce
15
+ - Expected vs actual behavior
16
+ - Environment details (OS, Node.js version, browser)
17
+ - Screenshots if applicable
18
+
19
+ ### Suggesting Features
20
+
21
+ We welcome feature suggestions! Please:
22
+ 1. **Check existing discussions** first
23
+ 2. **Describe the use case** clearly
24
+ 3. **Explain the benefits** for the community
25
+ 4. **Consider implementation complexity**
26
+
27
+ ### Code Contributions
28
+
29
+ #### Getting Started
30
+
31
+ 1. **Fork the repository** on Hugging Face
32
+ 2. **Clone your fork**:
33
+ ```bash
34
+ git clone git@hf.co:spaces/<your-username>/research-article-template
35
+ cd research-article-template
36
+ ```
37
+ 3. **Install dependencies**:
38
+ ```bash
39
+ cd app
40
+ npm install
41
+ ```
42
+ 4. **Create a feature branch**:
43
+ ```bash
44
+ git checkout -b feature/your-feature-name
45
+ ```
46
+
47
+ #### Development Workflow
48
+
49
+ 1. **Make your changes** following our coding standards
50
+ 2. **Test thoroughly**:
51
+ ```bash
52
+ npm run dev # Test locally
53
+ npm run build # Ensure build works
54
+ ```
55
+ 3. **Update documentation** if needed
56
+ 4. **Commit with clear messages**:
57
+ ```bash
58
+ git commit -m "feat: add new component for interactive charts"
59
+ ```
60
+
61
+ #### Pull Request Process
62
+
63
+ 1. **Push your branch**:
64
+ ```bash
65
+ git push origin feature/your-feature-name
66
+ ```
67
+ 2. **Create a Pull Request** with:
68
+ - Clear title and description
69
+ - Reference related issues
70
+ - Screenshots for UI changes
71
+ - Testing instructions
72
+
73
+ ## 📋 Coding Standards
74
+
75
+ ### Code Style
76
+
77
+ - **Use Prettier** for consistent formatting
78
+ - **Follow existing patterns** in the codebase
79
+ - **Write clear, self-documenting code**
80
+ - **Add comments** for complex logic
81
+ - **Use meaningful variable names**
82
+
83
+ ### File Organization
84
+
85
+ - **Components**: Place in `src/components/`
86
+ - **Styles**: Use CSS modules or component-scoped styles
87
+ - **Assets**: Organize in `src/content/assets/`
88
+ - **Documentation**: Update relevant `.mdx` files
89
+
90
+ ### Commit Message Format
91
+
92
+ We follow [Conventional Commits](https://www.conventionalcommits.org/):
93
+
94
+ ```
95
+ type(scope): description
96
+
97
+ feat: add new interactive chart component
98
+ fix: resolve mobile layout issues
99
+ docs: update installation instructions
100
+ style: improve button hover states
101
+ refactor: simplify component structure
102
+ test: add unit tests for utility functions
103
+ ```
104
+
105
+ **Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
106
+
107
+ ## 🧪 Testing
108
+
109
+ ### Manual Testing
110
+
111
+ Before submitting:
112
+ - [ ] Test on different screen sizes
113
+ - [ ] Verify dark/light theme compatibility
114
+ - [ ] Check browser compatibility (Chrome, Firefox, Safari)
115
+ - [ ] Test with different content types
116
+ - [ ] Ensure accessibility standards
117
+
118
+ ### Automated Testing
119
+
120
+ ```bash
121
+ # Run build to catch errors
122
+ npm run build
123
+
124
+ # Test PDF export
125
+ npm run export:pdf
126
+
127
+ # Test LaTeX conversion
128
+ npm run latex:convert
129
+ ```
130
+
131
+ ## 📚 Documentation
132
+
133
+ ### Writing Guidelines
134
+
135
+ - **Use clear, concise language**
136
+ - **Provide examples** for complex features
137
+ - **Include screenshots** for UI changes
138
+ - **Update both English content and code comments**
139
+
140
+ ### Documentation Structure
141
+
142
+ - **README.md**: Project overview and quick start
143
+ - **CONTRIBUTING.md**: This file
144
+ - **Content files**: In `src/content/chapters/demo/`
145
+ - **Component docs**: Inline comments and examples
146
+
147
+ ## 🎯 Areas for Contribution
148
+
149
+ ### High Priority
150
+
151
+ - **Bug fixes** and stability improvements
152
+ - **Accessibility enhancements**
153
+ - **Mobile responsiveness**
154
+ - **Performance optimizations**
155
+ - **Documentation improvements**
156
+
157
+ ### Feature Ideas
158
+
159
+ - **New interactive components**
160
+ - **Additional export formats**
161
+ - **Enhanced LaTeX import**
162
+ - **Theme customization**
163
+ - **Plugin system**
164
+
165
+ ### Community
166
+
167
+ - **Answer questions** in discussions
168
+ - **Share examples** of your work
169
+ - **Write tutorials** and guides
170
+ - **Help with translations**
171
+
172
+ ## 🚫 What Not to Contribute
173
+
174
+ - **Breaking changes** without discussion
175
+ - **Major architectural changes** without approval
176
+ - **Dependencies** that significantly increase bundle size
177
+ - **Features** that don't align with the project's goals
178
+
179
+ ## 📞 Getting Help
180
+
181
+ - **Discussions**: [Community tab](https://huggingface.co/spaces/tfrere/research-article-template/discussions)
182
+ - **Issues**: [Report bugs](https://huggingface.co/spaces/tfrere/research-article-template/discussions?status=open&type=issue)
183
+ - **Contact**: [@tfrere](https://huggingface.co/tfrere) on Hugging Face
184
+
185
+ ## 📄 License
186
+
187
+ By contributing, you agree that your contributions will be licensed under the same [CC-BY-4.0 license](LICENSE) that covers the project.
188
+
189
+ ## 🙏 Recognition
190
+
191
+ Contributors will be:
192
+ - **Listed in acknowledgments** (if desired)
193
+ - **Mentioned in release notes** for significant contributions
194
+ - **Credited** in relevant documentation
195
+
196
+ Thank you for helping make scientific writing more accessible and interactive! 🎉
Dockerfile ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Node runtime as the base image for building the application
2
+ # Build with Playwright (browsers and deps ready)
3
+ FROM mcr.microsoft.com/playwright:v1.55.0-jammy AS build
4
+
5
+ # Install git, git-lfs, and dependencies for Pandoc (only if ENABLE_LATEX_CONVERSION=true)
6
+ RUN apt-get update && apt-get install -y git git-lfs wget && apt-get clean
7
+
8
+ # Install latest Pandoc from GitHub releases (only installed if needed later)
9
+ RUN wget -qO- https://github.com/jgm/pandoc/releases/download/3.8/pandoc-3.8-linux-amd64.tar.gz | tar xzf - -C /tmp && \
10
+ cp /tmp/pandoc-3.8/bin/pandoc /usr/local/bin/ && \
11
+ cp /tmp/pandoc-3.8/bin/pandoc-lua /usr/local/bin/ && \
12
+ rm -rf /tmp/pandoc-3.8
13
+
14
+ # Set the working directory in the container
15
+ WORKDIR /app
16
+
17
+ # Copy package.json and package-lock.json
18
+ COPY app/package*.json ./
19
+
20
+ # Install dependencies
21
+ RUN npm install
22
+
23
+ # Copy the rest of the application code
24
+ COPY app/ .
25
+
26
+ # Conditionally convert LaTeX to MDX if ENABLE_LATEX_CONVERSION=true
27
+ ARG ENABLE_LATEX_CONVERSION=false
28
+ RUN if [ "$ENABLE_LATEX_CONVERSION" = "true" ]; then \
29
+ echo "🔄 LaTeX importer enabled - running latex:convert..."; \
30
+ npm run latex:convert; \
31
+ else \
32
+ echo "⏭️ LaTeX importer disabled - skipping..."; \
33
+ fi
34
+
35
+ # Ensure `public/data` is a real directory with real files (not a symlink)
36
+ # This handles the case where `public/data` is a symlink in the repo, which
37
+ # would be broken inside the container after COPY.
38
+ RUN set -e; \
39
+ if [ -e public ] && [ ! -d public ]; then rm -f public; fi; \
40
+ mkdir -p public; \
41
+ if [ -L public/data ] || { [ -e public/data ] && [ ! -d public/data ]; }; then rm -f public/data; fi; \
42
+ mkdir -p public/data; \
43
+ cp -a src/content/assets/data/. public/data/
44
+
45
+ # Build the application
46
+ RUN npm run build
47
+
48
+ # Generate the PDF (light theme, full wait)
49
+ RUN npm run export:pdf -- --theme=light --wait=full
50
+
51
+ # Use an official Nginx runtime as the base image for serving the application
52
+ FROM nginx:alpine
53
+
54
+ # Copy the built application from the build stage
55
+ COPY --from=build /app/dist /usr/share/nginx/html
56
+
57
+ # Copy a custom Nginx configuration file
58
+ COPY nginx.conf /etc/nginx/nginx.conf
59
+
60
+ # Create necessary directories and set permissions
61
+ RUN mkdir -p /var/cache/nginx /var/run /var/log/nginx && \
62
+ chmod -R 777 /var/cache/nginx /var/run /var/log/nginx /etc/nginx/nginx.conf
63
+
64
+ # Switch to non-root user
65
+ USER nginx
66
+
67
+ # Expose port 8080
68
+ EXPOSE 8080
69
+
70
+ # Command to run the application
71
+ CMD ["nginx", "-g", "daemon off;"]
LICENSE ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Creative Commons Attribution 4.0 International License
2
+
3
+ Copyright (c) 2024 Thibaud Frere
4
+
5
+ This work is licensed under the Creative Commons Attribution 4.0 International License.
6
+ To view a copy of this license, visit http://creativecommons.org/licenses/by/4.0/
7
+ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA.
8
+
9
+ You are free to:
10
+
11
+ Share — copy and redistribute the material in any medium or format
12
+ Adapt — remix, transform, and build upon the material for any purpose, even commercially.
13
+
14
+ The licensor cannot revoke these freedoms as long as you follow the license terms.
15
+
16
+ Under the following terms:
17
+
18
+ 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.
19
+
20
+ No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits.
21
+
22
+ Notices:
23
+
24
+ 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.
25
+
26
+ 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.
27
+
28
+ ---
29
+
30
+ For the source code and technical implementation:
31
+ - The source code is available at: https://huggingface.co/spaces/tfrere/research-article-template
32
+ - Third-party figures and assets are excluded from this license and marked in their captions
33
+ - Dependencies and third-party libraries maintain their respective licenses
README.md CHANGED
@@ -1,11 +1,121 @@
1
  ---
2
- title: Maintain the unmaintainable
3
  emoji: 📚
4
- colorFrom: pink
5
  colorTo: indigo
6
- sdk: static
7
- app_file: app/dist/index.html
8
  pinned: false
 
 
 
 
 
 
 
 
9
  ---
 
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: 'Maintain the unmaintainable'
3
  emoji: 📚
4
+ colorFrom: blue
5
  colorTo: indigo
6
+ sdk: docker
 
7
  pinned: false
8
+ header: mini
9
+ app_port: 8080
10
+ tags:
11
+ - research-article-template
12
+ - research paper
13
+ - scientific paper
14
+ - data visualization
15
+ thumbnail: https://huggingface.co/spaces/tfrere/research-paper-template/thumb.jpg
16
  ---
17
+ <div align="center">
18
 
19
+ # Research Article Template
20
+
21
+ [![License: CC BY 4.0](https://img.shields.io/badge/License-CC%20BY%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by/4.0/)
22
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen.svg)](https://nodejs.org/)
23
+ [![Astro](https://img.shields.io/badge/Astro-4.10.0-orange.svg)](https://astro.build/)
24
+ [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/tfrere/research-article-template)
25
+
26
+
27
+ **A modern, interactive template for scientific writing** that brings papers to life with web-native features. The web offers what static PDFs can't: **interactive diagrams**, **progressive notation**, and **exploratory views** that show how ideas behave. This template treats interactive artifacts—figures, math, code, and inspectable experiments—as **first-class** alongside prose, helping readers **build intuition** instead of skimming results—all with **minimal setup** and no web knowledge required.
28
+
29
+ **[Try the live demo & documentation →](https://huggingface.co/spaces/tfrere/research-article-template)**
30
+
31
+ </div>
32
+
33
+ ## 🚀 Quick Start
34
+
35
+ ### Option 1: Duplicate on Hugging Face (Recommended)
36
+
37
+ 1. Visit **[🤗 Research Article Template](https://huggingface.co/spaces/tfrere/research-article-template)**
38
+ 2. Click **"Duplicate this Space"**
39
+ 3. Clone your new repository:
40
+ ```bash
41
+ git clone git@hf.co:spaces/<your-username>/<your-space>
42
+ cd <your-space>
43
+ ```
44
+
45
+ ### Option 2: Clone Directly
46
+
47
+ ```bash
48
+ git clone https://github.com/tfrere/research-article-template.git
49
+ cd research-article-template
50
+ ```
51
+
52
+ ### Installation
53
+
54
+ ```bash
55
+ # Install Node.js 20+ (use nvm for version management)
56
+ nvm install 20
57
+ nvm use 20
58
+
59
+ # Install Git LFS and pull assets
60
+ git lfs install
61
+ git lfs pull
62
+
63
+ # Install dependencies
64
+ cd app
65
+ npm install
66
+
67
+ # Start development server
68
+ npm run dev
69
+ ```
70
+
71
+ Visit `http://localhost:4321` to see your site!
72
+
73
+ ## 🎯 Who This Is For
74
+
75
+ - **Scientists** writing modern, web-native research papers
76
+ - **Educators** creating interactive, explorable lessons
77
+ - **Researchers** who want to focus on ideas, not infrastructure
78
+ - **Anyone** who values clear, engaging technical communication
79
+
80
+ ## 🌟 Inspired by Distill
81
+
82
+ This template carries forward the spirit of [Distill](https://distill.pub/) (2016–2021), pushing interactive scientific writing even further with:
83
+ - Accessible, high-quality explanations
84
+ - Reproducible, production-ready demos
85
+ - Modern web technologies and best practices
86
+
87
+ ## 🤝 Contributing
88
+
89
+ We welcome contributions! Please see our [Contributing Guidelines](CONTRIBUTING.md) for details.
90
+
91
+ ### Ways to Contribute
92
+
93
+ - **Report bugs** - Open an issue with detailed information
94
+ - **Suggest features** - Share ideas for improvements
95
+ - **Improve documentation** - Help others get started
96
+ - **Submit code** - Fix bugs or add features
97
+ - **Join discussions** - Share feedback and ideas
98
+
99
+ ## 📄 License
100
+
101
+ This project is licensed under the [Creative Commons Attribution 4.0 International License](https://creativecommons.org/licenses/by/4.0/).
102
+
103
+ - **Diagrams and text**: CC-BY 4.0
104
+ - **Source code**: Available on [Hugging Face](https://huggingface.co/spaces/tfrere/research-article-template)
105
+ - **Third-party figures**: Excluded and marked in captions
106
+
107
+ ## 🙏 Acknowledgments
108
+
109
+ - Inspired by [Distill](https://distill.pub/) and the interactive scientific writing movement
110
+ - Built with [Astro](https://astro.build/), [MDX](https://mdxjs.com/), and modern web technologies
111
+ - Community feedback and contributions from researchers worldwide
112
+
113
+ ## 📞 Support
114
+
115
+ - **[Community Discussions](https://huggingface.co/spaces/tfrere/research-article-template/discussions)** - Ask questions and share ideas
116
+ - **[Report Issues](https://huggingface.co/spaces/tfrere/research-article-template/discussions?status=open&type=issue)** - Bug reports and feature requests
117
+ - **Contact**: [@tfrere](https://huggingface.co/tfrere) on Hugging Face
118
+
119
+ ---
120
+
121
+ **Made with ❤️ for the scientific community**
app/.astro/astro/content.d.ts CHANGED
@@ -151,13 +151,22 @@ declare module 'astro:content' {
151
  >;
152
 
153
  type ContentEntryMap = {
154
-
 
 
 
 
 
 
 
 
 
155
  };
156
 
157
  type DataEntryMap = {
158
- "embeds": Record<string, {
159
  id: string;
160
- collection: "embeds";
161
  data: any;
162
  }>;
163
 
 
151
  >;
152
 
153
  type ContentEntryMap = {
154
+ "embeds": {
155
+ "demo/vibe-code-d3-embeds-directives.md": {
156
+ id: "demo/vibe-code-d3-embeds-directives.md";
157
+ slug: "demo/vibe-code-d3-embeds-directives";
158
+ body: string;
159
+ collection: "embeds";
160
+ data: any
161
+ } & { render(): Render[".md"] };
162
+ };
163
+
164
  };
165
 
166
  type DataEntryMap = {
167
+ "assets": Record<string, {
168
  id: string;
169
+ collection: "assets";
170
  data: any;
171
  }>;
172
 
app/astro.config.mjs CHANGED
@@ -5,11 +5,16 @@ import mermaid from 'astro-mermaid';
5
  import compressor from 'astro-compressor';
6
  import remarkMath from 'remark-math';
7
  import rehypeKatex from 'rehype-katex';
 
8
  import rehypeSlug from 'rehype-slug';
9
  import rehypeAutolinkHeadings from 'rehype-autolink-headings';
 
10
  import rehypeCodeCopy from './plugins/rehype/code-copy.mjs';
 
 
11
  import remarkDirective from 'remark-directive';
12
  import remarkOutputContainer from './plugins/remark/output-container.mjs';
 
13
  import rehypeWrapTables from './plugins/rehype/wrap-tables.mjs';
14
  import rehypeWrapOutput from './plugins/rehype/wrap-outputs.mjs';
15
  // Built-in Shiki (dual themes) — no rehype-pretty-code
@@ -42,7 +47,9 @@ export default defineConfig({
42
  }
43
  },
44
  remarkPlugins: [
 
45
  remarkMath,
 
46
  remarkDirective,
47
  remarkOutputContainer
48
  ],
@@ -52,6 +59,13 @@ export default defineConfig({
52
  [rehypeKatex, {
53
  trust: true,
54
  }],
 
 
 
 
 
 
 
55
  rehypeCodeCopy,
56
  rehypeWrapOutput,
57
  rehypeWrapTables
 
5
  import compressor from 'astro-compressor';
6
  import remarkMath from 'remark-math';
7
  import rehypeKatex from 'rehype-katex';
8
+ import remarkFootnotes from 'remark-footnotes';
9
  import rehypeSlug from 'rehype-slug';
10
  import rehypeAutolinkHeadings from 'rehype-autolink-headings';
11
+ import rehypeCitation from 'rehype-citation';
12
  import rehypeCodeCopy from './plugins/rehype/code-copy.mjs';
13
+ import rehypeReferencesAndFootnotes from './plugins/rehype/post-citation.mjs';
14
+ import remarkIgnoreCitationsInCode from './plugins/remark/ignore-citations-in-code.mjs';
15
  import remarkDirective from 'remark-directive';
16
  import remarkOutputContainer from './plugins/remark/output-container.mjs';
17
+ import rehypeRestoreAtInCode from './plugins/rehype/restore-at-in-code.mjs';
18
  import rehypeWrapTables from './plugins/rehype/wrap-tables.mjs';
19
  import rehypeWrapOutput from './plugins/rehype/wrap-outputs.mjs';
20
  // Built-in Shiki (dual themes) — no rehype-pretty-code
 
47
  }
48
  },
49
  remarkPlugins: [
50
+ remarkIgnoreCitationsInCode,
51
  remarkMath,
52
+ [remarkFootnotes, { inlineNotes: true }],
53
  remarkDirective,
54
  remarkOutputContainer
55
  ],
 
59
  [rehypeKatex, {
60
  trust: true,
61
  }],
62
+ [rehypeCitation, {
63
+ bibliography: 'src/content/bibliography.bib',
64
+ linkCitations: true,
65
+ csl: "apa",
66
+ }],
67
+ rehypeReferencesAndFootnotes,
68
+ rehypeRestoreAtInCode,
69
  rehypeCodeCopy,
70
  rehypeWrapOutput,
71
  rehypeWrapTables
app/dist/_astro/index.BzKj3Iki.css DELETED
@@ -1 +0,0 @@
1
- @import"https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,200..900;1,200..900&display=swap";.html-embed{margin:0 0 var(--block-spacing-y);z-index:var(--z-elevated);position:relative;width:min(1100px,100vw - var(--content-padding-x) * 2);margin-left:50%;transform:translate(-50%)}.html-embed__title{text-align:left;font-weight:600;font-size:.95rem;color:var(--text-color);margin:0;padding:0;padding-bottom:var(--spacing-1);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:12px;z-index:calc(var(--z-elevated) + 1);position:relative}.html-embed__card.is-frameless{background:transparent;border-color:transparent;padding:0}.html-embed__desc{text-align:left;font-size:.9rem;color:var(--muted-color);margin:0;padding:0;padding-top:var(--spacing-1);position:relative;z-index:var(--z-elevated);display:block;width:100%;background:var(--page-bg)}.html-embed__card svg text{fill:var(--text-color)}.html-embed__card label{color:var(--text-color)}.plotly-graph-div{width:100%;min-height:320px}@media (max-width: 768px){.plotly-graph-div{min-height:260px}}[id^=plot-]{display:flex;flex-direction:column;align-items:center;gap:15px}.plotly_caption{font-style:italic;margin-top:10px}.plotly_controls{display:flex;flex-wrap:wrap;justify-content:center;gap:30px}.plotly_input_container{display:flex;align-items:center;flex-direction:column;gap:10px}.plotly_input_container>select{padding:2px 4px;line-height:1.5em;text-align:center;border-radius:4px;font-size:12px;background-color:var(--neutral-200);outline:none;border:1px solid var(--neutral-300)}.plotly_slider{display:flex;align-items:center;gap:10px}.plotly_slider>input[type=range]{-webkit-appearance:none;-moz-appearance:none;appearance:none;height:2px;background:var(--neutral-400);border-radius:5px;outline:none}.plotly_slider>input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;border-radius:50%;background:var(--primary-color);cursor:pointer}.plotly_slider>input[type=range]::-moz-range-thumb{width:18px;height:18px;border-radius:50%;background:var(--primary-color);cursor:pointer}.plotly_slider>span{font-size:14px;line-height:1.6em;min-width:16px}[data-theme=dark] .html-embed__card:not(.is-frameless){background:#12151b;border-color:#ffffff26}[data-theme=dark] .html-embed__card .xaxislayer-above text,[data-theme=dark] .html-embed__card .yaxislayer-above text,[data-theme=dark] .html-embed__card .infolayer text,[data-theme=dark] .html-embed__card .legend text,[data-theme=dark] .html-embed__card .annotation text,[data-theme=dark] .html-embed__card .colorbar text,[data-theme=dark] .html-embed__card .hoverlayer text{fill:#fff!important}[data-theme=dark] .html-embed__card .xaxislayer-above path,[data-theme=dark] .html-embed__card .yaxislayer-above path,[data-theme=dark] .html-embed__card .xlines-above,[data-theme=dark] .html-embed__card .ylines-above{stroke:#ffffff59!important}[data-theme=dark] .html-embed__card .gridlayer path{stroke:#ffffff26!important}[data-theme=dark] .html-embed__card .legend rect.bg{fill:#00000040!important;stroke:#fff3!important}[data-theme=dark] .html-embed__card .hoverlayer .bg{fill:#000c!important;stroke:#fff3!important}[data-theme=dark] .html-embed__card .colorbar .cbbg{fill:#00000040!important;stroke:#fff3!important}.force-light-mode{filter:invert(0);--csstools-color-scheme--light: initial;color-scheme:light;background:#fff;padding:20px;border-radius:10px}[data-theme=dark] .force-light-mode .html-embed__card{background:#fff!important;border-color:#ddd!important}[data-theme=dark] .force-light-mode *{color:#333!important}@media (max-width: 1024px){.html-embed{width:100%;margin-left:0;transform:none}}@media print{.html-embed,.html-embed__card{max-width:100%!important;width:100%!important;margin-left:0!important;margin-right:0!important}.html-embed__card{padding:6px}.html-embed__card.is-frameless{padding:0}.html-embed__card svg,.html-embed__card canvas,.html-embed__card img{max-width:100%!important;height:auto!important}.html-embed__card>div[id^=frag-]{width:100%!important}}@media print{.html-embed,.html-embed__card{-moz-column-break-inside:avoid;break-inside:avoid;page-break-inside:avoid}.html-embed,.html-embed__card{max-width:100%!important;width:100%!important}.html-embed__card{padding:6px}.html-embed__card.is-frameless{padding:0}.html-embed__card svg,.html-embed__card canvas,.html-embed__card img,.html-embed__card video,.html-embed__card iframe{max-width:100%!important;height:auto!important}.html-embed__card>div[id^=frag-]{width:100%!important;max-width:100%!important}.html-embed .d3-galaxy{width:100%!important;max-width:980px!important;margin-left:auto!important;margin-right:auto!important}}.hero[data-astro-cid-bbe6dxrz]{width:100%;padding:0;text-align:center}.hero-title[data-astro-cid-bbe6dxrz]{font-size:max(28px,min(4vw,48px));font-weight:800;line-height:1.1;max-width:100%;margin:auto}.hero-banner[data-astro-cid-bbe6dxrz]{max-width:980px;margin:0 auto}.hero-desc[data-astro-cid-bbe6dxrz]{color:var(--muted-color);font-style:italic;margin:0 0 16px}.meta[data-astro-cid-bbe6dxrz]{border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color);padding:1rem 0;font-size:.9rem}.meta-container[data-astro-cid-bbe6dxrz]{max-width:760px;display:flex;flex-direction:row;justify-content:space-between;margin:0 auto;padding:0 var(--content-padding-x);gap:8px}.meta-container[data-astro-cid-bbe6dxrz] a[data-astro-cid-bbe6dxrz]:not(.button){color:var(--primary-color);-webkit-text-decoration:underline;text-decoration:underline;text-underline-offset:2px;text-decoration-thickness:.06em;text-decoration-color:var(--link-underline);transition:text-decoration-color .15s ease-in-out}.meta-container[data-astro-cid-bbe6dxrz] a[data-astro-cid-bbe6dxrz]:hover{text-decoration-color:var(--link-underline-hover)}.meta-container[data-astro-cid-bbe6dxrz] a[data-astro-cid-bbe6dxrz].button,.meta-container[data-astro-cid-bbe6dxrz] .button[data-astro-cid-bbe6dxrz]{-webkit-text-decoration:none;text-decoration:none}.meta-container-cell[data-astro-cid-bbe6dxrz]{display:flex;flex-direction:column;gap:8px;max-width:250px}.meta-container-cell[data-astro-cid-bbe6dxrz] h3[data-astro-cid-bbe6dxrz]{margin:0;font-size:12px;font-weight:400;color:var(--muted-color);text-transform:uppercase;letter-spacing:.02em}.meta-container-cell[data-astro-cid-bbe6dxrz] p[data-astro-cid-bbe6dxrz]{margin:0}.authors[data-astro-cid-bbe6dxrz]{margin:0;list-style-type:none;padding-left:0;display:flex;flex-wrap:wrap}.authors[data-astro-cid-bbe6dxrz] li[data-astro-cid-bbe6dxrz]{white-space:nowrap;margin-right:4px}.affiliations[data-astro-cid-bbe6dxrz]{margin:0;padding-left:1.25em}.affiliations[data-astro-cid-bbe6dxrz] li[data-astro-cid-bbe6dxrz]{margin:0}header[data-astro-cid-bbe6dxrz].meta .meta-container[data-astro-cid-bbe6dxrz]{flex-wrap:wrap;row-gap:12px}@media (max-width: 768px){.meta-container-cell--affiliations[data-astro-cid-bbe6dxrz],.meta-container-cell--pdf[data-astro-cid-bbe6dxrz]{text-align:right}}@media print{.meta-container-cell--pdf[data-astro-cid-bbe6dxrz]{display:none!important}}.footer{contain:layout style;font-size:.8em;line-height:1.7em;margin-top:60px;margin-bottom:0;border-top:1px solid rgba(0,0,0,.1);color:#00000080}.footer-inner{max-width:1280px;margin:0 auto;padding:60px 16px 48px;display:grid;grid-template-columns:220px minmax(0,680px) 260px;grid-gap:32px;gap:32px;align-items:start}.citation-block,.acknowledgements-block,.references-block,.reuse-block,.doi-block{display:contents}.citation-block>h3,.acknowledgements-block>h3,.references-block>h3,.reuse-block>h3,.doi-block>h3{grid-column:1;font-size:15px;margin:0;text-align:right;padding-right:30px}.citation-block>:not(h3),.acknowledgements-block>:not(h3),.references-block>:not(h3),.reuse-block>:not(h3),.doi-block>:not(h3){grid-column:2}.citation-block h3{margin:0 0 8px}.citation-block h4{margin:16px 0 8px;font-size:14px;text-transform:uppercase;color:var(--muted-color)}.citation-block p,.acknowledgements-block p,.reuse-block p,.doi-block p,.footnotes ol,.footnotes ol p,.references{margin-top:0}.citation{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:11px;line-height:15px;border-left:1px solid rgba(0,0,0,.1);border:1px solid rgba(0,0,0,.1);background:#00000005;padding:10px 18px;border-radius:3px;color:#969696;overflow:hidden;margin-top:-12px;white-space:pre-wrap;word-wrap:break-word}.citation a{color:#0009;-webkit-text-decoration:underline;text-decoration:underline}.citation.short{margin-top:-4px}.references-block h3{margin:0}.references-block ol{padding:0 0 0 15px}@media (min-width: 768px){.references-block ol{padding:0 0 0 30px;margin-left:-30px}}.references-block li{margin-bottom:1em}.references-block a{color:var(--text-color)}[data-theme=dark] .footer{border-top-color:#ffffff26;color:#c8c8c8cc}[data-theme=dark] .citation{background:#ffffff0a;border-color:#ffffff26;color:#c8c8c8}[data-theme=dark] .citation a{color:#ffffffbf}.footer a{color:var(--primary-color);border-bottom:1px solid var(--link-underline);-webkit-text-decoration:none;text-decoration:none}.footer a:hover{color:var(--primary-color-hover);border-bottom-color:var(--link-underline-hover)}[data-theme=dark] .footer a{color:var(--primary-color)}#theme-toggle[data-astro-cid-x3pjskd3]{display:inline-flex;align-items:center;gap:8px;border:none;background:transparent;padding:6px 10px;border-radius:8px;cursor:pointer;color:var(--text-color)!important}#theme-toggle[data-astro-cid-x3pjskd3] .icon[data-astro-cid-x3pjskd3].dark,[data-astro-cid-x3pjskd3][data-theme=dark] #theme-toggle[data-astro-cid-x3pjskd3] .icon[data-astro-cid-x3pjskd3].light{display:none}[data-astro-cid-x3pjskd3][data-theme=dark] #theme-toggle[data-astro-cid-x3pjskd3] .icon[data-astro-cid-x3pjskd3].dark{display:inline}#theme-toggle[data-astro-cid-x3pjskd3] .icon[data-astro-cid-x3pjskd3]{filter:none!important}.table-of-contents{position:sticky;top:32px;margin-top:12px}.table-of-contents nav{border-left:1px solid var(--border-color);padding-left:16px;font-size:13px}.table-of-contents .title{font-weight:600;font-size:14px;margin-bottom:8px}.table-of-contents nav ul{margin:0 0 6px;padding-left:1em}.table-of-contents nav li{list-style:none;margin:.25em 0}.table-of-contents nav a,.table-of-contents nav a:link,.table-of-contents nav a:visited{color:var(--text-color);-webkit-text-decoration:none;text-decoration:none;border-bottom:none}.table-of-contents nav>ul>li>a{font-weight:700}.table-of-contents nav a:hover{-webkit-text-decoration:underline solid var(--muted-color);text-decoration:underline solid var(--muted-color)}.table-of-contents nav a.active{-webkit-text-decoration:underline;text-decoration:underline}.table-of-contents-mobile{display:none;margin:8px 0 16px}.table-of-contents-mobile>summary{cursor:pointer;list-style:none;padding:var(--spacing-3) var(--spacing-4);border:1px solid var(--border-color);border-radius:8px;color:var(--text-color);font-weight:600;position:relative}.table-of-contents-mobile[open]>summary{border-bottom-left-radius:0;border-bottom-right-radius:0}.table-of-contents-mobile>summary:after{content:"";position:absolute;right:var(--spacing-4);top:50%;width:8px;height:8px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:translateY(-70%) rotate(45deg);transition:transform .15s ease;opacity:.7}.table-of-contents-mobile[open]>summary:after{transform:translateY(-30%) rotate(-135deg)}.table-of-contents-mobile nav{border-left:none;padding:10px 12px;font-size:14px;border:1px solid var(--border-color);border-top:none;border-bottom-left-radius:8px;border-bottom-right-radius:8px}.table-of-contents-mobile nav ul{margin:0 0 6px;padding-left:1em}.table-of-contents-mobile nav li{list-style:none;margin:.25em 0}.table-of-contents-mobile nav a,.table-of-contents-mobile nav a:link,.table-of-contents-mobile nav a:visited{color:var(--text-color);-webkit-text-decoration:none;text-decoration:none;border-bottom:none}.table-of-contents-mobile nav>ul>li>a{font-weight:700}.table-of-contents-mobile nav a:hover{-webkit-text-decoration:underline solid var(--muted-color);text-decoration:underline solid var(--muted-color)}.table-of-contents-mobile nav a.active{-webkit-text-decoration:underline;text-decoration:underline}@font-face{font-family:KaTeX_AMS;font-style:normal;font-weight:400;src:url(/_astro/KaTeX_AMS-Regular.BQhdFMY1.woff2) format("woff2"),url(/_astro/KaTeX_AMS-Regular.DMm9YOAa.woff) format("woff"),url(/_astro/KaTeX_AMS-Regular.DRggAlZN.ttf) format("truetype")}@font-face{font-family:KaTeX_Caligraphic;font-style:normal;font-weight:700;src:url(/_astro/KaTeX_Caligraphic-Bold.Dq_IR9rO.woff2) format("woff2"),url(/_astro/KaTeX_Caligraphic-Bold.BEiXGLvX.woff) format("woff"),url(/_astro/KaTeX_Caligraphic-Bold.ATXxdsX0.ttf) format("truetype")}@font-face{font-family:KaTeX_Caligraphic;font-style:normal;font-weight:400;src:url(/_astro/KaTeX_Caligraphic-Regular.Di6jR-x-.woff2) format("woff2"),url(/_astro/KaTeX_Caligraphic-Regular.CTRA-rTL.woff) format("woff"),url(/_astro/KaTeX_Caligraphic-Regular.wX97UBjC.ttf) format("truetype")}@font-face{font-family:KaTeX_Fraktur;font-style:normal;font-weight:700;src:url(/_astro/KaTeX_Fraktur-Bold.CL6g_b3V.woff2) format("woff2"),url(/_astro/KaTeX_Fraktur-Bold.BsDP51OF.woff) format("woff"),url(/_astro/KaTeX_Fraktur-Bold.BdnERNNW.ttf) format("truetype")}@font-face{font-family:KaTeX_Fraktur;font-style:normal;font-weight:400;src:url(/_astro/KaTeX_Fraktur-Regular.CTYiF6lA.woff2) format("woff2"),url(/_astro/KaTeX_Fraktur-Regular.Dxdc4cR9.woff) format("woff"),url(/_astro/KaTeX_Fraktur-Regular.CB_wures.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:normal;font-weight:700;src:url(/_astro/KaTeX_Main-Bold.Cx986IdX.woff2) format("woff2"),url(/_astro/KaTeX_Main-Bold.Jm3AIy58.woff) format("woff"),url(/_astro/KaTeX_Main-Bold.waoOVXN0.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:italic;font-weight:700;src:url(/_astro/KaTeX_Main-BoldItalic.DxDJ3AOS.woff2) format("woff2"),url(/_astro/KaTeX_Main-BoldItalic.SpSLRI95.woff) format("woff"),url(/_astro/KaTeX_Main-BoldItalic.DzxPMmG6.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:italic;font-weight:400;src:url(/_astro/KaTeX_Main-Italic.NWA7e6Wa.woff2) format("woff2"),url(/_astro/KaTeX_Main-Italic.BMLOBm91.woff) format("woff"),url(/_astro/KaTeX_Main-Italic.3WenGoN9.ttf) format("truetype")}@font-face{font-family:KaTeX_Main;font-style:normal;font-weight:400;src:url(/_astro/KaTeX_Main-Regular.B22Nviop.woff2) format("woff2"),url(/_astro/KaTeX_Main-Regular.Dr94JaBh.woff) format("woff"),url(/_astro/KaTeX_Main-Regular.ypZvNtVU.ttf) format("truetype")}@font-face{font-family:KaTeX_Math;font-style:italic;font-weight:700;src:url(/_astro/KaTeX_Math-BoldItalic.CZnvNsCZ.woff2) format("woff2"),url(/_astro/KaTeX_Math-BoldItalic.iY-2wyZ7.woff) format("woff"),url(/_astro/KaTeX_Math-BoldItalic.B3XSjfu4.ttf) format("truetype")}@font-face{font-family:KaTeX_Math;font-style:italic;font-weight:400;src:url(/_astro/KaTeX_Math-Italic.t53AETM-.woff2) format("woff2"),url(/_astro/KaTeX_Math-Italic.DA0__PXp.woff) format("woff"),url(/_astro/KaTeX_Math-Italic.flOr_0UB.ttf) format("truetype")}@font-face{font-family:KaTeX_SansSerif;font-style:normal;font-weight:700;src:url(/_astro/KaTeX_SansSerif-Bold.D1sUS0GD.woff2) format("woff2"),url(/_astro/KaTeX_SansSerif-Bold.DbIhKOiC.woff) format("woff"),url(/_astro/KaTeX_SansSerif-Bold.CFMepnvq.ttf) format("truetype")}@font-face{font-family:KaTeX_SansSerif;font-style:italic;font-weight:400;src:url(/_astro/KaTeX_SansSerif-Italic.C3H0VqGB.woff2) format("woff2"),url(/_astro/KaTeX_SansSerif-Italic.DN2j7dab.woff) format("woff"),url(/_astro/KaTeX_SansSerif-Italic.YYjJ1zSn.ttf) format("truetype")}@font-face{font-family:KaTeX_SansSerif;font-style:normal;font-weight:400;src:url(/_astro/KaTeX_SansSerif-Regular.DDBCnlJ7.woff2) format("woff2"),url(/_astro/KaTeX_SansSerif-Regular.CS6fqUqJ.woff) format("woff"),url(/_astro/KaTeX_SansSerif-Regular.BNo7hRIc.ttf) format("truetype")}@font-face{font-family:KaTeX_Script;font-style:normal;font-weight:400;src:url(/_astro/KaTeX_Script-Regular.D3wIWfF6.woff2) format("woff2"),url(/_astro/KaTeX_Script-Regular.D5yQViql.woff) format("woff"),url(/_astro/KaTeX_Script-Regular.C5JkGWo-.ttf) format("truetype")}@font-face{font-family:KaTeX_Size1;font-style:normal;font-weight:400;src:url(/_astro/KaTeX_Size1-Regular.mCD8mA8B.woff2) format("woff2"),url(/_astro/KaTeX_Size1-Regular.C195tn64.woff) format("woff"),url(/_astro/KaTeX_Size1-Regular.Dbsnue_I.ttf) format("truetype")}@font-face{font-family:KaTeX_Size2;font-style:normal;font-weight:400;src:url(/_astro/KaTeX_Size2-Regular.Dy4dx90m.woff2) format("woff2"),url(/_astro/KaTeX_Size2-Regular.oD1tc_U0.woff) format("woff"),url(/_astro/KaTeX_Size2-Regular.B7gKUWhC.ttf) format("truetype")}@font-face{font-family:KaTeX_Size3;font-style:normal;font-weight:400;src:url(data:font/woff2;base64,d09GMgABAAAAAA4oAA4AAAAAHbQAAA3TAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAAgRQIDgmcDBEICo1oijYBNgIkA14LMgAEIAWJAAeBHAyBHBvbGiMRdnO0IkRRkiYDgr9KsJ1NUAf2kILNxgUmgqIgq1P89vcbIcmsQbRps3vCcXdYOKSWEPEKgZgQkprQQsxIXUgq0DqpGKmIvrgkeVGtEQD9DzAO29fM9jYhxZEsL2FeURH2JN4MIcTdO049NCVdxQ/w9NrSYFEBKTDKpLKfNkCGDc1RwjZLQcm3vqJ2UW9Xfa3tgAHz6ivp6vgC2yD4/6352ndnN0X0TL7seypkjZlMsjmZnf0Mm5Q+JykRWQBKCVCVPbARPXWyQtb5VgLB6Biq7/Uixcj2WGqdI8tGSgkuRG+t910GKP2D7AQH0DB9FMDW/obJZ8giFI3Wg8Cvevz0M+5m0rTh7XDBlvo9Y4vm13EXmfttwI4mBo1EG15fxJhUiCLbiiyCf/ZA6MFAhg3pGIZGdGIVjtPn6UcMk9A/UUr9PhoNsCENw1APAq0gpH73e+M+0ueyHbabc3vkbcdtzcf/fiy+NxQEjf9ud/ELBHAXJ0nk4z+MXH2Ev/kWyV4k7SkvpPc9Qr38F6RPWnM9cN6DJ0AdD1BhtgABtmoRoFCvPsBAumNm6soZG2Gk5GyVTo2sJncSyp0jQTYoR6WDvTwaaEcHsxHfvuWhHA3a6bN7twRKtcGok6NsCi7jYRrM2jExsUFMxMQYuJbMhuWNOumEJy9hi29Dmg5zMp/A5+hhPG19j1vBrq8JTLr8ki5VLPmG/PynJHVul440bxg5xuymHUFPBshC+nA9I1FmwbRBTNHAcik3Oae0cxKoI3MOriM42UrPe51nsaGxJ+WfXubAsP84aabUlQSJ1IiE0iPETLUU4CATgfXSCSpuRFRmCGbO+wSpAnzaeaCYW1VNEysRtuXCEL1kUFUbbtMv3Tilt/1c11jt3Q5bbMa84cpWipp8Elw3MZhOHsOlwwVUQM3lAR35JiFQbaYCRnMF2lxAWoOg2gyoIV4PouX8HytNIfLhqpJtXB4vjiViUI8IJ7bkC4ikkQvKksnOTKICwnqWSZ9YS5f0WCxmpgjbIq7EJcM4aI2nmhLNY2JIUgOjXZFWBHb+x5oh6cwb0Tv1ackHdKi0I9OO2wE9aogIOn540CCCziyhN+IaejtgAONKznHlHyutPrHGwCx9S6B8kfS4Mfi4Eyv7OU730bT1SCBjt834cXsf43zVjPUqqJjgrjeGnBxSG4aYAKFuVbeCfkDIjAqMb6yLNIbCuvXhMH2/+k2vkNpkORhR59N1CkzoOENvneIosjYmuTxlhUzaGEJQ/iWqx4dmwpmKjrwTiTGTCVozNAYqk/zXOndWxuWSmJkQpJw3pK5KX6QrLt5LATMqpmPAQhkhK6PUjzHUn7E0gHE0kPE0iKkolgkUx9SZmVAdDgpffdyJKg3k7VmzYGCwVXGz/tXmkOIp+vcWs+EMuhhvN0h9uhfzWJziBQmCREGSIFmQIkgVpAnSBRmC//6hkLZwaVhwxlrJSOdqlFtOYxlau9F2QN5Y98xmIAsiM1HVp2VFX+DHHGg6Ecjh3vmqtidX3qHI2qycTk/iwxSt5UzTmEP92ZBnEWTk4Mx8Mpl78ZDokxg/KWb+Q0QkvdKVmq3TMW+RXEgrsziSAfNXFMhDc60N5N9jQzjfO0kBKpUZl0ZmwJ41j/B9Hz6wmRaJB84niNmQrzp9eSlQCDDzazGDdVi3P36VZQ+Jy4f9UBNp+3zTjqI4abaFAm+GShVaXlsGdF3FYzZcDI6cori4kMxUECl9IjJZpzkvitAoxKue+90pDMvcKRxLl53TmOKCmV/xRolNKSqqUxc6LStOETmFOiLZZptlZepcKiAzteG8PEdpnQpbOMNcMsR4RR2Bs0cKFEvSmIjAFcnarqwUL4lDhHmnVkwu1IwshbiCcgvOheZuYyOteufZZwlcTlLgnZ3o/WcYdzZHW/WGaqaVfmTZ1aWCceJjkbZqsfbkOtcFlUZM/jy+hXHDbaUobWqqXaeWobbLO99yG5N3U4wxco0rQGGcOLASFMXeJoham8M+/x6O2WywK2l4HGbq1CoUyC/IZikQhdq3SiuNrvAEj0AVu9x2x3lp/xWzahaxidezFVtdcb5uEnzyl0ZmYiuKI0exvCd4Xc9CV1KB0db00z92wDPde0kukbvZIWN6jUWFTmPIC/Y4UPCm8UfDTFZpZNon1qLFTkBhxzB+FjQRA2Q/YRJT8pQigslMaUpFyAG8TMlXigiqmAZX4xgijKjRlGpLE0GdplRfCaJo0JQaSxNBk6ZmMzcya0FmrcisDdn0Q3HI2sWSppYigmlM1XT/kLQZSNpMJG0WkjYbSZuDpM1F0uYhFc1HxU4m1QJjDK6iL0S5uSj5rgXc3RejEigtcRBtqYPQsiTskmO5vosV+q4VGIKbOkDg0jtRrq+Em1YloaTFar3EGr1EUC8R0kus1Uus00usL97ABr2BjXoDm/QGNhuWtMVBKOwg/i78lT7hBsAvDmwHc/ao3vmUbBmhjeYySZNWvGkfZAgISDSaDo1SVpzGDsAEkF8B+gEapViUoZgUWXcRIGFZNm6gWbAKk0bp0k1MHG9fLYtV4iS2SmLEQFARzRcnf9PUS0LVn05/J9MiRRBU3v2IrvW974v4N00L7ZMk0wXP1409CHo/an8zTRHD3eSJ6m8D4YMkZNl3M79sqeuAsr/m3f+8/yl7A50aiAEJgeBeMWzu7ui9UfUBCe2TIqZIoOd/3/udRBOQidQZUERzb2/VwZN1H/Sju82ew2H2Wfr6qvfVf3hqwDvAIpkQVFy4B9Pe9e4/XvPeceu7h3dvO56iJPf0+A6cqA2ip18ER+iFgggiuOkvj24bby0N9j2UHIkgqIt+sVgfodC4YghLSMjSZbH0VR/6dMDrYJeKHilKTemt6v6kvzvn3/RrdWtr0GoN/xL+Sex/cPYLUpepx9cz/D46UPU5KXgAQa+NDps1v6J3xP1i2HtaDB0M9aX2deA7SYff//+gUCovMmIK/qfsFcOk+4Y5ZN97XlG6zebqtMbKgeRFi51vnxTQYBUik2rS/Cn6PC8ADR8FGxsRPB82dzfND90gIcshOcYUkfjherBz53odpm6TP8txlwOZ71xmfHHOvq053qFF/MRlS3jP0ELudrf2OeN8DHvp6ZceLe8qKYvWz/7yp0u4dKPfli3CYq0O13Ih71mylJ80tOi10On8wi+F4+LWgDPeJ30msSQt9/vkmHq9/Lvo2b461mP801v3W4xTcs6CbvF9UDdrSt+A8OUbpSh55qAUFXWznBBfdeJ8a4d7ugT5tvxUza3h9m4H7ptTqiG4z0g5dc0X29OcGlhpGFMpQo9ytTS+NViZpNdvU4kWx+LKxNY10kQ1yqGXrhe4/1nvP7E+nd5A92TtaRplbHSqoIdOqtRWti+fkB5/n1+/VvCmz12pG1kpQWsfi1ftlBobm0bpngs16CHkbIwdLnParxtTV3QYRlfJ0KFskH7pdN/YDn+yRuSd7sNH3aO0DYPggk6uWuXrfOc+fa3VTxFVvKaNxHsiHmsXyCLIE5yuOeN3/Jdf8HBL/5M6shjyhxHx9BjB1O0+4NLOnjLLSxwO7ukN4jMbOIcD879KLSi6Pk61Oqm2377n8079PXEEQ7cy7OKEC9nbpet118fxweTafpt69x/Bt8UqGzNQt7aelpc44dn5cqhwf71+qKp/Zf/+a0zcizOUWpl/iBcSXip0pplkatCchoH5c5aUM8I7/dWxAej8WicPL1URFZ9BDJelUwEwTkGqUhgSlydVes95YdXvhh9Gfz/aeFWvgVb4tuLbcv4+wLdutVZv/cUonwBD/6eDlE0aSiKK/uoH3+J1wDE/jMVqY2ysGufN84oIXB0sPzy8ollX/LegY74DgJXJR57sn+VGza0x3DnuIgABFM15LmajjjsNlYj+JEZGbuRYcAMOWxFkPN2w6Wd46xo4gVWQR/X4lyI/R6K/YK0110GzudPRW7Y+UOBGTfNNzHeYT0fiH0taunBpq9HEW8OKSaBGj21L0MqenEmNRWBAWDWAk4CpNoEZJ2tTaPFgbQYj8HxtFilErs3BTRwT8uO1NXQaWfIotchmPkAF5mMBAliEmZiOGVgCG9LgRzpscMAOOwowlT3JhusdazXGSC/hxR3UlmWVwWHpOIKheqONvjyhSiTHIkVUco5bnji8m//zL7PKaT1Vl5I6UE609f+gkr6MZKVyKc7zJRmCahLsdlyA5fdQkRSan9LgnnLEyGSkaKJCJog0wAgvepWBt80+1yKln1bMVtCljfNWDueKLsWwaEbBSfSPTEmVRsUcYYMnEjcjeyCZzBXK9E9BYBXLKjOSpUDR+nEV3TFSUdQaz+ot98QxgXwx0GQ+EEUAKB2qZPkQQ0GqFD8UPFMqyaCHM24BZmSGic9EYMagKizOw9Hz50DMrDLrqqLkTAhplMictiCAx5S3BIUQdeJeLnBy2CNtMfz6cV4u8XKoFZQesbf9YZiIERiHjaNodDW6LgcirX/mPnJIkBGDUpTBhSa0EIr38D5hCIszhCM8URGBqImoWjpvpt1ebu/v3Gl3qJfMnNM+9V+kiRFyROTPHQWOcs1dNW94/ukKMPZBvDi55i5CttdeJz84DLngLqjcdwEZ87bFFR8CIG35OAkDVN6VRDZ7aq67NteYqZ2lpT8oYB2CytoBd6VuAx4WgiAsnuj3WohG+LugzXiQRDeM3XYXlULv4dp5VFYC) format("woff2"),url(/_astro/KaTeX_Size3-Regular.CTq5MqoE.woff) format("woff"),url(/_astro/KaTeX_Size3-Regular.DgpXs0kz.ttf) format("truetype")}@font-face{font-family:KaTeX_Size4;font-style:normal;font-weight:400;src:url(/_astro/KaTeX_Size4-Regular.Dl5lxZxV.woff2) format("woff2"),url(/_astro/KaTeX_Size4-Regular.BF-4gkZK.woff) format("woff"),url(/_astro/KaTeX_Size4-Regular.DWFBv043.ttf) format("truetype")}@font-face{font-family:KaTeX_Typewriter;font-style:normal;font-weight:400;src:url(/_astro/KaTeX_Typewriter-Regular.CO6r4hn1.woff2) format("woff2"),url(/_astro/KaTeX_Typewriter-Regular.C0xS9mPB.woff) format("woff"),url(/_astro/KaTeX_Typewriter-Regular.D3Ib7_Hf.ttf) format("truetype")}.katex{font: 1.21em KaTeX_Main,Times New Roman,serif;line-height:1.2;text-indent:0;text-rendering:auto}.katex *{-ms-high-contrast-adjust:none!important;border-color:currentColor}.katex .katex-version:after{content:"0.16.22"}.katex .katex-mathml{clip:rect(1px,1px,1px,1px);border:0;height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.katex .katex-html>.newline{display:block}.katex .base{position:relative;white-space:nowrap;width:-moz-min-content;width:min-content}.katex .base,.katex .strut{display:inline-block}.katex .textbf{font-weight:700}.katex .textit{font-style:italic}.katex .textrm{font-family:KaTeX_Main}.katex .textsf{font-family:KaTeX_SansSerif}.katex .texttt{font-family:KaTeX_Typewriter}.katex .mathnormal{font-family:KaTeX_Math;font-style:italic}.katex .mathit{font-family:KaTeX_Main;font-style:italic}.katex .mathrm{font-style:normal}.katex .mathbf{font-family:KaTeX_Main;font-weight:700}.katex .boldsymbol{font-family:KaTeX_Math;font-style:italic;font-weight:700}.katex .amsrm,.katex .mathbb,.katex .textbb{font-family:KaTeX_AMS}.katex .mathcal{font-family:KaTeX_Caligraphic}.katex .mathfrak,.katex .textfrak{font-family:KaTeX_Fraktur}.katex .mathboldfrak,.katex .textboldfrak{font-family:KaTeX_Fraktur;font-weight:700}.katex .mathtt{font-family:KaTeX_Typewriter}.katex .mathscr,.katex .textscr{font-family:KaTeX_Script}.katex .mathsf,.katex .textsf{font-family:KaTeX_SansSerif}.katex .mathboldsf,.katex .textboldsf{font-family:KaTeX_SansSerif;font-weight:700}.katex .mathitsf,.katex .mathsfit,.katex .textitsf{font-family:KaTeX_SansSerif;font-style:italic}.katex .mainrm{font-family:KaTeX_Main;font-style:normal}.katex .vlist-t{border-collapse:collapse;display:inline-table;table-layout:fixed}.katex .vlist-r{display:table-row}.katex .vlist{display:table-cell;position:relative;vertical-align:bottom}.katex .vlist>span{display:block;height:0;position:relative}.katex .vlist>span>span{display:inline-block}.katex .vlist>span>.pstrut{overflow:hidden;width:0}.katex .vlist-t2{margin-right:-2px}.katex .vlist-s{display:table-cell;font-size:1px;min-width:2px;vertical-align:bottom;width:2px}.katex .vbox{align-items:baseline;display:inline-flex;flex-direction:column}.katex .hbox{width:100%}.katex .hbox,.katex .thinbox{display:inline-flex;flex-direction:row}.katex .thinbox{max-width:0;width:0}.katex .msupsub{text-align:left}.katex .mfrac>span>span{text-align:center}.katex .mfrac .frac-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .hdashline,.katex .hline,.katex .mfrac .frac-line,.katex .overline .overline-line,.katex .rule,.katex .underline .underline-line{min-height:1px}.katex .mspace{display:inline-block}.katex .clap,.katex .llap,.katex .rlap{position:relative;width:0}.katex .clap>.inner,.katex .llap>.inner,.katex .rlap>.inner{position:absolute}.katex .clap>.fix,.katex .llap>.fix,.katex .rlap>.fix{display:inline-block}.katex .llap>.inner{right:0}.katex .clap>.inner,.katex .rlap>.inner{left:0}.katex .clap>.inner>span{margin-left:-50%;margin-right:50%}.katex .rule{border:0 solid;display:inline-block;position:relative}.katex .hline,.katex .overline .overline-line,.katex .underline .underline-line{border-bottom-style:solid;display:inline-block;width:100%}.katex .hdashline{border-bottom-style:dashed;display:inline-block;width:100%}.katex .sqrt>.root{margin-left:.2777777778em;margin-right:-.5555555556em}.katex .fontsize-ensurer.reset-size1.size1,.katex .sizing.reset-size1.size1{font-size:1em}.katex .fontsize-ensurer.reset-size1.size2,.katex .sizing.reset-size1.size2{font-size:1.2em}.katex .fontsize-ensurer.reset-size1.size3,.katex .sizing.reset-size1.size3{font-size:1.4em}.katex .fontsize-ensurer.reset-size1.size4,.katex .sizing.reset-size1.size4{font-size:1.6em}.katex .fontsize-ensurer.reset-size1.size5,.katex .sizing.reset-size1.size5{font-size:1.8em}.katex .fontsize-ensurer.reset-size1.size6,.katex .sizing.reset-size1.size6{font-size:2em}.katex .fontsize-ensurer.reset-size1.size7,.katex .sizing.reset-size1.size7{font-size:2.4em}.katex .fontsize-ensurer.reset-size1.size8,.katex .sizing.reset-size1.size8{font-size:2.88em}.katex .fontsize-ensurer.reset-size1.size9,.katex .sizing.reset-size1.size9{font-size:3.456em}.katex .fontsize-ensurer.reset-size1.size10,.katex .sizing.reset-size1.size10{font-size:4.148em}.katex .fontsize-ensurer.reset-size1.size11,.katex .sizing.reset-size1.size11{font-size:4.976em}.katex .fontsize-ensurer.reset-size2.size1,.katex .sizing.reset-size2.size1{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size2.size2,.katex .sizing.reset-size2.size2{font-size:1em}.katex .fontsize-ensurer.reset-size2.size3,.katex .sizing.reset-size2.size3{font-size:1.1666666667em}.katex .fontsize-ensurer.reset-size2.size4,.katex .sizing.reset-size2.size4{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size2.size5,.katex .sizing.reset-size2.size5{font-size:1.5em}.katex .fontsize-ensurer.reset-size2.size6,.katex .sizing.reset-size2.size6{font-size:1.6666666667em}.katex .fontsize-ensurer.reset-size2.size7,.katex .sizing.reset-size2.size7{font-size:2em}.katex .fontsize-ensurer.reset-size2.size8,.katex .sizing.reset-size2.size8{font-size:2.4em}.katex .fontsize-ensurer.reset-size2.size9,.katex .sizing.reset-size2.size9{font-size:2.88em}.katex .fontsize-ensurer.reset-size2.size10,.katex .sizing.reset-size2.size10{font-size:3.4566666667em}.katex .fontsize-ensurer.reset-size2.size11,.katex .sizing.reset-size2.size11{font-size:4.1466666667em}.katex .fontsize-ensurer.reset-size3.size1,.katex .sizing.reset-size3.size1{font-size:.7142857143em}.katex .fontsize-ensurer.reset-size3.size2,.katex .sizing.reset-size3.size2{font-size:.8571428571em}.katex .fontsize-ensurer.reset-size3.size3,.katex .sizing.reset-size3.size3{font-size:1em}.katex .fontsize-ensurer.reset-size3.size4,.katex .sizing.reset-size3.size4{font-size:1.1428571429em}.katex .fontsize-ensurer.reset-size3.size5,.katex .sizing.reset-size3.size5{font-size:1.2857142857em}.katex .fontsize-ensurer.reset-size3.size6,.katex .sizing.reset-size3.size6{font-size:1.4285714286em}.katex .fontsize-ensurer.reset-size3.size7,.katex .sizing.reset-size3.size7{font-size:1.7142857143em}.katex .fontsize-ensurer.reset-size3.size8,.katex .sizing.reset-size3.size8{font-size:2.0571428571em}.katex .fontsize-ensurer.reset-size3.size9,.katex .sizing.reset-size3.size9{font-size:2.4685714286em}.katex .fontsize-ensurer.reset-size3.size10,.katex .sizing.reset-size3.size10{font-size:2.9628571429em}.katex .fontsize-ensurer.reset-size3.size11,.katex .sizing.reset-size3.size11{font-size:3.5542857143em}.katex .fontsize-ensurer.reset-size4.size1,.katex .sizing.reset-size4.size1{font-size:.625em}.katex .fontsize-ensurer.reset-size4.size2,.katex .sizing.reset-size4.size2{font-size:.75em}.katex .fontsize-ensurer.reset-size4.size3,.katex .sizing.reset-size4.size3{font-size:.875em}.katex .fontsize-ensurer.reset-size4.size4,.katex .sizing.reset-size4.size4{font-size:1em}.katex .fontsize-ensurer.reset-size4.size5,.katex .sizing.reset-size4.size5{font-size:1.125em}.katex .fontsize-ensurer.reset-size4.size6,.katex .sizing.reset-size4.size6{font-size:1.25em}.katex .fontsize-ensurer.reset-size4.size7,.katex .sizing.reset-size4.size7{font-size:1.5em}.katex .fontsize-ensurer.reset-size4.size8,.katex .sizing.reset-size4.size8{font-size:1.8em}.katex .fontsize-ensurer.reset-size4.size9,.katex .sizing.reset-size4.size9{font-size:2.16em}.katex .fontsize-ensurer.reset-size4.size10,.katex .sizing.reset-size4.size10{font-size:2.5925em}.katex .fontsize-ensurer.reset-size4.size11,.katex .sizing.reset-size4.size11{font-size:3.11em}.katex .fontsize-ensurer.reset-size5.size1,.katex .sizing.reset-size5.size1{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size5.size2,.katex .sizing.reset-size5.size2{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size5.size3,.katex .sizing.reset-size5.size3{font-size:.7777777778em}.katex .fontsize-ensurer.reset-size5.size4,.katex .sizing.reset-size5.size4{font-size:.8888888889em}.katex .fontsize-ensurer.reset-size5.size5,.katex .sizing.reset-size5.size5{font-size:1em}.katex .fontsize-ensurer.reset-size5.size6,.katex .sizing.reset-size5.size6{font-size:1.1111111111em}.katex .fontsize-ensurer.reset-size5.size7,.katex .sizing.reset-size5.size7{font-size:1.3333333333em}.katex .fontsize-ensurer.reset-size5.size8,.katex .sizing.reset-size5.size8{font-size:1.6em}.katex .fontsize-ensurer.reset-size5.size9,.katex .sizing.reset-size5.size9{font-size:1.92em}.katex .fontsize-ensurer.reset-size5.size10,.katex .sizing.reset-size5.size10{font-size:2.3044444444em}.katex .fontsize-ensurer.reset-size5.size11,.katex .sizing.reset-size5.size11{font-size:2.7644444444em}.katex .fontsize-ensurer.reset-size6.size1,.katex .sizing.reset-size6.size1{font-size:.5em}.katex .fontsize-ensurer.reset-size6.size2,.katex .sizing.reset-size6.size2{font-size:.6em}.katex .fontsize-ensurer.reset-size6.size3,.katex .sizing.reset-size6.size3{font-size:.7em}.katex .fontsize-ensurer.reset-size6.size4,.katex .sizing.reset-size6.size4{font-size:.8em}.katex .fontsize-ensurer.reset-size6.size5,.katex .sizing.reset-size6.size5{font-size:.9em}.katex .fontsize-ensurer.reset-size6.size6,.katex .sizing.reset-size6.size6{font-size:1em}.katex .fontsize-ensurer.reset-size6.size7,.katex .sizing.reset-size6.size7{font-size:1.2em}.katex .fontsize-ensurer.reset-size6.size8,.katex .sizing.reset-size6.size8{font-size:1.44em}.katex .fontsize-ensurer.reset-size6.size9,.katex .sizing.reset-size6.size9{font-size:1.728em}.katex .fontsize-ensurer.reset-size6.size10,.katex .sizing.reset-size6.size10{font-size:2.074em}.katex .fontsize-ensurer.reset-size6.size11,.katex .sizing.reset-size6.size11{font-size:2.488em}.katex .fontsize-ensurer.reset-size7.size1,.katex .sizing.reset-size7.size1{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size7.size2,.katex .sizing.reset-size7.size2{font-size:.5em}.katex .fontsize-ensurer.reset-size7.size3,.katex .sizing.reset-size7.size3{font-size:.5833333333em}.katex .fontsize-ensurer.reset-size7.size4,.katex .sizing.reset-size7.size4{font-size:.6666666667em}.katex .fontsize-ensurer.reset-size7.size5,.katex .sizing.reset-size7.size5{font-size:.75em}.katex .fontsize-ensurer.reset-size7.size6,.katex .sizing.reset-size7.size6{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size7.size7,.katex .sizing.reset-size7.size7{font-size:1em}.katex .fontsize-ensurer.reset-size7.size8,.katex .sizing.reset-size7.size8{font-size:1.2em}.katex .fontsize-ensurer.reset-size7.size9,.katex .sizing.reset-size7.size9{font-size:1.44em}.katex .fontsize-ensurer.reset-size7.size10,.katex .sizing.reset-size7.size10{font-size:1.7283333333em}.katex .fontsize-ensurer.reset-size7.size11,.katex .sizing.reset-size7.size11{font-size:2.0733333333em}.katex .fontsize-ensurer.reset-size8.size1,.katex .sizing.reset-size8.size1{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size8.size2,.katex .sizing.reset-size8.size2{font-size:.4166666667em}.katex .fontsize-ensurer.reset-size8.size3,.katex .sizing.reset-size8.size3{font-size:.4861111111em}.katex .fontsize-ensurer.reset-size8.size4,.katex .sizing.reset-size8.size4{font-size:.5555555556em}.katex .fontsize-ensurer.reset-size8.size5,.katex .sizing.reset-size8.size5{font-size:.625em}.katex .fontsize-ensurer.reset-size8.size6,.katex .sizing.reset-size8.size6{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size8.size7,.katex .sizing.reset-size8.size7{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size8.size8,.katex .sizing.reset-size8.size8{font-size:1em}.katex .fontsize-ensurer.reset-size8.size9,.katex .sizing.reset-size8.size9{font-size:1.2em}.katex .fontsize-ensurer.reset-size8.size10,.katex .sizing.reset-size8.size10{font-size:1.4402777778em}.katex .fontsize-ensurer.reset-size8.size11,.katex .sizing.reset-size8.size11{font-size:1.7277777778em}.katex .fontsize-ensurer.reset-size9.size1,.katex .sizing.reset-size9.size1{font-size:.2893518519em}.katex .fontsize-ensurer.reset-size9.size2,.katex .sizing.reset-size9.size2{font-size:.3472222222em}.katex .fontsize-ensurer.reset-size9.size3,.katex .sizing.reset-size9.size3{font-size:.4050925926em}.katex .fontsize-ensurer.reset-size9.size4,.katex .sizing.reset-size9.size4{font-size:.462962963em}.katex .fontsize-ensurer.reset-size9.size5,.katex .sizing.reset-size9.size5{font-size:.5208333333em}.katex .fontsize-ensurer.reset-size9.size6,.katex .sizing.reset-size9.size6{font-size:.5787037037em}.katex .fontsize-ensurer.reset-size9.size7,.katex .sizing.reset-size9.size7{font-size:.6944444444em}.katex .fontsize-ensurer.reset-size9.size8,.katex .sizing.reset-size9.size8{font-size:.8333333333em}.katex .fontsize-ensurer.reset-size9.size9,.katex .sizing.reset-size9.size9{font-size:1em}.katex .fontsize-ensurer.reset-size9.size10,.katex .sizing.reset-size9.size10{font-size:1.2002314815em}.katex .fontsize-ensurer.reset-size9.size11,.katex .sizing.reset-size9.size11{font-size:1.4398148148em}.katex .fontsize-ensurer.reset-size10.size1,.katex .sizing.reset-size10.size1{font-size:.2410800386em}.katex .fontsize-ensurer.reset-size10.size2,.katex .sizing.reset-size10.size2{font-size:.2892960463em}.katex .fontsize-ensurer.reset-size10.size3,.katex .sizing.reset-size10.size3{font-size:.337512054em}.katex .fontsize-ensurer.reset-size10.size4,.katex .sizing.reset-size10.size4{font-size:.3857280617em}.katex .fontsize-ensurer.reset-size10.size5,.katex .sizing.reset-size10.size5{font-size:.4339440694em}.katex .fontsize-ensurer.reset-size10.size6,.katex .sizing.reset-size10.size6{font-size:.4821600771em}.katex .fontsize-ensurer.reset-size10.size7,.katex .sizing.reset-size10.size7{font-size:.5785920926em}.katex .fontsize-ensurer.reset-size10.size8,.katex .sizing.reset-size10.size8{font-size:.6943105111em}.katex .fontsize-ensurer.reset-size10.size9,.katex .sizing.reset-size10.size9{font-size:.8331726133em}.katex .fontsize-ensurer.reset-size10.size10,.katex .sizing.reset-size10.size10{font-size:1em}.katex .fontsize-ensurer.reset-size10.size11,.katex .sizing.reset-size10.size11{font-size:1.1996142719em}.katex .fontsize-ensurer.reset-size11.size1,.katex .sizing.reset-size11.size1{font-size:.2009646302em}.katex .fontsize-ensurer.reset-size11.size2,.katex .sizing.reset-size11.size2{font-size:.2411575563em}.katex .fontsize-ensurer.reset-size11.size3,.katex .sizing.reset-size11.size3{font-size:.2813504823em}.katex .fontsize-ensurer.reset-size11.size4,.katex .sizing.reset-size11.size4{font-size:.3215434084em}.katex .fontsize-ensurer.reset-size11.size5,.katex .sizing.reset-size11.size5{font-size:.3617363344em}.katex .fontsize-ensurer.reset-size11.size6,.katex .sizing.reset-size11.size6{font-size:.4019292605em}.katex .fontsize-ensurer.reset-size11.size7,.katex .sizing.reset-size11.size7{font-size:.4823151125em}.katex .fontsize-ensurer.reset-size11.size8,.katex .sizing.reset-size11.size8{font-size:.578778135em}.katex .fontsize-ensurer.reset-size11.size9,.katex .sizing.reset-size11.size9{font-size:.6945337621em}.katex .fontsize-ensurer.reset-size11.size10,.katex .sizing.reset-size11.size10{font-size:.8336012862em}.katex .fontsize-ensurer.reset-size11.size11,.katex .sizing.reset-size11.size11{font-size:1em}.katex .delimsizing.size1{font-family:KaTeX_Size1}.katex .delimsizing.size2{font-family:KaTeX_Size2}.katex .delimsizing.size3{font-family:KaTeX_Size3}.katex .delimsizing.size4{font-family:KaTeX_Size4}.katex .delimsizing.mult .delim-size1>span{font-family:KaTeX_Size1}.katex .delimsizing.mult .delim-size4>span{font-family:KaTeX_Size4}.katex .nulldelimiter{display:inline-block;width:.12em}.katex .delimcenter,.katex .op-symbol{position:relative}.katex .op-symbol.small-op{font-family:KaTeX_Size1}.katex .op-symbol.large-op{font-family:KaTeX_Size2}.katex .accent>.vlist-t,.katex .op-limits>.vlist-t{text-align:center}.katex .accent .accent-body{position:relative}.katex .accent .accent-body:not(.accent-full){width:0}.katex .overlay{display:block}.katex .mtable .vertical-separator{display:inline-block;min-width:1px}.katex .mtable .arraycolsep{display:inline-block}.katex .mtable .col-align-c>.vlist-t{text-align:center}.katex .mtable .col-align-l>.vlist-t{text-align:left}.katex .mtable .col-align-r>.vlist-t{text-align:right}.katex .svg-align{text-align:left}.katex svg{fill:currentColor;stroke:currentColor;fill-rule:nonzero;fill-opacity:1;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;display:block;height:inherit;position:absolute;width:100%}.katex svg path{stroke:none}.katex img{border-style:none;max-height:none;max-width:none;min-height:0;min-width:0}.katex .stretchy{display:block;overflow:hidden;position:relative;width:100%}.katex .stretchy:after,.katex .stretchy:before{content:""}.katex .hide-tail{overflow:hidden;position:relative;width:100%}.katex .halfarrow-left{left:0;overflow:hidden;position:absolute;width:50.2%}.katex .halfarrow-right{overflow:hidden;position:absolute;right:0;width:50.2%}.katex .brace-left{left:0;overflow:hidden;position:absolute;width:25.1%}.katex .brace-center{left:25%;overflow:hidden;position:absolute;width:50%}.katex .brace-right{overflow:hidden;position:absolute;right:0;width:25.1%}.katex .x-arrow-pad{padding:0 .5em}.katex .cd-arrow-pad{padding:0 .55556em 0 .27778em}.katex .mover,.katex .munder,.katex .x-arrow{text-align:center}.katex .boxpad{padding:0 .3em}.katex .fbox,.katex .fcolorbox{border:.04em solid;box-sizing:border-box}.katex .cancel-pad{padding:0 .2em}.katex .cancel-lap{margin-left:-.2em;margin-right:-.2em}.katex .sout{border-bottom-style:solid;border-bottom-width:.08em}.katex .angl{border-right:.049em solid;border-top:.049em solid;box-sizing:border-box;margin-right:.03889em}.katex .anglpad{padding:0 .03889em}.katex .eqn-num:before{content:"(" counter(katexEqnNo) ")";counter-increment:katexEqnNo}.katex .mml-eqn-num:before{content:"(" counter(mmlEqnNo) ")";counter-increment:mmlEqnNo}.katex .mtr-glue{width:50%}.katex .cd-vert-arrow{display:inline-block;position:relative}.katex .cd-label-left{display:inline-block;position:absolute;right:calc(50% + .3em);text-align:left}.katex .cd-label-right{display:inline-block;left:calc(50% + .3em);position:absolute;text-align:right}.katex-display{display:block;margin:1em 0;text-align:center}.katex-display>.katex{display:block;text-align:center;white-space:nowrap}.katex-display>.katex>.katex-html{display:block;position:relative}.katex-display>.katex>.katex-html>.tag{position:absolute;right:0}.katex-display.leqno>.katex>.katex-html>.tag{left:0;right:auto}.katex-display.fleqn>.katex{padding-left:2em;text-align:left}body{counter-reset:katexEqnNo mmlEqnNo}:root{--neutral-600: rgb(107, 114, 128);--neutral-400: rgb(185, 185, 185);--neutral-300: rgb(228, 228, 228);--neutral-200: rgb(245, 245, 245);--default-font-family: Source Sans Pro, ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--primary-base: rgb(222, 144, 202);--primary-color: var(--primary-base);--primary-color-hover: oklch(from var(--primary-color) calc(l - .05) c h);--primary-color-active: oklch(from var(--primary-color) calc(l - .1) c h);--on-primary: #ffffff;--page-bg: #ffffff;--text-color: rgba(0, 0, 0, .85);--transparent-page-contrast: rgba(255, 255, 255, .85);--muted-color: rgba(0, 0, 0, .6);--border-color: rgba(0, 0, 0, .1);--surface-bg: #fafafa;--code-bg: #f6f8fa;--link-underline: var(--primary-color);--link-underline-hover: var(--primary-color-hover);--spacing-1: 8px;--spacing-2: 12px;--spacing-3: 16px;--spacing-4: 24px;--spacing-5: 32px;--spacing-6: 40px;--spacing-7: 48px;--spacing-8: 56px;--spacing-9: 64px;--spacing-10: 72px;--content-padding-x: 16px;--block-spacing-y: var(--spacing-4);--palette-count: 8;--button-radius: 6px;--button-padding-x: 12px;--button-padding-y: 8px;--button-font-size: 14px;--button-icon-padding: 8px;--button-big-padding-x: 16px;--button-big-padding-y: 12px;--button-big-font-size: 16px;--button-big-icon-padding: 12px;--table-border-radius: 8px;--table-header-bg: oklch(from var(--surface-bg) calc(l - .02) c h);--table-row-odd-bg: oklch(from var(--surface-bg) calc(l - .01) c h);--z-base: 0;--z-content: 1;--z-elevated: 10;--z-overlay: 1000;--z-modal: 1100;--z-tooltip: 1200;--axis-color: var(--muted-color);--tick-color: var(--text-color);--grid-color: rgba(0, 0, 0, .08)}[data-theme=dark]{--page-bg: #0f1115;--text-color: rgba(255, 255, 255, .9);--muted-color: rgba(255, 255, 255, .7);--border-color: rgba(255, 255, 255, .15);--surface-bg: #12151b;--code-bg: #12151b;--transparent-page-contrast: rgba(0, 0, 0, .85);--axis-color: var(--muted-color);--tick-color: var(--muted-color);--grid-color: rgba(255, 255, 255, .1);--primary-color-hover: oklch(from var(--primary-color) calc(l - .05) c h);--primary-color-active: oklch(from var(--primary-color) calc(l - .1) c h);--on-primary: #0f1115;--csstools-color-scheme--light: ;color-scheme:dark}html{box-sizing:border-box;background:#fff;background:var(--page-bg);color:#000000d9;color:var(--text-color)}*,*:before,*:after{box-sizing:inherit}body{margin:0;font-family:Source Sans Pro,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-family:var(--default-font-family);background:#fff;background:var(--page-bg);color:#000000d9;color:var(--text-color)}audio{display:block;width:100%}img,picture{max-width:100%;height:auto;display:block;position:relative;z-index:10;z-index:var(--z-elevated)}html{font-size:16px;line-height:1.6}.content-grid main{color:#000000d9;color:var(--text-color)}.content-grid main p{margin:0 0 16px;margin:0 0 var(--spacing-3)}.content-grid main h2{font-weight:600;font-size:max(22px,min(2.6vw,32px));line-height:1.2;margin:72px 0 32px;margin:var(--spacing-10) 0 var(--spacing-5);padding-bottom:12px;padding-bottom:var(--spacing-2);border-bottom:1px solid rgba(0,0,0,.1);border-bottom:1px solid var(--border-color)}.content-grid main h3{font-weight:700;font-size:max(18px,min(2.1vw,22px));line-height:1.25;margin:56px 0 24px;margin:var(--spacing-8) 0 var(--spacing-4)}.content-grid main h4{font-weight:600;text-transform:uppercase;font-size:14px;line-height:1.2;margin:56px 0 24px;margin:var(--spacing-8) 0 var(--spacing-4)}.content-grid main a{color:#de90ca;color:var(--primary-color);-webkit-text-decoration:none;text-decoration:none;background:var(--sufrace-bg);border-bottom:1px solid rgba(222,144,202,.4)}@supports (color: color-mix(in lch,red,blue)){.content-grid main a{border-bottom:1px solid color-mix(in srgb,var(--primary-color, #007AFF) 40%,transparent)}}.content-grid main a:hover{color:#ce80ba;color:var(--primary-color-hover);border-bottom:1px solid rgba(222,144,202,.4)}@supports (color: color-mix(in lch,red,blue)){.content-grid main a:hover{border-bottom:1px solid color-mix(in srgb,var(--primary-color, #007AFF) 40%,transparent)}}.content-grid main h2 a,.content-grid main h3 a,.content-grid main h4 a,.content-grid main h5 a,.content-grid main h6 a{color:inherit;border-bottom:none;-webkit-text-decoration:none;text-decoration:none}.content-grid main h2 a:hover,.content-grid main h3 a:hover,.content-grid main h4 a:hover,.content-grid main h5 a:hover,.content-grid main h6 a:hover{color:inherit;border-bottom:none;-webkit-text-decoration:none;text-decoration:none}.content-grid main ul,.content-grid main ol{padding-left:24px;margin:0 0 16px;margin:0 0 var(--spacing-3)}.content-grid main li{margin-bottom:12px;margin-bottom:var(--spacing-2)}.content-grid main li:last-child{margin-bottom:0}.content-grid main blockquote{border-left:2px solid rgba(0,0,0,.1);border-left:2px solid var(--border-color);padding-left:24px;padding-left:var(--spacing-4);font-style:italic;color:#0009;color:var(--muted-color);margin:24px 0;margin:var(--spacing-4) 0}.muted{color:#0009;color:var(--muted-color)}[data-footnote-ref]{margin-left:4px}.content-grid main mark{background-color:#de90ca08;border:1px solid rgba(222,144,202,.05);color:inherit;padding:1px 3px;border-radius:2px;font-weight:500;box-decoration-break:clone;-webkit-box-decoration-break:clone}@supports (color: color-mix(in lch,red,blue)){.content-grid main mark{background-color:color-mix(in srgb,var(--primary-color, #007AFF) 3%,transparent);border:1px solid color-mix(in srgb,var(--primary-color) 5%,transparent)}}.feature-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));grid-gap:12px;gap:12px;margin:46px 0}.feature-card{display:flex;flex-direction:column;padding:16px;border:1px solid rgba(222,144,202,.4);background:#de90ca0d!important;border-radius:8px;-webkit-text-decoration:none;text-decoration:none;color:inherit;transition:all .2s ease}@supports (color: color-mix(in lch,red,blue)){.feature-card{border:1px solid color-mix(in srgb,var(--primary-color) 40%,transparent);background:color-mix(in srgb,var(--primary-color, #007AFF) 05%,transparent)!important}}.feature-card:hover{transform:translateY(-2px);box-shadow:0 2px 8px #00000014}.feature-card strong{font-size:14px;font-weight:600;color:#000000d9;color:var(--text-color);color:#de90ca!important;color:var(--primary-color)!important;margin-bottom:0!important}.feature-card span{font-size:12px;color:#0009;color:var(--muted-color);color:#de90ca!important;color:var(--primary-color)!important;margin-bottom:0!important;opacity:1}.katex .tag{background:none;border:none;opacity:.4}.content-grid{max-width:1280px;margin:40px auto 0;padding:0 16px;padding:0 var(--content-padding-x);display:grid;grid-template-columns:260px minmax(0,680px) 260px;grid-gap:32px;gap:32px;align-items:start}.content-grid>main{max-width:100%;margin:0;padding:0}.content-grid>main>*:first-child{margin-top:0}@media (max-width: 1100px){.content-grid{overflow:hidden;display:block;margin-top:12px;margin-top:var(--spacing-2)}.content-grid{grid-template-columns:1fr}.table-of-contents{position:static;display:none}.table-of-contents-mobile{display:block}.footer-inner{grid-template-columns:1fr;gap:16px}.footer-inner>h3{grid-column:auto;margin-top:16px}.footer-inner{display:block;padding:40px 16px}}.wide,.full-width{box-sizing:border-box;position:relative;z-index:10;z-index:var(--z-elevated);background-color:var(--background-color)}.wide{width:min(1100px,100vw - 16px * 2);width:min(1100px,100vw - var(--content-padding-x) * 2);margin-left:50%;transform:translate(-50%);padding:16px;padding:var(--content-padding-x);border-radius:6px;border-radius:var(--button-radius);background-color:#fff;background-color:var(--page-bg)}.full-width{width:100vw;margin-left:calc(50% - 50vw);margin-right:calc(50% - 50vw)}@media (max-width: 1100px){.wide,.full-width{width:100%;margin-left:0;margin-right:0;padding:0;transform:none}}#theme-toggle{position:fixed;top:24px;top:calc(var(--spacing-4) + var(--hf-spaces-topbar, 0px));right:16px;right:var(--spacing-3);margin:0;z-index:1000;z-index:var(--z-overlay)}@media (max-width: 640px){header.meta .meta-container{display:flex;flex-wrap:wrap;row-gap:12px;-moz-column-gap:8px;column-gap:8px;max-width:100%;padding:0 24px;padding:0 var(--spacing-4)}header.meta .meta-container .meta-container-cell{flex:1 1 calc(50% - 8px);min-width:0}}@media (max-width: 320px){header.meta .meta-container .meta-container-cell{flex-basis:100%;text-align:center}header.meta .affiliations{list-style-position:inside;padding-left:0;margin-left:0}header.meta .affiliations li{text-align:center}}@media (max-width: 768px){.d3-neural .panel{flex-direction:column}.d3-neural .panel .left{flex:0 0 auto;width:100%}.d3-neural .panel .right{flex:0 0 auto;width:100%;min-width:0}}@media print{html,body{background:#fff}body{margin:0}#theme-toggle{display:none!important}.content-grid main a{-webkit-text-decoration:none;text-decoration:none;border-bottom:1px solid rgba(0,0,0,.2)}.content-grid main pre,.content-grid main blockquote,.content-grid main table,.content-grid main figure{-moz-column-break-inside:avoid;break-inside:avoid;page-break-inside:avoid}.content-grid main h2{page-break-before:auto;page-break-after:avoid;-moz-column-break-after:avoid;break-after:avoid-page}.code-lang-chip{display:none!important}:root{--border-color: rgba(0,0,0,.2);--link-underline: rgba(0,0,0,.3);--link-underline-hover: rgba(0,0,0,.4)}.content-grid{grid-template-columns:1fr!important}.table-of-contents,.right-aside,.table-of-contents-mobile{display:none!important}main>nav:first-of-type{display:none!important}.hero,.hero-banner,.d3-banner,.d3-banner svg,.html-embed__card,.js-plotly-plot,figure,pre,table,blockquote,.wide,.full-width{-moz-column-break-inside:avoid;break-inside:avoid;page-break-inside:avoid}.hero{page-break-after:avoid}}@media print{.meta-container-cell--pdf{display:none!important}}code{font-size:14px;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;background-color:#f6f8fa;background-color:var(--code-bg);border-radius:.3em;border:1px solid rgba(0,0,0,.1);border:1px solid var(--border-color);color:#000000d9;color:var(--text-color);font-weight:400;line-height:1.5}p code,.note code{white-space:nowrap;padding:calc(8px/3) 4px;padding:calc(var(--spacing-1)/3) calc(var(--spacing-1)/2)}.astro-code{position:relative;border:1px solid rgba(0,0,0,.1);border:1px solid var(--border-color);border-radius:6px;padding:0;font-size:14px;--code-gutter-width: 2.5em}.astro-code,section.content-grid pre{width:100%;max-width:100%;box-sizing:border-box;-webkit-overflow-scrolling:touch;padding:0;margin-bottom:24px!important;margin-bottom:var(--block-spacing-y)!important;overflow-x:auto}section.content-grid pre.astro-code{margin:0;padding:8px 0;padding:var(--spacing-1) 0}section.content-grid pre code{display:inline-block;min-width:100%}@media (max-width: 1100px){.astro-code,section.content-grid pre{white-space:pre-wrap;word-wrap:anywhere;word-break:break-word}section.content-grid pre code{white-space:pre-wrap;display:block;min-width:0}}[data-theme=light] .astro-code{background-color:#f6f8fa;background-color:var(--code-bg)}[data-theme=light] .astro-code span{color:var(--shiki-light)!important}[data-theme=dark] .astro-code span{color:var(--shiki-dark)!important}[data-theme=light] .astro-code{--shiki-foreground: #24292f;--shiki-background: #ffffff}.astro-code code{counter-reset:astro-code-line;display:block;background:none;border:none}.astro-code .line{display:inline-block;position:relative;padding-left:calc(var(--code-gutter-width) + 8px);padding-left:calc(var(--code-gutter-width) + var(--spacing-1));min-height:1.25em}.astro-code .line:before{counter-increment:astro-code-line;content:counter(astro-code-line);position:absolute;left:0;top:0;bottom:0;width:calc(var(--code-gutter-width));text-align:right;color:#0009;color:var(--muted-color);opacity:.3;-webkit-user-select:none;-moz-user-select:none;user-select:none;padding-right:12px;padding-right:var(--spacing-2);border-right:1px solid rgba(0,0,0,.1);border-right:1px solid var(--border-color)}.astro-code .line:empty:after{content:" "}.astro-code code>.line:last-child:empty{display:none}.code-card{position:relative}.code-card .code-copy{position:absolute;top:12px;top:var(--spacing-2);right:12px;right:var(--spacing-2);z-index:3;display:none}.code-card:hover .code-copy{display:block}.code-card .code-copy svg{width:16px;height:16px;display:block;fill:currentColor}.code-card pre{margin:0 0 8px;margin:0 0 var(--spacing-1)}.code-card.no-copy:after{top:8px;right:8px}.accordion .astro-code{padding:0;border:none}.accordion .astro-code{margin-bottom:0!important}.accordion .code-output{border:none;border-top:1px solid rgba(0,0,0,.1)!important;border-top:1px solid var(--border-color)!important}.accordion pre{margin-bottom:0!important}.accordion .code-card pre{margin:0!important}.accordion .astro-code:after{right:0;bottom:0}.code-output{position:relative;background:#f4f6f8;border:1px solid rgba(0,0,0,.1);border:1px solid var(--border-color);border-radius:6px;margin-top:0;margin-bottom:24px;margin-bottom:var(--block-spacing-y);padding:0!important}@supports (color: lab(from red l 1 1% / calc(alpha + .1))){.code-output{background:oklch(from var(--code-bg) calc(l - .005) c h)}}.code-output pre{padding:22px 16px 16px!important;padding:calc(var(--spacing-3) + 6px) var(--spacing-3) var(--spacing-3) var(--spacing-3)!important}.code-card+.code-output,.astro-code+.code-output,section.content-grid pre+.code-output{margin-top:0;border-top:none;border-top-left-radius:0;border-top-right-radius:0;box-shadow:inset 0 8px 12px -12px #00000026}.astro-code:has(+.code-output){margin-bottom:0!important}.code-card:has(+.code-output) .astro-code{margin-bottom:0!important}section.content-grid pre:has(+.code-output){margin-bottom:0!important}.astro-code:has(+.code-output){border-bottom-left-radius:0;border-bottom-right-radius:0}.code-card:has(+.code-output) .astro-code{border-bottom-left-radius:0;border-bottom-right-radius:0}section.content-grid pre:has(+.code-output){border-bottom-left-radius:0;border-bottom-right-radius:0}.code-output:before{content:"Output";position:absolute;top:0;right:0;font-size:10px;line-height:1;color:#0009;color:var(--muted-color);text-transform:uppercase;letter-spacing:.04em;border-top:none;border-right:none;border-radius:0 0 0 6px;padding:10px}.code-output>:where(*):first-child{margin-top:0!important}.code-output>:where(*):last-child{margin-bottom:0!important}.code-filename{display:inline-block;font-size:12px;line-height:1;color:#0009;color:var(--muted-color);background:#fafafa;background:var(--surface-bg);border:1px solid rgba(0,0,0,.1);border:1px solid var(--border-color);border-bottom:none;border-radius:6px 6px 0 0;padding:4px 8px;margin:0}.code-filename+.code-card .astro-code,.code-filename+.astro-code,.code-filename+section.content-grid pre{border-top-left-radius:0;border-top-right-radius:6px}button,.button{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:linear-gradient(15deg,#de90ca,#ce80ba 35%);background:linear-gradient(15deg,var(--primary-color) 0%,var(--primary-color-hover) 35%);color:#fff;border:1px solid transparent;border-radius:6px;border-radius:var(--button-radius);padding:8px 12px;padding:var(--button-padding-y) var(--button-padding-x);font-size:14px;font-size:var(--button-font-size);line-height:1;cursor:pointer;display:inline-block;-webkit-text-decoration:none;text-decoration:none;transition:background-color .15s ease,border-color .15s ease,box-shadow .15s ease,transform .02s ease}button:has(>svg:only-child),.button:has(>svg:only-child){padding:8px;padding:var(--button-icon-padding)}button:hover,.button:hover{filter:brightness(96%)}button:active,.button:active{transform:translateY(1px)}button:focus-visible,.button:focus-visible{outline:none}button:disabled,.button:disabled{opacity:.6;cursor:not-allowed}.button--ghost{background:transparent!important;color:#de90ca!important;color:var(--primary-color)!important;border-color:#de90ca!important;border-color:var(--primary-color)!important}.button--ghost:hover{color:#ce80ba!important;color:var(--primary-color-hover)!important;border-color:#ce80ba!important;border-color:var(--primary-color-hover)!important;filter:none}.button.button--big{padding:12px 16px;padding:var(--button-big-padding-y) var(--button-big-padding-x);font-size:16px;font-size:var(--button-big-font-size)}.button.button--big:has(>svg:only-child){padding:12px;padding:var(--button-big-icon-padding)}.button-group .button{margin:5px}.content-grid main table{border-collapse:collapse;table-layout:auto;margin:0}.content-grid main th,.content-grid main td{border-bottom:1px solid rgba(0,0,0,.1);border-bottom:1px solid var(--border-color);padding:6px 8px;font-size:15px;white-space:nowrap;word-break:auto-phrase;white-space:break-spaces;vertical-align:top}.content-grid main thead th{border-bottom:1px solid rgba(0,0,0,.1);border-bottom:1px solid var(--border-color)}.content-grid main thead th{background:#f3f3f3;background:var(--table-header-bg);padding-top:10px;padding-bottom:10px;font-weight:600}.content-grid main hr{border:none;border-bottom:1px solid rgba(0,0,0,.1);border-bottom:1px solid var(--border-color);margin:32px 0;margin:var(--spacing-5) 0}.content-grid main .table-scroll{width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;border:1px solid rgba(0,0,0,.1);border:1px solid var(--border-color);border-radius:8px;border-radius:var(--table-border-radius);background:#fafafa;background:var(--surface-bg);margin:0 0 24px;margin:0 0 var(--block-spacing-y)}.content-grid main .table-scroll>table{width:-moz-fit-content;width:fit-content;min-width:100%;max-width:none}.content-grid main .table-scroll>table th,.content-grid main .table-scroll>table td{border-right:1px solid rgba(0,0,0,.1);border-right:1px solid var(--border-color)}.content-grid main .table-scroll>table th:last-child,.content-grid main .table-scroll>table td:last-child{border-right:none}.content-grid main .table-scroll>table thead th:first-child{border-top-left-radius:8px;border-top-left-radius:var(--table-border-radius)}.content-grid main .table-scroll>table thead th:last-child{border-top-right-radius:8px;border-top-right-radius:var(--table-border-radius)}.content-grid main .table-scroll>table tbody tr:last-child td:first-child{border-bottom-left-radius:8px;border-bottom-left-radius:var(--table-border-radius)}.content-grid main .table-scroll>table tbody tr:last-child td:last-child{border-bottom-right-radius:8px;border-bottom-right-radius:var(--table-border-radius)}.content-grid main .table-scroll>table tbody tr:nth-child(odd) td{background:#f7f7f7;background:var(--table-row-odd-bg)}.content-grid main .table-scroll>table tbody tr:last-child td{border-bottom:none}.accordion .accordion__content .table-scroll{border:none;border-radius:0;margin:0;margin-bottom:0!important}.accordion .accordion__content table{margin:0!important}.accordion .accordion__content .table-scroll>table thead th:first-child,.accordion .accordion__content .table-scroll>table thead th:last-child,.accordion .accordion__content .table-scroll>table tbody tr:last-child td:first-child,.accordion .accordion__content .table-scroll>table tbody tr:last-child td:last-child{border-radius:0}@supports not ((width: -moz-fit-content) or (width: fit-content)){.content-grid main .table-scroll>table{width:-moz-max-content;width:max-content;min-width:100%}}.tag-list{display:flex;flex-wrap:wrap;gap:8px;margin:8px 0 16px}.tag{display:inline-flex;align-items:center;gap:6px;padding:8px 12px;font-size:12px;line-height:1;border-radius:6px;border-radius:var(--button-radius);background:#fafafa;background:var(--surface-bg);border:1px solid rgba(0,0,0,.1);border:1px solid var(--border-color);color:#000000d9;color:var(--text-color)}.card{background:#fafafa;background:var(--surface-bg);border:1px solid rgba(0,0,0,.1);border:1px solid var(--border-color);border-radius:10px;padding:12px;padding:var(--spacing-2);z-index:11;z-index:calc(var(--z-elevated) + 1);position:relative;margin-bottom:24px;margin-bottom:var(--block-spacing-y)}select{background-color:#fff;background-color:var(--page-bg);border:1px solid rgba(202,131,183,.55);border-radius:6px;border-radius:var(--button-radius);padding:8px 12px;padding:var(--button-padding-y) var(--button-padding-x) var(--button-padding-y) var(--button-padding-x);font-family:Source Sans Pro,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-family:var(--default-font-family);font-size:14px;font-size:var(--button-font-size);color:#000000d9;color:var(--text-color);background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8.825L1.175 4 2.35 2.825 6 6.475 9.65 2.825 10.825 4z'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 14px center;background-position:right calc(var(--button-padding-x) + 2px) center;background-size:12px;cursor:pointer;transition:border-color .2s ease,box-shadow .2s ease;-webkit-appearance:none;-moz-appearance:none;appearance:none}@supports (color: color-mix(in lch,red,blue)){select{border:1px solid color-mix(in srgb,var(--primary-color) 50%,var(--border-color))}}select:hover,select:focus,select:active{border-color:#de90ca;border-color:var(--primary-color)}select:focus{outline:none;border-color:#de90ca;border-color:var(--primary-color);box-shadow:0 0 0 2px #de90ca1a}@supports (color: lab(from red l 1 1% / calc(alpha + .1))){select:focus{box-shadow:0 0 0 2px rgba(from var(--primary-color) r g b / .1)}}select:disabled{opacity:.6;cursor:not-allowed;background-color:#fafafa;background-color:var(--surface-bg)}[data-theme=dark] select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23bbb' d='M6 8.825L1.175 4 2.35 2.825 6 6.475 9.65 2.825 10.825 4z'/%3E%3C/svg%3E")}input[type=checkbox]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:16px;height:16px;border:2px solid rgba(0,0,0,.1);border:2px solid var(--border-color);border-radius:3px;background-color:#fff;background-color:var(--page-bg);cursor:pointer;position:relative;transition:all .2s ease;margin-right:12px;margin-right:var(--spacing-2)}input[type=checkbox]:hover{border-color:#de90ca;border-color:var(--primary-color)}input[type=checkbox]:focus{outline:none;border-color:#de90ca;border-color:var(--primary-color);box-shadow:0 0 0 2px #de90ca1a}@supports (color: lab(from red l 1 1% / calc(alpha + .1))){input[type=checkbox]:focus{box-shadow:0 0 0 2px rgba(from var(--primary-color) r g b / .1)}}input[type=checkbox]:checked{background-color:#de90ca;background-color:var(--primary-color);border-color:#de90ca;border-color:var(--primary-color)}input[type=checkbox]:checked:before{content:"";position:absolute;top:1px;left:4px;width:4px;height:8px;border:solid #ffffff;border:solid var(--on-primary);border-width:0 2px 2px 0;transform:rotate(45deg)}input[type=checkbox]:disabled{opacity:.6;cursor:not-allowed}input[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:16px;height:16px;border:2px solid rgba(0,0,0,.1);border:2px solid var(--border-color);border-radius:50%;background-color:#fff;background-color:var(--page-bg);cursor:pointer;position:relative;transition:all .2s ease;margin-right:12px;margin-right:var(--spacing-2)}input[type=radio]:hover{border-color:#de90ca;border-color:var(--primary-color)}input[type=radio]:focus{outline:none;border-color:#de90ca;border-color:var(--primary-color);box-shadow:0 0 0 2px #de90ca1a}@supports (color: lab(from red l 1 1% / calc(alpha + .1))){input[type=radio]:focus{box-shadow:0 0 0 2px rgba(from var(--primary-color) r g b / .1)}}input[type=radio]:checked{border-color:#de90ca;border-color:var(--primary-color)}input[type=radio]:checked:before{content:"";position:absolute;top:2px;left:2px;width:8px;height:8px;border-radius:50%;background-color:#de90ca;background-color:var(--primary-color)}input[type=radio]:disabled{opacity:.6;cursor:not-allowed}input[type=text],input[type=email],input[type=password],input[type=number],input[type=url],input[type=search],textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-color:var(--page-bg);border:1px solid rgba(0,0,0,.1);border:1px solid var(--border-color);border-radius:6px;border-radius:var(--button-radius);padding:8px 12px;padding:var(--button-padding-y) var(--button-padding-x);font-family:Source Sans Pro,ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-family:var(--default-font-family);font-size:14px;font-size:var(--button-font-size);color:#000000d9;color:var(--text-color);transition:border-color .2s ease,box-shadow .2s ease;width:100%}input[type=text]:hover,input[type=email]:hover,input[type=password]:hover,input[type=number]:hover,input[type=url]:hover,input[type=search]:hover,textarea:hover{border-color:#de90ca;border-color:var(--primary-color)}input[type=text]:focus,input[type=email]:focus,input[type=password]:focus,input[type=number]:focus,input[type=url]:focus,input[type=search]:focus,textarea:focus{outline:none;border-color:#de90ca;border-color:var(--primary-color);box-shadow:0 0 0 2px #de90ca1a}@supports (color: lab(from red l 1 1% / calc(alpha + .1))){input[type=text]:focus,input[type=email]:focus,input[type=password]:focus,input[type=number]:focus,input[type=url]:focus,input[type=search]:focus,textarea:focus{box-shadow:0 0 0 2px rgba(from var(--primary-color) r g b / .1)}}input[type=text]:disabled,input[type=email]:disabled,input[type=password]:disabled,input[type=number]:disabled,input[type=url]:disabled,input[type=search]:disabled,textarea:disabled{opacity:.6;cursor:not-allowed;background-color:#fafafa;background-color:var(--surface-bg)}label{display:flex;align-items:center;font-size:14px;font-size:var(--button-font-size);color:#000000d9;color:var(--text-color);cursor:pointer;margin-bottom:0;line-height:1.4;-webkit-user-select:none;-moz-user-select:none;user-select:none}.form-group{margin-bottom:24px;margin-bottom:var(--spacing-4);display:flex;align-items:center;gap:12px;gap:var(--spacing-2)}.form-group label{margin-bottom:0}.form-group.vertical{flex-direction:column;align-items:flex-start}.form-group.vertical label{margin-bottom:8px;margin-bottom:var(--spacing-1)}.form-inline{display:flex;align-items:center;gap:12px;gap:var(--spacing-2);margin-bottom:16px;margin-bottom:var(--spacing-3)}.form-inline label{margin-bottom:0}div[style*="display: flex"] label,div[class*=flex] label,.trackio-controls label,.scale-controls label,.theme-selector label{margin-bottom:0!important;align-self:center}.tenet-list{margin:3rem 0}.tenet-list ol{counter-reset:tenet-counter 0;list-style:none;padding-left:0;display:grid;grid-template-columns:1fr;grid-gap:2.5rem;gap:2.5rem;max-width:900px;margin:0 auto}.tenet-list li.tenet{counter-increment:tenet-counter;background:linear-gradient(135deg,#fff,#f8f9fa);border:2px solid #e2e8f0;border-radius:16px;padding:2rem 2rem 2rem 4rem;margin:0;position:relative;box-shadow:0 12px 35px #0000001f;transition:all .3s ease;cursor:pointer}.tenet-list li.tenet:hover{transform:translateY(-8px) scale(1.02);box-shadow:0 20px 50px #00000040;border-color:#007bff80;background:linear-gradient(135deg,#fff,#f0f8ff)}.tenet-list li.tenet:nth-child(1):before{background:linear-gradient(135deg,#667eea,#764ba2)}.tenet-list li.tenet:nth-child(2):before{background:linear-gradient(135deg,#f093fb,#f5576c)}.tenet-list li.tenet:nth-child(3):before{background:linear-gradient(135deg,#4facfe,#00f2fe)}.tenet-list li.tenet:nth-child(4):before{background:linear-gradient(135deg,#43e97b,#38f9d7)}.tenet-list li.tenet:nth-child(5):before{background:linear-gradient(135deg,#fa709a,#fee140)}.tenet-list li.tenet:nth-child(6):before{background:linear-gradient(135deg,#a8edea,#fed6e3)}.tenet-list li.tenet:nth-child(7):before{background:linear-gradient(135deg,#ff9a9e,#fecfef)}.tenet-list li.tenet:nth-child(8):before{background:linear-gradient(135deg,#a18cd1,#fbc2eb)}.tenet-list li.tenet:nth-child(9):before{background:linear-gradient(135deg,#ffecd2,#fcb69f)}.tenet-list li.tenet:before{content:counter(tenet-counter);position:absolute;top:-12px;left:-12px;color:#fff;width:48px;height:48px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.2em;font-weight:700;box-shadow:0 4px 12px #00000026;border:3px solid white}.tenet-list li.tenet strong{color:#1a202c;font-size:1.1em;display:block;margin-bottom:.5rem}.tenet-list li.tenet em{color:#4a5568;font-size:.95em;font-style:italic;display:block;margin-top:.75rem;padding:1rem;background:#00000008;border-radius:8px;border-left:3px solid #e2e8f0}.tenet-list li.tenet p{color:#2d3748;line-height:1.6;margin:.5rem 0}@keyframes pulse-glow{0%{box-shadow:0 4px 12px #00000026}50%{box-shadow:0 4px 20px #00000040}to{box-shadow:0 4px 12px #00000026}}.tenet-list li.tenet:hover:before{animation:pulse-glow 2s ease-in-out infinite}[data-theme=dark] .tenet-list li.tenet{background:linear-gradient(135deg,#1a202c,#2d3748);border-color:#4a5568}[data-theme=dark] .tenet-list li.tenet:hover{background:linear-gradient(135deg,#2d3748,#374151);border-color:#667eea80}[data-theme=dark] .tenet-list li.tenet strong{color:#e2e8f0}[data-theme=dark] .tenet-list li.tenet p{color:#cbd5e0}[data-theme=dark] .tenet-list li.tenet em{color:#a0aec0;background:#ffffff0d;border-left-color:#4a5568}@media (max-width: 768px){.tenet-list li.tenet{padding:1.5rem}}.crumbs{background:linear-gradient(135deg,#f0f4ff,#e6eeff);border-left:5px solid #667eea;padding:1.25rem 1.75rem;margin:2.5rem 0;border-radius:0 8px 8px 0;box-shadow:0 2px 8px #667eea1f;font-size:.95em;line-height:1.6;color:#4a5568}.crumbs strong{color:#667eea;font-weight:700}.crumbs code{background:#667eea1a;padding:.15em .4em;border-radius:3px;font-size:.9em;color:#4c51bf}.crumbs a{color:#667eea;font-weight:500}[data-theme=dark] .crumbs{background:linear-gradient(135deg,#1e293b,#334155);border-left-color:#818cf8;color:#cbd5e0}[data-theme=dark] .crumbs strong{color:#a5b4fc}[data-theme=dark] .crumbs code{background:#818cf833;color:#c7d2fe}[data-theme=dark] .crumbs a{color:#a5b4fc}main a[href^="http://"],main a[href^="https://"]{background:linear-gradient(135deg,#e3f2fd,#bbdefb);color:#1565c0;-webkit-text-decoration:none;text-decoration:none;padding:.15em .5em;border-radius:12px;border:1px solid #90caf9;display:inline-block;transition:all .3s ease;font-weight:500;box-shadow:0 1px 3px #1565c026}main a[href^="http://"]:hover,main a[href^="https://"]:hover{background:linear-gradient(135deg,#2196f3,#1976d2);color:#fff;border-color:#1565c0;transform:translateY(-1px);box-shadow:0 4px 12px #1565c04d}main a[href^="http://"]:active,main a[href^="https://"]:active{transform:translateY(0);box-shadow:0 1px 3px #1565c033}a[href^="#source-of-truth"],a[href^="#one-model-one-file"],a[href^="#code-is-product"],a[href^="#standardize-dont-abstract"],a[href^="#do-repeat-yourself"],a[href^="#minimal-user-api"],a[href^="#backwards-compatibility"],a[href^="#consistent-public-surface"],a[href^="#modular"]{position:relative;color:#667eea;font-weight:600;-webkit-text-decoration:underline;text-decoration:underline;text-decoration-color:#667eea4d;transition:all .3s ease}a[href^="#source-of-truth"]:hover,a[href^="#one-model-one-file"]:hover,a[href^="#code-is-product"]:hover,a[href^="#standardize-dont-abstract"]:hover,a[href^="#do-repeat-yourself"]:hover,a[href^="#minimal-user-api"]:hover,a[href^="#backwards-compatibility"]:hover,a[href^="#consistent-public-surface"]:hover,a[href^="#modular"]:hover{color:#4c51bf;text-decoration-color:#4c51bf;background:#667eea1a;padding:2px 4px;border-radius:4px}a[href^="#source-of-truth"]:after{content:"Model implementations should be reliable, reproducible, and faithful to original performances."}a[href^="#one-model-one-file"]:after{content:"All inference and training core logic visible, top‑to‑bottom, in a single file."}a[href^="#code-is-product"]:after{content:"Optimize for reading, diffing, and tweaking. Code quality matters as much as functionality."}a[href^="#standardize-dont-abstract"]:after{content:"Model-specific logic belongs in the model file, not hidden behind abstractions."}a[href^="#do-repeat-yourself"]:after{content:"Strategic duplication can improve readability and maintainability when done thoughtfully."}a[href^="#minimal-user-api"]:after{content:"Config, model, preprocessing; from_pretrained, save_pretrained, push_to_hub. Least amount of codepaths."}a[href^="#backwards-compatibility"]:after{content:"Any artifact once on the hub must remain loadable. Breaking changes are unacceptable."}a[href^="#consistent-public-surface"]:after{content:"Uniform naming, signatures, and conventions across all models for predictability."}a[href^="#modular"]:after{content:"Architecture components shared via modular system, removing boilerplate while keeping expanded files visible."}a[href^="#source-of-truth"]:after,a[href^="#one-model-one-file"]:after,a[href^="#code-is-product"]:after,a[href^="#standardize-dont-abstract"]:after,a[href^="#do-repeat-yourself"]:after,a[href^="#minimal-user-api"]:after,a[href^="#backwards-compatibility"]:after,a[href^="#consistent-public-surface"]:after,a[href^="#modular"]:after{position:absolute;bottom:100%;left:50%;transform:translate(-50%);background:#1a202c;color:#fff;padding:.75rem 1rem;border-radius:8px;font-size:.85em;font-weight:400;white-space:normal;width:300px;line-height:1.4;z-index:1001;opacity:0;visibility:hidden;transition:opacity .3s ease,visibility .3s ease;pointer-events:none;box-shadow:0 4px 12px #0003;margin-bottom:.5rem}a[href^="#source-of-truth"]:hover:after,a[href^="#one-model-one-file"]:hover:after,a[href^="#code-is-product"]:hover:after,a[href^="#standardize-dont-abstract"]:hover:after,a[href^="#do-repeat-yourself"]:hover:after,a[href^="#minimal-user-api"]:hover:after,a[href^="#backwards-compatibility"]:hover:after,a[href^="#consistent-public-surface"]:hover:after,a[href^="#modular"]:hover:after{opacity:1;visibility:visible}[data-theme=dark] main a[href^="http://"],[data-theme=dark] main a[href^="https://"]{background:linear-gradient(135deg,#1e3a5f,#2563eb);color:#bfdbfe;border-color:#3b82f6}[data-theme=dark] main a[href^="http://"]:hover,[data-theme=dark] main a[href^="https://"]:hover{background:linear-gradient(135deg,#2563eb,#1d4ed8);color:#fff;border-color:#60a5fa}[data-theme=dark] a[href^="#source-of-truth"]:after,[data-theme=dark] a[href^="#one-model-one-file"]:after,[data-theme=dark] a[href^="#code-is-product"]:after,[data-theme=dark] a[href^="#standardize-dont-abstract"]:after,[data-theme=dark] a[href^="#do-repeat-yourself"]:after,[data-theme=dark] a[href^="#minimal-user-api"]:after,[data-theme=dark] a[href^="#backwards-compatibility"]:after,[data-theme=dark] a[href^="#consistent-public-surface"]:after,[data-theme=dark] a[href^="#modular"]:after{background:#2d3748;color:#e2e8f0}[data-theme=dark] a[href^="#source-of-truth"],[data-theme=dark] a[href^="#one-model-one-file"],[data-theme=dark] a[href^="#code-is-product"],[data-theme=dark] a[href^="#standardize-dont-abstract"],[data-theme=dark] a[href^="#do-repeat-yourself"],[data-theme=dark] a[href^="#minimal-user-api"],[data-theme=dark] a[href^="#backwards-compatibility"],[data-theme=dark] a[href^="#consistent-public-surface"],[data-theme=dark] a[href^="#modular"]{color:#a5b4fc;text-decoration-color:#a5b4fc4d}[data-theme=dark] a[href^="#source-of-truth"]:hover,[data-theme=dark] a[href^="#one-model-one-file"]:hover,[data-theme=dark] a[href^="#code-is-product"]:hover,[data-theme=dark] a[href^="#standardize-dont-abstract"]:hover,[data-theme=dark] a[href^="#do-repeat-yourself"]:hover,[data-theme=dark] a[href^="#minimal-user-api"]:hover,[data-theme=dark] a[href^="#backwards-compatibility"]:hover,[data-theme=dark] a[href^="#consistent-public-surface"]:hover,[data-theme=dark] a[href^="#modular"]:hover{color:#c7d2fe;background:#a5b4fc26}.demo-wide,.demo-full-width{display:flex;flex-direction:column;align-items:center;justify-content:center;width:100%;min-height:150px;color:#0009;color:var(--muted-color);font-size:12px;border:2px dashed rgba(0,0,0,.1);border:2px dashed var(--border-color);border-radius:8px;background:#fafafa;background:var(--surface-bg);margin-bottom:24px;margin-bottom:var(--block-spacing-y)}.mermaid{background:none!important;margin-bottom:24px!important;margin-bottom:var(--block-spacing-y)!important}.content-grid main img{max-width:100%;height:auto;width:min(1100px,100vw - 16px * 2);width:min(1100px,100vw - var(--content-padding-x) * 2);margin-left:50%;transform:translate(-50%);display:block}.content-grid main .figure-legend{text-align:center;font-size:.9rem;color:#0009;color:var(--muted-color);font-style:italic;margin:12px 0 24px;margin:var(--spacing-2) 0 var(--spacing-4);width:min(1100px,100vw - 16px * 2);width:min(1100px,100vw - var(--content-padding-x) * 2);margin-left:50%;transform:translate(-50%)}@media (max-width: 1024px){.content-grid main img,.content-grid main .figure-legend{width:100%;margin-left:0;transform:none}}
 
 
app/dist/index.html CHANGED
The diff for this file is too large to render. See raw diff
 
app/dist/index.html.gz CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:51df88386a12e2ccc402f63334931f4d00f472c98202d9f54dd49be895a5a0a9
3
- size 1490581
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:37ea57936a220876b21506e54a0fd9b034c0a8f35cd65c3a062de0f2d65a576d
3
+ size 63568
app/package.json CHANGED
@@ -50,7 +50,7 @@
50
  "devDependencies": {
51
  "@astrojs/mdx": "^3.1.9",
52
  "@astrojs/svelte": "^5.5.0",
53
- "astro": "^4.16.19",
54
  "astro-compressor": "^0.4.1",
55
  "astro-mermaid": "^1.0.4",
56
  "mermaid": "^11.10.1",
 
50
  "devDependencies": {
51
  "@astrojs/mdx": "^3.1.9",
52
  "@astrojs/svelte": "^5.5.0",
53
+ "astro": "^4.10.0",
54
  "astro-compressor": "^0.4.1",
55
  "astro-mermaid": "^1.0.4",
56
  "mermaid": "^11.10.1",
app/public/data ADDED
@@ -0,0 +1 @@
 
 
1
+ ../src/content/assets/data
app/public/image/Bloatedness_visualizer copy.png ADDED

Git LFS Details

  • SHA256: ea224af69bd6c008c670b4db7049c4c29e6447309a2353e48978756895e8f0be
  • Pointer size: 131 Bytes
  • Size of remote file: 474 kB
app/public/image/Bloatedness_visualizer.png ADDED

Git LFS Details

  • SHA256: ea224af69bd6c008c670b4db7049c4c29e6447309a2353e48978756895e8f0be
  • Pointer size: 131 Bytes
  • Size of remote file: 474 kB
app/public/image/Jaccard_similarity_plot.png ADDED

Git LFS Details

  • SHA256: 04e01af0724c6146e8d57f495c67dbaf87fe843c959a13eabe9fd0d1e56907e2
  • Pointer size: 131 Bytes
  • Size of remote file: 130 kB
app/public/image/big_picture_zoomout.png ADDED

Git LFS Details

  • SHA256: 6b48173ad33c50e9b1b7f674bb21948da982db04e4a927cf0cecee45bc749297
  • Pointer size: 131 Bytes
  • Size of remote file: 218 kB
app/public/image/classic_encoders.png ADDED

Git LFS Details

  • SHA256: fd9a7c4300b8fcfdc8fe0aebe6f84f0131efcab8c8928783388e6c54148c4a68
  • Pointer size: 131 Bytes
  • Size of remote file: 532 kB
app/{dist/_astro/index.BzKj3Iki.css.gz → public/image/cluster_wave2vec2.png} RENAMED
File without changes
app/public/image/detr_island.png ADDED

Git LFS Details

  • SHA256: 6f6daf8ce4f8e71a0a9b3c60f2a7a18aacf1812a54337cf345b9005eaa251125
  • Pointer size: 130 Bytes
  • Size of remote file: 18.5 kB
app/public/image/fast_image_processors copy.png ADDED

Git LFS Details

  • SHA256: 9e981efa2635c9b7dfef9c03e9e7233316d87f057d71bae7429b9830ca065827
  • Pointer size: 131 Bytes
  • Size of remote file: 407 kB
app/public/image/fast_image_processors.png ADDED

Git LFS Details

  • SHA256: 9e981efa2635c9b7dfef9c03e9e7233316d87f057d71bae7429b9830ca065827
  • Pointer size: 131 Bytes
  • Size of remote file: 407 kB
app/public/image/graph_modular_related_models.png ADDED

Git LFS Details

  • SHA256: 5faa98d5cf7555a74882b881c30bfe50737395e538c9bfed7c32082574d76eb5
  • Pointer size: 131 Bytes
  • Size of remote file: 461 kB
app/public/image/hf-logo.svg ADDED
app/public/image/llama_center.png ADDED

Git LFS Details

  • SHA256: 3ec2caa493f919717ece1366836e156d8d05a3bf09ef4313ea502d5130a82cb0
  • Pointer size: 131 Bytes
  • Size of remote file: 406 kB
app/public/image/llama_glm_attn.png ADDED

Git LFS Details

  • SHA256: 6b2c88d5eb3d461d791e7e280f74e54d05f01babb02ea0536b50386b7b1b1b8a
  • Pointer size: 131 Bytes
  • Size of remote file: 138 kB
app/public/image/model_debugger copy.png ADDED

Git LFS Details

  • SHA256: 6f32cc4f604170d045a2bace165fa4b1ebd5d8fc09bc827b6f1ff9fd1558f8aa
  • Pointer size: 131 Bytes
  • Size of remote file: 548 kB
app/public/image/model_debugger.png ADDED

Git LFS Details

  • SHA256: 6f32cc4f604170d045a2bace165fa4b1ebd5d8fc09bc827b6f1ff9fd1558f8aa
  • Pointer size: 131 Bytes
  • Size of remote file: 548 kB
app/public/image/modular_candidates.png ADDED

Git LFS Details

  • SHA256: 5964930f30790fa0f89ad19c31767c5b29d10b12bca8a75d4a83a92d53c5deab
  • Pointer size: 131 Bytes
  • Size of remote file: 741 kB
app/public/image/popular_models_barplot.png ADDED

Git LFS Details

  • SHA256: c9bc4e2c3588f23232e4de1161ca03c5a5197c49d74be99ff5ab7d412fa512f0
  • Pointer size: 131 Bytes
  • Size of remote file: 166 kB
app/public/image/still_graph_bloat.png ADDED

Git LFS Details

  • SHA256: 4356f948ccea807ae828488bbf7793383f90d7dfaf5d2fa47a323d3c0accfe4c
  • Pointer size: 131 Bytes
  • Size of remote file: 663 kB
app/public/image/timeline_llava.png ADDED

Git LFS Details

  • SHA256: 2bd0469fa24737bf309c1225005b62d7f0a9d722df0e56a18c578a1327cf94fc
  • Pointer size: 131 Bytes
  • Size of remote file: 621 kB
app/public/scripts/color-palettes.js ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Global color palettes generator and watcher
2
+ // - Observes CSS variable --primary-color and theme changes
3
+ // - Generates categorical, sequential, and diverging palettes (OKLCH/OKLab)
4
+ // - Exposes results as CSS variables on :root
5
+ // - Supports variable color counts per palette via CSS vars
6
+ // - Dispatches a 'palettes:updated' CustomEvent after each update
7
+
8
+ (() => {
9
+ const MODE = { cssRoot: document.documentElement };
10
+
11
+ const getCssVar = (name) => {
12
+ try { return getComputedStyle(MODE.cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
13
+ };
14
+ const getIntFromCssVar = (name, fallback) => {
15
+ const raw = getCssVar(name);
16
+ if (!raw) return fallback;
17
+ const v = parseInt(String(raw), 10);
18
+ if (Number.isNaN(v)) return fallback;
19
+ return v;
20
+ };
21
+ const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
22
+
23
+ // Color math (OKLab/OKLCH)
24
+ const srgbToLinear = (u) => (u <= 0.04045 ? u / 12.92 : Math.pow((u + 0.055) / 1.055, 2.4));
25
+ const linearToSrgb = (u) => (u <= 0.0031308 ? 12.92 * u : 1.055 * Math.pow(Math.max(0, u), 1 / 2.4) - 0.055);
26
+ const rgbToOklab = (r, g, b) => {
27
+ const rl = srgbToLinear(r), gl = srgbToLinear(g), bl = srgbToLinear(b);
28
+ const l = Math.cbrt(0.4122214708 * rl + 0.5363325363 * gl + 0.0514459929 * bl);
29
+ const m = Math.cbrt(0.2119034982 * rl + 0.6806995451 * gl + 0.1073969566 * bl);
30
+ const s = Math.cbrt(0.0883024619 * rl + 0.2817188376 * gl + 0.6299787005 * bl);
31
+ const L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
32
+ const a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
33
+ const b2 = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
34
+ return { L, a, b: b2 };
35
+ };
36
+ const oklabToRgb = (L, a, b) => {
37
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
38
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
39
+ const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
40
+ const l = l_ * l_ * l_;
41
+ const m = m_ * m_ * m_;
42
+ const s = s_ * s_ * s_;
43
+ const r = linearToSrgb(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s);
44
+ const g = linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s);
45
+ const b3 = linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s);
46
+ return { r, g, b: b3 };
47
+ };
48
+ const oklchToOklab = (L, C, hDeg) => { const h = (hDeg * Math.PI) / 180; return { L, a: C * Math.cos(h), b: C * Math.sin(h) }; };
49
+ 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 }; };
50
+ const clamp01 = (x) => Math.min(1, Math.max(0, x));
51
+ const isInGamut = ({ r, g, b }) => r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;
52
+ const toHex = ({ r, g, b }) => {
53
+ const R = Math.round(clamp01(r) * 255), G = Math.round(clamp01(g) * 255), B = Math.round(clamp01(b) * 255);
54
+ const h = (n) => n.toString(16).padStart(2, '0');
55
+ return `#${h(R)}${h(G)}${h(B)}`.toUpperCase();
56
+ };
57
+ 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)); };
58
+ 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; } };
59
+
60
+ // Get primary color in OKLCH format to preserve precision
61
+ const getPrimaryOKLCH = () => {
62
+ const css = getCssVar('--primary-color');
63
+ if (!css) return null;
64
+
65
+ // For OKLCH colors, return the exact values without conversion
66
+ if (css.includes('oklch')) {
67
+ const oklchMatch = css.match(/oklch\(([^)]+)\)/);
68
+ if (oklchMatch) {
69
+ const values = oklchMatch[1].split(/\s+/).map(v => parseFloat(v.trim()));
70
+ if (values.length >= 3) {
71
+ const [L, C, h] = values;
72
+ return { L, C, h };
73
+ }
74
+ }
75
+ }
76
+
77
+ // For non-OKLCH colors, convert to OKLCH for consistency
78
+ const rgb = parseCssColorToRgb(css);
79
+ if (rgb) {
80
+ const { L, a, b } = rgbToOklab(rgb.r, rgb.g, rgb.b);
81
+ const { C, h } = oklabToOklch(L, a, b);
82
+ return { L, C, h };
83
+ }
84
+ return null;
85
+ };
86
+
87
+ // Keep getPrimaryHex for backward compatibility, but now it converts from OKLCH
88
+ const getPrimaryHex = () => {
89
+ const oklch = getPrimaryOKLCH();
90
+ if (!oklch) return null;
91
+
92
+ const { a, b } = oklchToOklab(oklch.L, oklch.C, oklch.h);
93
+ const rgb = oklabToRgb(oklch.L, a, b);
94
+ return toHex(rgb);
95
+ };
96
+ // No count management via CSS anymore; counts are passed directly to the API
97
+
98
+ const generators = {
99
+ categorical: (baseOKLCH, count) => {
100
+ const { L, C, h } = baseOKLCH;
101
+ const L0 = Math.min(0.85, Math.max(0.4, L));
102
+ const C0 = Math.min(0.35, Math.max(0.1, C || 0.2));
103
+ const total = Math.max(1, Math.min(12, count || 8));
104
+ const hueStep = 360 / total;
105
+ const results = [];
106
+ for (let i = 0; i < total; i++) {
107
+ const hDeg = (h + i * hueStep) % 360;
108
+ const lVar = ((i % 3) - 1) * 0.04;
109
+ results.push(oklchToHexSafe(Math.max(0.4, Math.min(0.85, L0 + lVar)), C0, hDeg));
110
+ }
111
+ return results;
112
+ },
113
+ sequential: (baseOKLCH, count) => {
114
+ const { L, C, h } = baseOKLCH;
115
+ const total = Math.max(1, Math.min(12, count || 8));
116
+ const startL = Math.max(0.25, L - 0.18);
117
+ const endL = Math.min(0.92, L + 0.18);
118
+ const cBase = Math.min(0.33, Math.max(0.08, C * 0.9 + 0.06));
119
+ const out = [];
120
+ for (let i = 0; i < total; i++) {
121
+ const t = total === 1 ? 0 : i / (total - 1);
122
+ const lNow = startL * (1 - t) + endL * t;
123
+ const cNow = cBase * (0.85 + 0.15 * (1 - Math.abs(0.5 - t) * 2));
124
+ out.push(oklchToHexSafe(lNow, cNow, h));
125
+ }
126
+ return out;
127
+ },
128
+ diverging: (baseOKLCH, count) => {
129
+ const { L, C, h } = baseOKLCH;
130
+ const total = Math.max(1, Math.min(12, count || 8));
131
+
132
+ // Left endpoint: EXACT primary color (no darkening)
133
+ const leftLab = oklchToOklab(L, C, h);
134
+ // Right endpoint: complement with same L and similar C (clamped safe)
135
+ const compH = (h + 180) % 360;
136
+ const cSafe = Math.min(0.35, Math.max(0.08, C));
137
+ const rightLab = oklchToOklab(L, cSafe, compH);
138
+ const whiteLab = { L: 0.98, a: 0, b: 0 }; // center near‑white
139
+
140
+ const hexFromOKLab = (L, a, b) => toHex(oklabToRgb(L, a, b));
141
+ const lerp = (a, b, t) => a + (b - a) * t;
142
+ const lerpOKLabHex = (A, B, t) => hexFromOKLab(lerp(A.L, B.L, t), lerp(A.a, B.a, t), lerp(A.b, B.b, t));
143
+
144
+ const out = [];
145
+ if (total % 2 === 1) {
146
+ const nSide = (total - 1) >> 1; // items on each side
147
+ // Left side: include left endpoint exactly at index 0
148
+ for (let i = 0; i < nSide; i++) {
149
+ const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
150
+ // Move from leftLab to a value close (but not equal) to white; ensure last before center is lighter
151
+ const tt = t * 0.9; // keep some distance from pure white before center
152
+ out.push(lerpOKLabHex(leftLab, whiteLab, tt));
153
+ }
154
+ // Center
155
+ out.push(hexFromOKLab(whiteLab.L, whiteLab.a, whiteLab.b));
156
+ // Right side: start near white and end EXACTLY at rightLab
157
+ for (let i = 0; i < nSide; i++) {
158
+ const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
159
+ const tt = Math.max(0.1, t); // avoid starting at pure white
160
+ out.push(lerpOKLabHex(whiteLab, rightLab, tt));
161
+ }
162
+ // Ensure first and last are exact endpoints
163
+ if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
164
+ } else {
165
+ const nSide = total >> 1;
166
+ // Left half including left endpoint, approaching white but not reaching it
167
+ for (let i = 0; i < nSide; i++) {
168
+ const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
169
+ const tt = t * 0.9;
170
+ out.push(lerpOKLabHex(leftLab, whiteLab, tt));
171
+ }
172
+ // Right half: mirror from near white to exact right endpoint
173
+ for (let i = 0; i < nSide; i++) {
174
+ const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
175
+ const tt = Math.max(0.1, t);
176
+ out.push(lerpOKLabHex(whiteLab, rightLab, tt));
177
+ }
178
+ if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
179
+ }
180
+ return out;
181
+ }
182
+ };
183
+
184
+ let lastSignature = '';
185
+
186
+ const updatePalettes = () => {
187
+ const primaryOKLCH = getPrimaryOKLCH();
188
+ const primaryHex = getPrimaryHex();
189
+ const signature = `${primaryOKLCH?.L},${primaryOKLCH?.C},${primaryOKLCH?.h}`;
190
+ if (signature === lastSignature) return;
191
+ lastSignature = signature;
192
+ try { document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { }
193
+ };
194
+
195
+ const bootstrap = () => {
196
+ // Initial setup - only run once on page load
197
+ updatePalettes();
198
+
199
+ // Observer will handle all subsequent changes
200
+ const mo = new MutationObserver(() => updatePalettes());
201
+ mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] });
202
+
203
+ // Utility: choose high-contrast (or softened) text style against an arbitrary background color
204
+ const pickTextStyleForBackground = (bgCss, opts = {}) => {
205
+ const cssRoot = document.documentElement;
206
+ const getCssVar = (name) => {
207
+ try { return getComputedStyle(cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
208
+ };
209
+ const resolveCssToRgb01 = (css) => {
210
+ const rgb = parseCssColorToRgb(css);
211
+ if (!rgb) return null;
212
+ return rgb; // already 0..1
213
+ };
214
+ 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 });
215
+ const relLum = (rgb) => {
216
+ const f = (u) => srgbToLinear(u);
217
+ return 0.2126 * f(rgb.r) + 0.7152 * f(rgb.g) + 0.0722 * f(rgb.b);
218
+ };
219
+ const contrast = (fg, bg) => {
220
+ const L1 = relLum(fg), L2 = relLum(bg); const a = Math.max(L1, L2), b = Math.min(L1, L2);
221
+ return (a + 0.05) / (b + 0.05);
222
+ };
223
+ try {
224
+ const bg = resolveCssToRgb01(bgCss);
225
+ if (!bg) return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
226
+ const candidatesCss = [getCssVar('--text-color') || '#111', getCssVar('--on-primary') || '#0f1115', '#000', '#fff'];
227
+ const candidates = candidatesCss
228
+ .map(css => ({ css, rgb: resolveCssToRgb01(css) }))
229
+ .filter(x => !!x.rgb);
230
+ // Pick the max contrast
231
+ let best = candidates[0]; let bestCR = contrast(best.rgb, bg);
232
+ for (let i = 1; i < candidates.length; i++) {
233
+ const cr = contrast(candidates[i].rgb, bg);
234
+ if (cr > bestCR) { best = candidates[i]; bestCR = cr; }
235
+ }
236
+ // Optional softening via blend factor (0..1), blending towards muted color
237
+ const blend = Math.min(1, Math.max(0, Number(opts.blend || 0)));
238
+ let finalRgb = best.rgb;
239
+ if (blend > 0) {
240
+ const mutedCss = getCssVar('--muted-color') || (getCssVar('--text-color') || '#111');
241
+ const mutedRgb = resolveCssToRgb01(mutedCss) || best.rgb;
242
+ finalRgb = mixRgb01(best.rgb, mutedRgb, blend);
243
+ }
244
+ const haloStrength = Math.min(1, Math.max(0, Number(opts.haloStrength == null ? 0.5 : opts.haloStrength)));
245
+ 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})`;
246
+ return { fill: toHex(finalRgb), stroke, strokeWidth: (opts.haloWidth == null ? 1 : Number(opts.haloWidth)) };
247
+ } catch {
248
+ return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
249
+ }
250
+ };
251
+ window.ColorPalettes = {
252
+ refresh: updatePalettes,
253
+ notify: () => { try { const primaryOKLCH = getPrimaryOKLCH(); const primaryHex = getPrimaryHex(); document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { } },
254
+ getPrimary: () => getPrimaryHex(),
255
+ getPrimaryOKLCH: () => getPrimaryOKLCH(),
256
+ getColors: (key, count = 6) => {
257
+ const primaryOKLCH = getPrimaryOKLCH();
258
+ if (!primaryOKLCH) return [];
259
+ const total = Math.max(1, Math.min(12, Number(count) || 6));
260
+ if (key === 'categorical') return generators.categorical(primaryOKLCH, total);
261
+ if (key === 'sequential') return generators.sequential(primaryOKLCH, total);
262
+ if (key === 'diverging') return generators.diverging(primaryOKLCH, total);
263
+ return [];
264
+ },
265
+ getTextStyleForBackground: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {}),
266
+ chooseReadableText: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {})
267
+ };
268
+ };
269
+
270
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
271
+ else bootstrap();
272
+ })();
273
+
274
+
app/scripts/export-latex.mjs ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { promises as fs } from 'node:fs';
4
+ import { resolve, dirname, basename, extname } from 'node:path';
5
+ import process from 'node:process';
6
+
7
+ async function run(command, args = [], options = {}) {
8
+ return new Promise((resolvePromise, reject) => {
9
+ const child = spawn(command, args, { stdio: 'inherit', shell: false, ...options });
10
+ child.on('error', reject);
11
+ child.on('exit', (code) => {
12
+ if (code === 0) resolvePromise(undefined);
13
+ else reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
14
+ });
15
+ });
16
+ }
17
+
18
+ function parseArgs(argv) {
19
+ const out = {};
20
+ for (const arg of argv.slice(2)) {
21
+ if (!arg.startsWith('--')) continue;
22
+ const [k, v] = arg.replace(/^--/, '').split('=');
23
+ out[k] = v === undefined ? true : v;
24
+ }
25
+ return out;
26
+ }
27
+
28
+ function slugify(text) {
29
+ return String(text || '')
30
+ .normalize('NFKD')
31
+ .replace(/\p{Diacritic}+/gu, '')
32
+ .toLowerCase()
33
+ .replace(/[^a-z0-9]+/g, '-')
34
+ .replace(/^-+|-+$/g, '')
35
+ .slice(0, 120) || 'article';
36
+ }
37
+
38
+ async function checkPandocInstalled() {
39
+ try {
40
+ await run('pandoc', ['--version'], { stdio: 'pipe' });
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ async function readMdxFile(filePath) {
48
+ try {
49
+ const content = await fs.readFile(filePath, 'utf-8');
50
+ return content;
51
+ } catch (error) {
52
+ console.warn(`Warning: Could not read ${filePath}:`, error.message);
53
+ return '';
54
+ }
55
+ }
56
+
57
+ function extractFrontmatter(content) {
58
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
59
+ if (!frontmatterMatch) return { frontmatter: {}, content };
60
+
61
+ const frontmatterText = frontmatterMatch[1];
62
+ const contentWithoutFrontmatter = content.replace(frontmatterMatch[0], '');
63
+
64
+ // Simple YAML parsing for basic fields
65
+ const frontmatter = {};
66
+ const lines = frontmatterText.split('\n');
67
+ let currentKey = null;
68
+ let currentValue = '';
69
+
70
+ for (const line of lines) {
71
+ const trimmed = line.trim();
72
+ if (trimmed.includes(':') && !trimmed.startsWith('-')) {
73
+ if (currentKey) {
74
+ frontmatter[currentKey] = currentValue.trim();
75
+ }
76
+ const [key, ...valueParts] = trimmed.split(':');
77
+ currentKey = key.trim();
78
+ currentValue = valueParts.join(':').trim();
79
+ } else if (currentKey) {
80
+ currentValue += '\n' + trimmed;
81
+ }
82
+ }
83
+
84
+ if (currentKey) {
85
+ frontmatter[currentKey] = currentValue.trim();
86
+ }
87
+
88
+ return { frontmatter, content: contentWithoutFrontmatter };
89
+ }
90
+
91
+ function cleanMdxToMarkdown(content) {
92
+ // Remove import statements
93
+ content = content.replace(/^import .+?;?\s*$/gm, '');
94
+
95
+ // Remove JSX component calls like <ComponentName />
96
+ content = content.replace(/<[A-Z][a-zA-Z0-9]*\s*\/>/g, '');
97
+
98
+ // Convert JSX components to simpler markdown
99
+ // Handle Sidenote components specially
100
+ content = content.replace(/<Sidenote>([\s\S]*?)<\/Sidenote>/g, (match, innerContent) => {
101
+ // Extract main content and aside content
102
+ const asideMatch = innerContent.match(/<Fragment slot="aside">([\s\S]*?)<\/Fragment>/);
103
+ const mainContent = innerContent.replace(/<Fragment slot="aside">[\s\S]*?<\/Fragment>/, '').trim();
104
+ const asideContent = asideMatch ? asideMatch[1].trim() : '';
105
+
106
+ let result = mainContent;
107
+ if (asideContent) {
108
+ result += `\n\n> **Note:** ${asideContent}`;
109
+ }
110
+ return result;
111
+ });
112
+
113
+ // Handle Note components
114
+ content = content.replace(/<Note[^>]*>([\s\S]*?)<\/Note>/g, (match, innerContent) => {
115
+ return `\n> **Note:** ${innerContent.trim()}\n`;
116
+ });
117
+
118
+ // Handle Wide and FullWidth components
119
+ content = content.replace(/<(Wide|FullWidth)>([\s\S]*?)<\/\1>/g, '$2');
120
+
121
+ // Handle HtmlEmbed components (convert to simple text)
122
+ content = content.replace(/<HtmlEmbed[^>]*\/>/g, '*[Interactive content not available in LaTeX]*');
123
+
124
+ // Remove remaining JSX fragments
125
+ content = content.replace(/<Fragment[^>]*>([\s\S]*?)<\/Fragment>/g, '$1');
126
+ content = content.replace(/<[A-Z][a-zA-Z0-9]*[^>]*>([\s\S]*?)<\/[A-Z][a-zA-Z0-9]*>/g, '$1');
127
+
128
+ // Clean up className attributes
129
+ content = content.replace(/className="[^"]*"/g, '');
130
+
131
+ // Clean up extra whitespace
132
+ content = content.replace(/\n{3,}/g, '\n\n');
133
+
134
+ return content.trim();
135
+ }
136
+
137
+ async function processChapterImports(content, contentDir) {
138
+ let processedContent = content;
139
+
140
+ // First, extract all import statements and their corresponding component calls
141
+ const importPattern = /import\s+(\w+)\s+from\s+["']\.\/chapters\/([^"']+)["'];?/g;
142
+ const imports = new Map();
143
+ let match;
144
+
145
+ // Collect all imports
146
+ while ((match = importPattern.exec(content)) !== null) {
147
+ const [fullImport, componentName, chapterPath] = match;
148
+ imports.set(componentName, { path: chapterPath, importStatement: fullImport });
149
+ }
150
+
151
+ // Remove all import statements
152
+ processedContent = processedContent.replace(importPattern, '');
153
+
154
+ // Process each component call
155
+ for (const [componentName, { path: chapterPath }] of imports) {
156
+ const componentCallPattern = new RegExp(`<${componentName}\\s*\\/>`, 'g');
157
+
158
+ try {
159
+ const chapterFile = resolve(contentDir, 'chapters', chapterPath);
160
+ const chapterContent = await readMdxFile(chapterFile);
161
+ const { content: chapterMarkdown } = extractFrontmatter(chapterContent);
162
+ const cleanChapter = cleanMdxToMarkdown(chapterMarkdown);
163
+
164
+ processedContent = processedContent.replace(componentCallPattern, cleanChapter);
165
+ console.log(`✅ Processed chapter: ${chapterPath}`);
166
+ } catch (error) {
167
+ console.warn(`Warning: Could not process chapter ${chapterPath}:`, error.message);
168
+ processedContent = processedContent.replace(componentCallPattern, `\n*[Chapter ${chapterPath} could not be loaded]*\n`);
169
+ }
170
+ }
171
+
172
+ return processedContent;
173
+ }
174
+
175
+ function createLatexPreamble(frontmatter) {
176
+ const title = frontmatter.title ? frontmatter.title.replace(/\n/g, ' ') : 'Untitled Article';
177
+ const subtitle = frontmatter.subtitle || '';
178
+ const authors = frontmatter.authors || '';
179
+ const date = frontmatter.published || '';
180
+
181
+ return `\\documentclass[11pt,a4paper]{article}
182
+ \\usepackage[utf8]{inputenc}
183
+ \\usepackage[T1]{fontenc}
184
+ \\usepackage{amsmath,amsfonts,amssymb}
185
+ \\usepackage{graphicx}
186
+ \\usepackage{hyperref}
187
+ \\usepackage{booktabs}
188
+ \\usepackage{longtable}
189
+ \\usepackage{array}
190
+ \\usepackage{multirow}
191
+ \\usepackage{wrapfig}
192
+ \\usepackage{float}
193
+ \\usepackage{colortbl}
194
+ \\usepackage{pdflscape}
195
+ \\usepackage{tabu}
196
+ \\usepackage{threeparttable}
197
+ \\usepackage{threeparttablex}
198
+ \\usepackage{ulem}
199
+ \\usepackage{makecell}
200
+ \\usepackage{xcolor}
201
+ \\usepackage{listings}
202
+ \\usepackage{fancyvrb}
203
+ \\usepackage{geometry}
204
+ \\geometry{margin=1in}
205
+
206
+ \\title{${title}${subtitle ? `\\\\\\large ${subtitle}` : ''}}
207
+ ${authors ? `\\author{${authors}}` : ''}
208
+ ${date ? `\\date{${date}}` : ''}
209
+
210
+ \\begin{document}
211
+ \\maketitle
212
+ \\tableofcontents
213
+ \\newpage
214
+
215
+ `;
216
+ }
217
+
218
+ async function main() {
219
+ const cwd = process.cwd();
220
+ const args = parseArgs(process.argv);
221
+
222
+ // Check if pandoc is installed
223
+ const hasPandoc = await checkPandocInstalled();
224
+ if (!hasPandoc) {
225
+ console.error('❌ Pandoc is not installed. Please install it first:');
226
+ console.error(' macOS: brew install pandoc');
227
+ console.error(' Ubuntu: apt-get install pandoc');
228
+ console.error(' Windows: choco install pandoc');
229
+ process.exit(1);
230
+ }
231
+
232
+ const contentDir = resolve(cwd, 'src/content');
233
+ const articleFile = resolve(contentDir, 'article.mdx');
234
+
235
+ // Check if article.mdx exists
236
+ try {
237
+ await fs.access(articleFile);
238
+ } catch {
239
+ console.error(`❌ Could not find article.mdx at ${articleFile}`);
240
+ process.exit(1);
241
+ }
242
+
243
+ console.log('> Reading article content...');
244
+ const articleContent = await readMdxFile(articleFile);
245
+ const { frontmatter, content } = extractFrontmatter(articleContent);
246
+
247
+ console.log('> Processing chapters...');
248
+ const processedContent = await processChapterImports(content, contentDir);
249
+
250
+ console.log('> Converting MDX to Markdown...');
251
+ const markdownContent = cleanMdxToMarkdown(processedContent);
252
+
253
+ // Generate output filename
254
+ const title = frontmatter.title ? frontmatter.title.replace(/\n/g, ' ') : 'article';
255
+ const outFileBase = args.filename ? String(args.filename).replace(/\.(tex|pdf)$/i, '') : slugify(title);
256
+
257
+ // Create temporary markdown file
258
+ const tempMdFile = resolve(cwd, 'temp-article.md');
259
+ await fs.writeFile(tempMdFile, markdownContent);
260
+
261
+
262
+ console.log('> Converting to LaTeX with Pandoc...');
263
+ const outputLatex = resolve(cwd, 'dist', `${outFileBase}.tex`);
264
+
265
+ // Ensure dist directory exists
266
+ await fs.mkdir(resolve(cwd, 'dist'), { recursive: true });
267
+
268
+ // Pandoc conversion arguments
269
+ const pandocArgs = [
270
+ tempMdFile,
271
+ '-o', outputLatex,
272
+ '--from=markdown',
273
+ '--to=latex',
274
+ '--standalone',
275
+ '--toc',
276
+ '--number-sections',
277
+ '--highlight-style=tango',
278
+ '--listings'
279
+ ];
280
+
281
+ // Add bibliography if it exists
282
+ const bibFile = resolve(contentDir, 'bibliography.bib');
283
+ try {
284
+ await fs.access(bibFile);
285
+ pandocArgs.push('--bibliography', bibFile);
286
+ pandocArgs.push('--citeproc');
287
+ console.log('✅ Found bibliography file, including citations');
288
+ } catch {
289
+ console.log('ℹ️ No bibliography file found');
290
+ }
291
+
292
+ try {
293
+ await run('pandoc', pandocArgs);
294
+ console.log(`✅ LaTeX generated: ${outputLatex}`);
295
+
296
+ // Optionally compile to PDF if requested
297
+ if (args.pdf) {
298
+ console.log('> Compiling LaTeX to PDF...');
299
+ const outputPdf = resolve(cwd, 'dist', `${outFileBase}.pdf`);
300
+ await run('pdflatex', ['-output-directory', resolve(cwd, 'dist'), outputLatex]);
301
+ console.log(`✅ PDF generated: ${outputPdf}`);
302
+ }
303
+
304
+ } catch (error) {
305
+ console.error('❌ Pandoc conversion failed:', error.message);
306
+ process.exit(1);
307
+ } finally {
308
+ // Clean up temporary file
309
+ try {
310
+ await fs.unlink(tempMdFile);
311
+ } catch { }
312
+ }
313
+ }
314
+
315
+ main().catch((err) => {
316
+ console.error(err);
317
+ process.exit(1);
318
+ });
app/scripts/export-pdf.mjs ADDED
@@ -0,0 +1,483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import { setTimeout as delay } from 'node:timers/promises';
4
+ import { chromium } from 'playwright';
5
+ import { resolve } from 'node:path';
6
+ import { promises as fs } from 'node:fs';
7
+ import process from 'node:process';
8
+
9
+ async function run(command, args = [], options = {}) {
10
+ return new Promise((resolvePromise, reject) => {
11
+ const child = spawn(command, args, { stdio: 'inherit', shell: false, ...options });
12
+ child.on('error', reject);
13
+ child.on('exit', (code) => {
14
+ if (code === 0) resolvePromise(undefined);
15
+ else reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
16
+ });
17
+ });
18
+ }
19
+
20
+ async function waitForServer(url, timeoutMs = 60000) {
21
+ const start = Date.now();
22
+ while (Date.now() - start < timeoutMs) {
23
+ try {
24
+ const res = await fetch(url);
25
+ if (res.ok) return;
26
+ } catch {}
27
+ await delay(500);
28
+ }
29
+ throw new Error(`Server did not start in time: ${url}`);
30
+ }
31
+
32
+ function parseArgs(argv) {
33
+ const out = {};
34
+ for (const arg of argv.slice(2)) {
35
+ if (!arg.startsWith('--')) continue;
36
+ const [k, v] = arg.replace(/^--/, '').split('=');
37
+ out[k] = v === undefined ? true : v;
38
+ }
39
+ return out;
40
+ }
41
+
42
+ function slugify(text) {
43
+ return String(text || '')
44
+ .normalize('NFKD')
45
+ .replace(/\p{Diacritic}+/gu, '')
46
+ .toLowerCase()
47
+ .replace(/[^a-z0-9]+/g, '-')
48
+ .replace(/^-+|-+$/g, '')
49
+ .slice(0, 120) || 'article';
50
+ }
51
+
52
+ function parseMargin(margin) {
53
+ if (!margin) return { top: '12mm', right: '12mm', bottom: '16mm', left: '12mm' };
54
+ const parts = String(margin).split(',').map(s => s.trim()).filter(Boolean);
55
+ if (parts.length === 1) {
56
+ return { top: parts[0], right: parts[0], bottom: parts[0], left: parts[0] };
57
+ }
58
+ if (parts.length === 2) {
59
+ return { top: parts[0], right: parts[1], bottom: parts[0], left: parts[1] };
60
+ }
61
+ if (parts.length === 3) {
62
+ return { top: parts[0], right: parts[1], bottom: parts[2], left: parts[1] };
63
+ }
64
+ return { top: parts[0] || '12mm', right: parts[1] || '12mm', bottom: parts[2] || '16mm', left: parts[3] || '12mm' };
65
+ }
66
+
67
+ function cssLengthToMm(val) {
68
+ if (!val) return 0;
69
+ const s = String(val).trim();
70
+ if (/mm$/i.test(s)) return parseFloat(s);
71
+ if (/cm$/i.test(s)) return parseFloat(s) * 10;
72
+ if (/in$/i.test(s)) return parseFloat(s) * 25.4;
73
+ if (/px$/i.test(s)) return (parseFloat(s) / 96) * 25.4; // 96 CSS px per inch
74
+ const num = parseFloat(s);
75
+ return Number.isFinite(num) ? num : 0; // assume mm if unitless
76
+ }
77
+
78
+ function getFormatSizeMm(format) {
79
+ const f = String(format || 'A4').toLowerCase();
80
+ switch (f) {
81
+ case 'letter': return { w: 215.9, h: 279.4 };
82
+ case 'legal': return { w: 215.9, h: 355.6 };
83
+ case 'a3': return { w: 297, h: 420 };
84
+ case 'tabloid': return { w: 279.4, h: 431.8 };
85
+ case 'a4':
86
+ default: return { w: 210, h: 297 };
87
+ }
88
+ }
89
+
90
+ async function waitForImages(page, timeoutMs = 15000) {
91
+ await page.evaluate(async (timeout) => {
92
+ const deadline = Date.now() + timeout;
93
+ const imgs = Array.from(document.images || []);
94
+ const unloaded = imgs.filter(img => !img.complete || (img.naturalWidth === 0));
95
+ await Promise.race([
96
+ Promise.all(unloaded.map(img => new Promise(res => {
97
+ if (img.complete && img.naturalWidth !== 0) return res(undefined);
98
+ img.addEventListener('load', () => res(undefined), { once: true });
99
+ img.addEventListener('error', () => res(undefined), { once: true });
100
+ }))),
101
+ new Promise(res => setTimeout(res, Math.max(0, deadline - Date.now())))
102
+ ]);
103
+ }, timeoutMs);
104
+ }
105
+
106
+ async function waitForPlotly(page, timeoutMs = 20000) {
107
+ await page.evaluate(async (timeout) => {
108
+ const start = Date.now();
109
+ const hasPlots = () => Array.from(document.querySelectorAll('.js-plotly-plot')).length > 0;
110
+ // Wait until plots exist or timeout
111
+ while (!hasPlots() && (Date.now() - start) < timeout) {
112
+ await new Promise(r => setTimeout(r, 200));
113
+ }
114
+ const deadline = start + timeout;
115
+ // Then wait until each plot contains the main svg
116
+ const allReady = () => Array.from(document.querySelectorAll('.js-plotly-plot')).every(el => el.querySelector('svg.main-svg'));
117
+ while (!allReady() && Date.now() < deadline) {
118
+ await new Promise(r => setTimeout(r, 200));
119
+ }
120
+ }, timeoutMs);
121
+ }
122
+
123
+ async function waitForD3(page, timeoutMs = 20000) {
124
+ await page.evaluate(async (timeout) => {
125
+ const start = Date.now();
126
+ const isReady = () => {
127
+ // Prioritize hero banner if present (generic container)
128
+ const hero = document.querySelector('.hero-banner');
129
+ if (hero) {
130
+ return !!hero.querySelector('svg circle, svg path, svg rect, svg g');
131
+ }
132
+ // Else require all D3 containers on page to have shapes
133
+ const containers = [
134
+ ...Array.from(document.querySelectorAll('.d3-line')),
135
+ ...Array.from(document.querySelectorAll('.d3-bar'))
136
+ ];
137
+ if (!containers.length) return true;
138
+ return containers.every(c => c.querySelector('svg circle, svg path, svg rect, svg g'));
139
+ };
140
+ while (!isReady() && (Date.now() - start) < timeout) {
141
+ await new Promise(r => setTimeout(r, 200));
142
+ }
143
+ }, timeoutMs);
144
+ }
145
+
146
+ async function waitForStableLayout(page, timeoutMs = 5000) {
147
+ const start = Date.now();
148
+ let last = await page.evaluate(() => document.scrollingElement ? document.scrollingElement.scrollHeight : document.body.scrollHeight);
149
+ let stableCount = 0;
150
+ while ((Date.now() - start) < timeoutMs && stableCount < 3) {
151
+ await page.waitForTimeout(250);
152
+ const now = await page.evaluate(() => document.scrollingElement ? document.scrollingElement.scrollHeight : document.body.scrollHeight);
153
+ if (now === last) stableCount += 1; else { stableCount = 0; last = now; }
154
+ }
155
+ }
156
+
157
+ async function main() {
158
+ const cwd = process.cwd();
159
+ const port = Number(process.env.PREVIEW_PORT || 8080);
160
+ const baseUrl = `http://127.0.0.1:${port}/`;
161
+ const args = parseArgs(process.argv);
162
+ // Default: light (do not rely on env vars implicitly)
163
+ const theme = (args.theme === 'dark' || args.theme === 'light') ? args.theme : 'light';
164
+ const format = args.format || 'A4';
165
+ const margin = parseMargin(args.margin);
166
+ const wait = (args.wait || 'full'); // 'networkidle' | 'images' | 'plotly' | 'full'
167
+
168
+ // filename can be provided, else computed from DOM (button) or page title later
169
+ let outFileBase = (args.filename && String(args.filename).replace(/\.pdf$/i, '')) || 'article';
170
+
171
+ // Build only if dist/ does not exist
172
+ const distDir = resolve(cwd, 'dist');
173
+ let hasDist = false;
174
+ try {
175
+ const st = await fs.stat(distDir);
176
+ hasDist = st && st.isDirectory();
177
+ } catch {}
178
+ if (!hasDist) {
179
+ console.log('> Building Astro site…');
180
+ await run('npm', ['run', 'build']);
181
+ } else {
182
+ console.log('> Skipping build (dist/ exists)…');
183
+ }
184
+
185
+ console.log('> Starting Astro preview…');
186
+ // Start preview in its own process group so we can terminate all children reliably
187
+ const preview = spawn('npm', ['run', 'preview'], { cwd, stdio: 'inherit', detached: true });
188
+ const previewExit = new Promise((resolvePreview) => {
189
+ preview.on('close', (code, signal) => resolvePreview({ code, signal }));
190
+ });
191
+
192
+ try {
193
+ await waitForServer(baseUrl, 60000);
194
+ console.log('> Server ready, generating PDF…');
195
+
196
+ const browser = await chromium.launch({ headless: true });
197
+ try {
198
+ const context = await browser.newContext();
199
+ await context.addInitScript((desired) => {
200
+ try {
201
+ localStorage.setItem('theme', desired);
202
+ // Apply theme immediately to avoid flashes
203
+ if (document && document.documentElement) {
204
+ document.documentElement.dataset.theme = desired;
205
+ }
206
+ } catch {}
207
+ }, theme);
208
+ const page = await context.newPage();
209
+ // Pre-fit viewport width to printable width so charts size correctly
210
+ const fmt = getFormatSizeMm(format);
211
+ const mw = fmt.w - cssLengthToMm(margin.left) - cssLengthToMm(margin.right);
212
+ const printableWidthPx = Math.max(320, Math.round((mw / 25.4) * 96));
213
+ await page.setViewportSize({ width: printableWidthPx, height: 1200 });
214
+ await page.goto(baseUrl, { waitUntil: 'load', timeout: 60000 });
215
+ // Give time for CDN scripts (Plotly/D3) to attach and for our fragment hooks to run
216
+ try { await page.waitForFunction(() => !!window.Plotly, { timeout: 8000 }); } catch {}
217
+ try { await page.waitForFunction(() => !!window.d3, { timeout: 8000 }); } catch {}
218
+ // Prefer explicit filename from the download button if present
219
+ if (!args.filename) {
220
+ const fromBtn = await page.evaluate(() => {
221
+ const btn = document.getElementById('download-pdf-btn');
222
+ const f = btn ? btn.getAttribute('data-pdf-filename') : null;
223
+ return f || '';
224
+ });
225
+ if (fromBtn) {
226
+ outFileBase = String(fromBtn).replace(/\.pdf$/i, '');
227
+ } else {
228
+ // Fallback: compute slug from hero title or document.title
229
+ const title = await page.evaluate(() => {
230
+ const h1 = document.querySelector('h1.hero-title');
231
+ const t = h1 ? h1.textContent : document.title;
232
+ return (t || '').replace(/\s+/g, ' ').trim();
233
+ });
234
+ outFileBase = slugify(title);
235
+ }
236
+ }
237
+
238
+ // Wait for render readiness
239
+ if (wait === 'images' || wait === 'full') {
240
+ await waitForImages(page);
241
+ }
242
+ if (wait === 'd3' || wait === 'full') {
243
+ await waitForD3(page);
244
+ }
245
+ if (wait === 'plotly' || wait === 'full') {
246
+ await waitForPlotly(page);
247
+ }
248
+ if (wait === 'full') {
249
+ await waitForStableLayout(page);
250
+ }
251
+ await page.emulateMedia({ media: 'print' });
252
+
253
+ // Enforce responsive sizing for SVG/iframes by removing hard attrs and injecting CSS (top-level and inside same-origin iframes)
254
+ try {
255
+ await page.evaluate(() => {
256
+ function isSmallSvg(svg){
257
+ try {
258
+ const vb = svg && svg.viewBox && svg.viewBox.baseVal ? svg.viewBox.baseVal : null;
259
+ if (vb && vb.width && vb.height && vb.width <= 50 && vb.height <= 50) return true;
260
+ const r = svg.getBoundingClientRect && svg.getBoundingClientRect();
261
+ if (r && r.width && r.height && r.width <= 50 && r.height <= 50) return true;
262
+ } catch {}
263
+ return false;
264
+ }
265
+ function lockSmallSvgSize(svg){
266
+ try {
267
+ const r = svg.getBoundingClientRect ? svg.getBoundingClientRect() : null;
268
+ const w = (r && r.width) ? Math.round(r.width) : null;
269
+ const h = (r && r.height) ? Math.round(r.height) : null;
270
+ if (w) svg.style.setProperty('width', w + 'px', 'important');
271
+ if (h) svg.style.setProperty('height', h + 'px', 'important');
272
+ svg.style.setProperty('max-width', 'none', 'important');
273
+ } catch {}
274
+ }
275
+ function fixSvg(svg){
276
+ if (!svg) return;
277
+ // Do not alter hero banner SVG sizing; it may rely on explicit width/height
278
+ try { if (svg.closest && svg.closest('.hero-banner')) return; } catch {}
279
+ if (isSmallSvg(svg)) { lockSmallSvgSize(svg); return; }
280
+ try { svg.removeAttribute('width'); } catch {}
281
+ try { svg.removeAttribute('height'); } catch {}
282
+ svg.style.maxWidth = '100%';
283
+ svg.style.width = '100%';
284
+ svg.style.height = 'auto';
285
+ if (!svg.getAttribute('preserveAspectRatio')) svg.setAttribute('preserveAspectRatio','xMidYMid meet');
286
+ }
287
+ document.querySelectorAll('svg').forEach(fixSvg);
288
+ document.querySelectorAll('.mermaid, .mermaid svg').forEach((el)=>{
289
+ if (el.tagName && el.tagName.toLowerCase() === 'svg') fixSvg(el);
290
+ else { el.style.display='block'; el.style.width='100%'; el.style.maxWidth='100%'; }
291
+ });
292
+ document.querySelectorAll('iframe, embed, object').forEach((el) => {
293
+ el.style.width = '100%';
294
+ el.style.maxWidth = '100%';
295
+ try { el.removeAttribute('width'); } catch {}
296
+ // Best-effort inject into same-origin frames
297
+ try {
298
+ const doc = (el.tagName.toLowerCase()==='object' ? el.contentDocument : el.contentDocument);
299
+ if (doc && doc.head) {
300
+ const s = doc.createElement('style');
301
+ s.textContent = 'html,body{overflow-x:hidden;} svg,canvas,img,video{max-width:100%!important;height:auto!important;} svg[width]{width:100%!important}';
302
+ doc.head.appendChild(s);
303
+ doc.querySelectorAll('svg').forEach((svg)=>{ if (isSmallSvg(svg)) lockSmallSvgSize(svg); else fixSvg(svg); });
304
+ }
305
+ } catch (_) { /* cross-origin; ignore */ }
306
+ });
307
+ });
308
+ } catch {}
309
+
310
+ // Generate OG thumbnail (1200x630)
311
+ try {
312
+ const ogW = 1200, ogH = 630;
313
+ await page.setViewportSize({ width: ogW, height: ogH });
314
+ // Give layout a tick to adjust
315
+ await page.waitForTimeout(200);
316
+ // Ensure layout & D3 re-rendered after viewport change
317
+ await page.evaluate(() => { window.scrollTo(0, 0); window.dispatchEvent(new Event('resize')); });
318
+ try { await waitForD3(page, 8000); } catch {}
319
+
320
+ // Temporarily improve visibility for light theme thumbnails
321
+ // - Force normal blend for points
322
+ // - Ensure an SVG background (CSS background on svg element)
323
+ const cssHandle = await page.addStyleTag({ content: `
324
+ .hero .points { mix-blend-mode: normal !important; }
325
+ ` });
326
+ const thumbPath = resolve(cwd, 'dist', 'thumb.auto.jpg');
327
+ await page.screenshot({ path: thumbPath, type: 'jpeg', quality: 85, fullPage: false });
328
+ // Also emit PNG for compatibility if needed
329
+ const thumbPngPath = resolve(cwd, 'dist', 'thumb.auto.png');
330
+ await page.screenshot({ path: thumbPngPath, type: 'png', fullPage: false });
331
+ const publicThumb = resolve(cwd, 'public', 'thumb.auto.jpg');
332
+ const publicThumbPng = resolve(cwd, 'public', 'thumb.auto.png');
333
+ try { await fs.copyFile(thumbPath, publicThumb); } catch {}
334
+ try { await fs.copyFile(thumbPngPath, publicThumbPng); } catch {}
335
+ // Remove temporary style so PDF is unaffected
336
+ try { await cssHandle.evaluate((el) => el.remove()); } catch {}
337
+ console.log(`✅ OG thumbnail generated: ${thumbPath}`);
338
+ } catch (e) {
339
+ console.warn('Unable to generate OG thumbnail:', e?.message || e);
340
+ }
341
+ const outPath = resolve(cwd, 'dist', `${outFileBase}.pdf`);
342
+ // Restore viewport to printable width before PDF (thumbnail changed it)
343
+ try {
344
+ const fmt2 = getFormatSizeMm(format);
345
+ const mw2 = fmt2.w - cssLengthToMm(margin.left) - cssLengthToMm(margin.right);
346
+ const printableWidthPx2 = Math.max(320, Math.round((mw2 / 25.4) * 96));
347
+ await page.setViewportSize({ width: printableWidthPx2, height: 1400 });
348
+ await page.evaluate(() => { window.scrollTo(0, 0); window.dispatchEvent(new Event('resize')); });
349
+ try { await waitForD3(page, 8000); } catch {}
350
+ await waitForStableLayout(page);
351
+ // Re-apply responsive fixes after viewport change
352
+ try {
353
+ await page.evaluate(() => {
354
+ function isSmallSvg(svg){
355
+ try {
356
+ const vb = svg && svg.viewBox && svg.viewBox.baseVal ? svg.viewBox.baseVal : null;
357
+ if (vb && vb.width && vb.height && vb.width <= 50 && vb.height <= 50) return true;
358
+ const r = svg.getBoundingClientRect && svg.getBoundingClientRect();
359
+ if (r && r.width && r.height && r.width <= 50 && r.height <= 50) return true;
360
+ } catch {}
361
+ return false;
362
+ }
363
+ function lockSmallSvgSize(svg){
364
+ try {
365
+ const r = svg.getBoundingClientRect ? svg.getBoundingClientRect() : null;
366
+ const w = (r && r.width) ? Math.round(r.width) : null;
367
+ const h = (r && r.height) ? Math.round(r.height) : null;
368
+ if (w) svg.style.setProperty('width', w + 'px', 'important');
369
+ if (h) svg.style.setProperty('height', h + 'px', 'important');
370
+ svg.style.setProperty('max-width', 'none', 'important');
371
+ } catch {}
372
+ }
373
+ function fixSvg(svg){
374
+ if (!svg) return;
375
+ // Do not alter hero banner SVG sizing; it may rely on explicit width/height
376
+ try { if (svg.closest && svg.closest('.hero-banner')) return; } catch {}
377
+ if (isSmallSvg(svg)) { lockSmallSvgSize(svg); return; }
378
+ try { svg.removeAttribute('width'); } catch {}
379
+ try { svg.removeAttribute('height'); } catch {}
380
+ svg.style.maxWidth = '100%';
381
+ svg.style.width = '100%';
382
+ svg.style.height = 'auto';
383
+ if (!svg.getAttribute('preserveAspectRatio')) svg.setAttribute('preserveAspectRatio','xMidYMid meet');
384
+ }
385
+ document.querySelectorAll('svg').forEach((svg)=>{ if (isSmallSvg(svg)) lockSmallSvgSize(svg); else fixSvg(svg); });
386
+ document.querySelectorAll('.mermaid, .mermaid svg').forEach((el)=>{
387
+ if (el.tagName && el.tagName.toLowerCase() === 'svg') fixSvg(el);
388
+ else { el.style.display='block'; el.style.width='100%'; el.style.maxWidth='100%'; }
389
+ });
390
+ document.querySelectorAll('iframe, embed, object').forEach((el) => {
391
+ el.style.width = '100%';
392
+ el.style.maxWidth = '100%';
393
+ try { el.removeAttribute('width'); } catch {}
394
+ try {
395
+ const doc = (el.tagName.toLowerCase()==='object' ? el.contentDocument : el.contentDocument);
396
+ if (doc && doc.head) {
397
+ const s = doc.createElement('style');
398
+ s.textContent = 'html,body{overflow-x:hidden;} svg,canvas,img,video{max-width:100%!important;height:auto!important;} svg[width]{width:100%!important}';
399
+ doc.head.appendChild(s);
400
+ doc.querySelectorAll('svg').forEach((svg)=>{ if (isSmallSvg(svg)) lockSmallSvgSize(svg); else fixSvg(svg); });
401
+ }
402
+ } catch (_) {}
403
+ });
404
+ });
405
+ } catch {}
406
+ } catch {}
407
+ // Temporarily enforce print-safe responsive sizing (SVG/iframes) and improve banner visibility
408
+ let pdfCssHandle = null;
409
+ try {
410
+ pdfCssHandle = await page.addStyleTag({ content: `
411
+ /* General container safety */
412
+ html, body { overflow-x: hidden !important; }
413
+
414
+ /* Make all vector/bitmap media responsive for print */
415
+ svg, canvas, img, video { max-width: 100% !important; height: auto !important; }
416
+ /* Mermaid diagrams */
417
+ .mermaid, .mermaid svg { display: block; width: 100% !important; max-width: 100% !important; height: auto !important; }
418
+ /* Any explicit width attributes */
419
+ svg[width] { width: 100% !important; }
420
+ /* Iframes and similar embeds */
421
+ iframe, embed, object { width: 100% !important; max-width: 100% !important; height: auto; }
422
+
423
+ /* HtmlEmbed wrappers (defensive) */
424
+ .html-embed, .html-embed__card { max-width: 100% !important; width: 100% !important; }
425
+ .html-embed__card > div[id^="frag-"] { width: 100% !important; max-width: 100% !important; }
426
+
427
+ /* Banner centering & visibility */
428
+ .hero .points { mix-blend-mode: normal !important; }
429
+ /* Do NOT force a fixed height to avoid clipping in PDF */
430
+ .hero-banner { width: 100% !important; max-width: 980px !important; margin-left: auto !important; margin-right: auto !important; }
431
+ .hero-banner svg { width: 100% !important; height: auto !important; }
432
+ ` });
433
+ } catch {}
434
+ await page.pdf({
435
+ path: outPath,
436
+ format,
437
+ printBackground: true,
438
+ margin
439
+ });
440
+ try { if (pdfCssHandle) await pdfCssHandle.evaluate((el) => el.remove()); } catch {}
441
+ console.log(`✅ PDF generated: ${outPath}`);
442
+
443
+ // Copy into public only under the slugified name
444
+ const publicSlugPath = resolve(cwd, 'public', `${outFileBase}.pdf`);
445
+ try {
446
+ await fs.mkdir(resolve(cwd, 'public'), { recursive: true });
447
+ await fs.copyFile(outPath, publicSlugPath);
448
+ console.log(`✅ PDF copied to: ${publicSlugPath}`);
449
+ } catch (e) {
450
+ console.warn('Unable to copy PDF to public/:', e?.message || e);
451
+ }
452
+ } finally {
453
+ await browser.close();
454
+ }
455
+ } finally {
456
+ // Try a clean shutdown of preview (entire process group first)
457
+ try {
458
+ if (process.platform !== 'win32') {
459
+ try { process.kill(-preview.pid, 'SIGINT'); } catch {}
460
+ }
461
+ try { preview.kill('SIGINT'); } catch {}
462
+ await Promise.race([previewExit, delay(3000)]);
463
+ // Force kill if still alive
464
+ // eslint-disable-next-line no-unsafe-optional-chaining
465
+ if (!preview.killed) {
466
+ try {
467
+ if (process.platform !== 'win32') {
468
+ try { process.kill(-preview.pid, 'SIGKILL'); } catch {}
469
+ }
470
+ try { preview.kill('SIGKILL'); } catch {}
471
+ } catch {}
472
+ await Promise.race([previewExit, delay(1000)]);
473
+ }
474
+ } catch {}
475
+ }
476
+ }
477
+
478
+ main().catch((err) => {
479
+ console.error(err);
480
+ process.exit(1);
481
+ });
482
+
483
+
app/scripts/generate-trackio-data.mjs ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ // Generate synthetic Trackio-like CSV data with realistic ML curves.
4
+ // - Steps are simple integers (e.g., 1..N)
5
+ // - Metrics: epoch, train_accuracy, val_accuracy, train_loss, val_loss
6
+ // - W&B-like run names (e.g., pleasant-flower-1)
7
+ // - Deterministic with --seed
8
+ //
9
+ // Usage:
10
+ // node app/scripts/generate-trackio-data.mjs \
11
+ // --runs 3 \
12
+ // --steps 10 \
13
+ // --out app/src/content/assets/data/trackio_wandb_synth.csv \
14
+ // [--seed 42] [--epoch-max 3.0] [--amount 1.0] [--start 1]
15
+ //
16
+ // To overwrite the demo file used by the embed:
17
+ // node app/scripts/generate-trackio-data.mjs --runs 3 --steps 10 --out app/src/content/assets/data/trackio_wandb_demo.csv --seed 1337
18
+
19
+ import fs from 'node:fs/promises';
20
+ import path from 'node:path';
21
+
22
+ function parseArgs(argv){
23
+ const args = { runs: 3, steps: 10, out: '', seed: undefined, epochMax: 3.0, amount: 1, start: 1 };
24
+ for (let i = 2; i < argv.length; i++){
25
+ const a = argv[i];
26
+ if (a === '--runs' && argv[i+1]) { args.runs = Math.max(1, parseInt(argv[++i], 10) || 3); continue; }
27
+ if (a === '--steps' && argv[i+1]) { args.steps = Math.max(2, parseInt(argv[++i], 10) || 10); continue; }
28
+ if (a === '--out' && argv[i+1]) { args.out = argv[++i]; continue; }
29
+ if (a === '--seed' && argv[i+1]) { args.seed = Number(argv[++i]); continue; }
30
+ if (a === '--epoch-max' && argv[i+1]) { args.epochMax = Number(argv[++i]) || 3.0; continue; }
31
+ if (a === '--amount' && argv[i+1]) { args.amount = Number(argv[++i]) || 1.0; continue; }
32
+ if (a === '--start' && argv[i+1]) { args.start = parseInt(argv[++i], 10) || 1; continue; }
33
+ }
34
+ if (!args.out) {
35
+ args.out = path.join('app', 'src', 'content', 'assets', 'data', 'trackio_wandb_synth.csv');
36
+ }
37
+ return args;
38
+ }
39
+
40
+ function mulberry32(seed){
41
+ let t = seed >>> 0;
42
+ return function(){
43
+ t += 0x6D2B79F5;
44
+ let r = Math.imul(t ^ (t >>> 15), 1 | t);
45
+ r ^= r + Math.imul(r ^ (r >>> 7), 61 | r);
46
+ return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
47
+ };
48
+ }
49
+
50
+ function makeRng(seed){
51
+ if (Number.isFinite(seed)) return mulberry32(seed);
52
+ return Math.random;
53
+ }
54
+
55
+ function randn(rng){
56
+ // Box-Muller transform
57
+ let u = 0, v = 0;
58
+ while (u === 0) u = rng();
59
+ while (v === 0) v = rng();
60
+ return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
61
+ }
62
+
63
+ function clamp(x, lo, hi){
64
+ return Math.max(lo, Math.min(hi, x));
65
+ }
66
+
67
+ function logistic(t, k=6, x0=0.5){
68
+ // 1 / (1 + e^{-k (t - x0)}) in [0,1]
69
+ return 1 / (1 + Math.exp(-k * (t - x0)));
70
+ }
71
+
72
+ function expDecay(t, k=3){
73
+ // (1 - e^{-k t}) in [0,1]
74
+ return 1 - Math.exp(-k * t);
75
+ }
76
+
77
+ function pick(array, rng){
78
+ return array[Math.floor(rng() * array.length) % array.length];
79
+ }
80
+
81
+ function buildRunNames(count, rng){
82
+ const adjectives = [
83
+ 'pleasant','brisk','silent','ancient','bold','gentle','rapid','shy','curious','lively',
84
+ 'fearless','soothing','glossy','hidden','misty','bright','calm','keen','noble','swift'
85
+ ];
86
+ const nouns = [
87
+ 'flower','glade','sky','river','forest','ember','comet','meadow','harbor','dawn',
88
+ 'mountain','prairie','breeze','valley','lagoon','desert','monsoon','reef','thunder','willow'
89
+ ];
90
+ const names = new Set();
91
+ let attempts = 0;
92
+ while (names.size < count && attempts < count * 20){
93
+ attempts++;
94
+ const left = pick(adjectives, rng);
95
+ const right = pick(nouns, rng);
96
+ const idx = 1 + Math.floor(rng() * 9);
97
+ names.add(`${left}-${right}-${idx}`);
98
+ }
99
+ return Array.from(names);
100
+ }
101
+
102
+ function formatLike(value, decimals){
103
+ return Number.isFinite(decimals) && decimals >= 0 ? value.toFixed(decimals) : String(value);
104
+ }
105
+
106
+ async function main(){
107
+ const args = parseArgs(process.argv);
108
+ const rng = makeRng(args.seed);
109
+
110
+ // Steps: integers from start .. start+steps-1
111
+ const steps = Array.from({ length: args.steps }, (_, i) => args.start + i);
112
+ const stepNorm = (i) => (i - steps[0]) / (steps[steps.length-1] - steps[0]);
113
+
114
+ const runs = buildRunNames(args.runs, rng);
115
+
116
+ // Per-run slight variations
117
+ const runParams = runs.map((_r, idx) => {
118
+ const r = rng();
119
+ // Final accuracies
120
+ const trainAccFinal = clamp(0.86 + (r - 0.5) * 0.12 * args.amount, 0.78, 0.97);
121
+ const valAccFinal = clamp(trainAccFinal - (0.02 + rng() * 0.05), 0.70, 0.95);
122
+ // Loss plateau
123
+ const lossStart = 7.0 + (rng() - 0.5) * 0.10 * args.amount; // ~7.0 ±0.05
124
+ const lossPlateau = 6.78 + (rng() - 0.5) * 0.04 * args.amount; // ~6.78 ±0.02
125
+ const lossK = 2.0 + rng() * 1.5; // decay speed
126
+ // Acc growth steepness and midpoint
127
+ const kAcc = 4.5 + rng() * 3.0;
128
+ const x0Acc = 0.35 + rng() * 0.25;
129
+ return { trainAccFinal, valAccFinal, lossStart, lossPlateau, lossK, kAcc, x0Acc };
130
+ });
131
+
132
+ const lines = [];
133
+ lines.push('run,step,metric,value,stderr');
134
+
135
+ // EPOCH: linear 0..epochMax across steps
136
+ for (let r = 0; r < runs.length; r++){
137
+ const run = runs[r];
138
+ for (let i = 0; i < steps.length; i++){
139
+ const t = stepNorm(steps[i]);
140
+ const epoch = args.epochMax * t;
141
+ lines.push(`${run},${steps[i]},epoch,${formatLike(epoch, 2)},`);
142
+ }
143
+ }
144
+
145
+ // TRAIN LOSS & VAL LOSS
146
+ for (let r = 0; r < runs.length; r++){
147
+ const run = runs[r];
148
+ const p = runParams[r];
149
+ let prevTrain = null;
150
+ let prevVal = null;
151
+ for (let i = 0; i < steps.length; i++){
152
+ const t = stepNorm(steps[i]);
153
+ const d = expDecay(t, p.lossK); // 0..1
154
+ let trainLoss = p.lossStart - (p.lossStart - p.lossPlateau) * d;
155
+ let valLoss = trainLoss + 0.02 + (rng() * 0.03);
156
+ // Add mild noise
157
+ trainLoss += randn(rng) * 0.01 * args.amount;
158
+ valLoss += randn(rng) * 0.012 * args.amount;
159
+ // Keep reasonable and mostly monotonic (small upward blips allowed)
160
+ if (prevTrain != null) trainLoss = Math.min(prevTrain + 0.01, trainLoss);
161
+ if (prevVal != null) valLoss = Math.min(prevVal + 0.012, valLoss);
162
+ prevTrain = trainLoss; prevVal = valLoss;
163
+ const stderrTrain = clamp(0.03 - 0.02 * t + Math.abs(randn(rng)) * 0.003, 0.006, 0.04);
164
+ const stderrVal = clamp(0.035 - 0.022 * t + Math.abs(randn(rng)) * 0.003, 0.008, 0.045);
165
+ lines.push(`${run},${steps[i]},train_loss,${formatLike(trainLoss, 3)},${formatLike(stderrTrain, 3)}`);
166
+ lines.push(`${run},${steps[i]},val_loss,${formatLike(valLoss, 3)},${formatLike(stderrVal, 3)}`);
167
+ }
168
+ }
169
+
170
+ // TRAIN ACCURACY & VAL ACCURACY (logistic)
171
+ for (let r = 0; r < runs.length; r++){
172
+ const run = runs[r];
173
+ const p = runParams[r];
174
+ for (let i = 0; i < steps.length; i++){
175
+ const t = stepNorm(steps[i]);
176
+ const accBase = logistic(t, p.kAcc, p.x0Acc);
177
+ let trainAcc = clamp(0.55 + accBase * (p.trainAccFinal - 0.55), 0, 1);
178
+ let valAcc = clamp(0.52 + accBase * (p.valAccFinal - 0.52), 0, 1);
179
+ // Gentle noise
180
+ trainAcc = clamp(trainAcc + randn(rng) * 0.005 * args.amount, 0, 1);
181
+ valAcc = clamp(valAcc + randn(rng) * 0.006 * args.amount, 0, 1);
182
+ const stderrTrain = clamp(0.02 - 0.011 * t + Math.abs(randn(rng)) * 0.002, 0.006, 0.03);
183
+ const stderrVal = clamp(0.022 - 0.012 * t + Math.abs(randn(rng)) * 0.002, 0.007, 0.032);
184
+ lines.push(`${run},${steps[i]},train_accuracy,${formatLike(trainAcc, 4)},${formatLike(stderrTrain, 3)}`);
185
+ lines.push(`${run},${steps[i]},val_accuracy,${formatLike(valAcc, 4)},${formatLike(stderrVal, 3)}`);
186
+ }
187
+ }
188
+
189
+ // Ensure directory exists
190
+ await fs.mkdir(path.dirname(args.out), { recursive: true });
191
+ await fs.writeFile(args.out, lines.join('\n') + '\n', 'utf8');
192
+ const relOut = path.relative(process.cwd(), args.out);
193
+ console.log(`Synthetic CSV generated: ${relOut}`);
194
+ }
195
+
196
+ main().catch(err => { console.error(err?.stack || String(err)); process.exit(1); });
app/scripts/jitter-trackio-data.mjs ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ // Jitter Trackio CSV data with small, controlled noise.
4
+ // - Preserves comments (# ...) and blank lines
5
+ // - Leaves 'epoch' values unchanged
6
+ // - Adds mild noise to train/val accuracy (clamped to [0,1])
7
+ // - Adds mild noise to train/val loss (kept >= 0)
8
+ // - Keeps steps untouched
9
+ // Usage:
10
+ // node app/scripts/jitter-trackio-data.mjs \
11
+ // --in app/src/content/assets/data/trackio_wandb_demo.csv \
12
+ // --out app/src/content/assets/data/trackio_wandb_demo.jitter.csv \
13
+ // [--seed 42] [--amount 1.0] [--in-place]
14
+
15
+ import fs from 'node:fs/promises';
16
+ import path from 'node:path';
17
+
18
+ function parseArgs(argv){
19
+ const args = { in: '', out: '', seed: undefined, amount: 1, inPlace: false };
20
+ for (let i = 2; i < argv.length; i++){
21
+ const a = argv[i];
22
+ if (a === '--in' && argv[i+1]) { args.in = argv[++i]; continue; }
23
+ if (a === '--out' && argv[i+1]) { args.out = argv[++i]; continue; }
24
+ if (a === '--seed' && argv[i+1]) { args.seed = Number(argv[++i]); continue; }
25
+ if (a === '--amount' && argv[i+1]) { args.amount = Number(argv[++i]) || 3; continue; }
26
+ if (a === '--in-place') { args.inPlace = true; continue; }
27
+ }
28
+ if (!args.in) throw new Error('--in is required');
29
+ if (args.inPlace) args.out = args.in;
30
+ if (!args.out) {
31
+ const { dir, name, ext } = path.parse(args.in);
32
+ args.out = path.join(dir, `${name}.jitter${ext || '.csv'}`);
33
+ }
34
+ return args;
35
+ }
36
+
37
+ function mulberry32(seed){
38
+ let t = seed >>> 0;
39
+ return function(){
40
+ t += 0x6D2B79F5;
41
+ let r = Math.imul(t ^ (t >>> 15), 1 | t);
42
+ r ^= r + Math.imul(r ^ (r >>> 7), 61 | r);
43
+ return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
44
+ };
45
+ }
46
+
47
+ function makeRng(seed){
48
+ if (Number.isFinite(seed)) return mulberry32(seed);
49
+ return Math.random;
50
+ }
51
+
52
+ function randn(rng){
53
+ // Box-Muller transform
54
+ let u = 0, v = 0;
55
+ while (u === 0) u = rng();
56
+ while (v === 0) v = rng();
57
+ return Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
58
+ }
59
+
60
+ function jitterValue(metric, value, amount, rng){
61
+ const m = metric.toLowerCase();
62
+ if (m === 'epoch') return value; // keep as-is
63
+ if (m.includes('accuracy')){
64
+ const n = Math.max(-0.02 * amount, Math.min(0.02 * amount, randn(rng) * 0.01 * amount));
65
+ return Math.max(0, Math.min(1, value + n));
66
+ }
67
+ if (m.includes('loss')){
68
+ const n = Math.max(-0.03 * amount, Math.min(0.03 * amount, randn(rng) * 0.01 * amount));
69
+ return Math.max(0, value + n);
70
+ }
71
+ // default: tiny noise
72
+ const n = Math.max(-0.01 * amount, Math.min(0.01 * amount, randn(rng) * 0.005 * amount));
73
+ return value + n;
74
+ }
75
+
76
+ function formatNumberLike(original, value){
77
+ const s = String(original);
78
+ const dot = s.indexOf('.')
79
+ const decimals = dot >= 0 ? (s.length - dot - 1) : 0;
80
+ if (!Number.isFinite(value)) return s;
81
+ if (decimals <= 0) return String(Math.round(value));
82
+ return value.toFixed(decimals);
83
+ }
84
+
85
+ async function main(){
86
+ const args = parseArgs(process.argv);
87
+ const rng = makeRng(args.seed);
88
+ const raw = await fs.readFile(args.in, 'utf8');
89
+ const lines = raw.split(/\r?\n/);
90
+ const out = new Array(lines.length);
91
+
92
+ for (let i = 0; i < lines.length; i++){
93
+ const line = lines[i];
94
+ if (!line || line.trim().length === 0) { out[i] = line; continue; }
95
+ if (/^\s*#/.test(line)) { out[i] = line; continue; }
96
+
97
+ // Preserve header line unmodified
98
+ if (i === 0 && /^\s*run\s*,\s*step\s*,\s*metric\s*,\s*value\s*,\s*stderr\s*$/i.test(line)) {
99
+ out[i] = line; continue;
100
+ }
101
+
102
+ const cols = line.split(',');
103
+ if (cols.length < 4) { out[i] = line; continue; }
104
+
105
+ const [run, stepStr, metric, valueStr, stderrStr = ''] = cols;
106
+ const trimmedMetric = (metric || '').trim();
107
+ const valueNum = Number((valueStr || '').trim());
108
+
109
+ if (!Number.isFinite(valueNum)) { out[i] = line; continue; }
110
+
111
+ const jittered = jitterValue(trimmedMetric, valueNum, args.amount, rng);
112
+ const valueOut = formatNumberLike(valueStr, jittered);
113
+
114
+ // Reassemble with original column count and positions
115
+ const result = [run, stepStr, metric, valueOut, stderrStr].join(',');
116
+ out[i] = result;
117
+ }
118
+
119
+ const finalText = out.join('\n');
120
+ await fs.writeFile(args.out, finalText, 'utf8');
121
+ const relIn = path.relative(process.cwd(), args.in);
122
+ const relOut = path.relative(process.cwd(), args.out);
123
+ console.log(`Jittered data written: ${relOut} (from ${relIn})`);
124
+ }
125
+
126
+ main().catch(err => {
127
+ console.error(err?.stack || String(err));
128
+ process.exit(1);
129
+ });
app/scripts/latex-importer/README.md ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LaTeX Importer
2
+
3
+ Complete LaTeX to MDX (Markdown + JSX) importer optimized for Astro with advanced support for references, interactive equations, and components.
4
+
5
+ ## 🚀 Quick Start
6
+
7
+ ```bash
8
+ # Complete LaTeX → MDX conversion with all features
9
+ node index.mjs
10
+
11
+ # For step-by-step debugging
12
+ node latex-converter.mjs # LaTeX → Markdown
13
+ node mdx-converter.mjs # Markdown → MDX
14
+ ```
15
+
16
+ ## 📁 Structure
17
+
18
+ ```
19
+ latex-importer/
20
+ ├── index.mjs # Complete LaTeX → MDX pipeline
21
+ ├── latex-converter.mjs # LaTeX → Markdown with Pandoc
22
+ ├── mdx-converter.mjs # Markdown → MDX with Astro components
23
+ ├── reference-preprocessor.mjs # LaTeX references cleanup
24
+ ├── post-processor.mjs # Markdown post-processing
25
+ ├── bib-cleaner.mjs # Bibliography cleaner
26
+ ├── filters/
27
+ │ └── equation-ids.lua # Pandoc filter for KaTeX equations
28
+ ├── input/ # LaTeX sources
29
+ │ ├── main.tex
30
+ │ ├── main.bib
31
+ │ └── sections/
32
+ └── output/ # Results
33
+ ├── main.md # Intermediate Markdown
34
+ └── main.mdx # Final MDX for Astro
35
+ ```
36
+
37
+ ## ✨ Key Features
38
+
39
+ ### 🎯 **Smart References**
40
+ - **Invisible anchors**: Automatic conversion of `\label{}` to `<span id="..." style="position: absolute;"></span>`
41
+ - **Clean links**: Identifier cleanup (`:` → `-`, removing prefixes `sec:`, `fig:`, `eq:`)
42
+ - **Cross-references**: Full support for `\ref{}` with functional links
43
+
44
+ ### 🧮 **Interactive Equations**
45
+ - **KaTeX IDs**: Conversion of `\label{eq:...}` to `\htmlId{id}{equation}`
46
+ - **Equation references**: Clickable links to mathematical equations
47
+ - **Advanced KaTeX support**: `trust: true` configuration for `\htmlId{}`
48
+
49
+ ### 🎨 **Automatic Styling**
50
+ - **Highlights**: `\highlight{text}` → `<span class="highlight">text</span>`
51
+ - **Auto cleanup**: Removal of numbering `(1)`, `(2)`, etc.
52
+ - **Astro components**: Images → `Figure` with automatic imports
53
+
54
+ ### 🔧 **Robust Pipeline**
55
+ - **LaTeX preprocessor**: Reference cleanup before Pandoc
56
+ - **Lua filter**: Equation processing in Pandoc AST
57
+ - **Post-processor**: Markdown cleanup and optimization
58
+ - **MDX converter**: Final transformation with Astro components
59
+
60
+ ## 📊 Example Workflow
61
+
62
+ ```bash
63
+ # 1. Prepare LaTeX sources
64
+ cp my-paper/* input/
65
+
66
+ # 2. Complete automatic conversion
67
+ node index.mjs
68
+
69
+ # 3. Generated results
70
+ ls output/
71
+ # → main.md (Intermediate Markdown)
72
+ # → main.mdx (Final MDX for Astro)
73
+ # → assets/image/ (extracted images)
74
+ ```
75
+
76
+ ### 📋 Conversion Result
77
+
78
+ The pipeline generates an MDX file optimized for Astro with:
79
+
80
+ ```mdx
81
+ ---
82
+ title: "Your Article Title"
83
+ description: "Generated from LaTeX"
84
+ ---
85
+
86
+ import Figure from '../components/Figure.astro';
87
+ import figure1 from '../assets/image/figure1.png';
88
+
89
+ ## Section with invisible anchor
90
+ <span id="introduction" style="position: absolute;"></span>
91
+
92
+ Here is some text with <span class="highlight">highlighted words</span>.
93
+
94
+ Reference to an interactive [equation](#equation-name).
95
+
96
+ Equation with KaTeX ID:
97
+ $$\htmlId{equation-name}{E = mc^2}$$
98
+
99
+ <Figure src={figure1} alt="Description" />
100
+ ```
101
+
102
+ ## ⚙️ Required Astro Configuration
103
+
104
+ To use equations with IDs, add to `astro.config.mjs`:
105
+
106
+ ```javascript
107
+ import rehypeKatex from 'rehype-katex';
108
+
109
+ export default defineConfig({
110
+ markdown: {
111
+ rehypePlugins: [
112
+ [rehypeKatex, { trust: true }], // ← Important for \htmlId{}
113
+ ],
114
+ },
115
+ });
116
+ ```
117
+
118
+ ## 🛠️ Prerequisites
119
+
120
+ - **Node.js** with ESM support
121
+ - **Pandoc** (`brew install pandoc`)
122
+ - **Astro** to use the generated MDX
123
+
124
+ ## 🎯 Technical Architecture
125
+
126
+ ### 4-Stage Pipeline
127
+
128
+ 1. **LaTeX Preprocessing** (`reference-preprocessor.mjs`)
129
+ - Cleanup of `\label{}` and `\ref{}`
130
+ - Conversion `\highlight{}` → CSS spans
131
+ - Removal of prefixes and problematic characters
132
+
133
+ 2. **Pandoc + Lua Filter** (`equation-ids.lua`)
134
+ - LaTeX → Markdown conversion with `gfm+tex_math_dollars+raw_html`
135
+ - Equation processing: `\label{eq:name}` → `\htmlId{name}{equation}`
136
+ - Automatic image extraction
137
+
138
+ 3. **Markdown Post-processing** (`post-processor.mjs`)
139
+ - KaTeX, Unicode, grouping commands cleanup
140
+ - Attribute correction with `:`
141
+ - Code snippet injection
142
+
143
+ 4. **MDX Conversion** (`mdx-converter.mjs`)
144
+ - Images transformation → `Figure`
145
+ - HTML span escaping correction
146
+ - Automatic imports generation
147
+ - MDX frontmatter
148
+
149
+ ## 📊 Conversion Statistics
150
+
151
+ For a typical scientific document:
152
+ - **87 labels** detected and processed
153
+ - **48 invisible anchors** created
154
+ - **13 highlight spans** with CSS class
155
+ - **4 equations** with `\htmlId{}` KaTeX
156
+ - **40 images** converted to components
157
+
158
+ ## ✅ Project Status
159
+
160
+ ### 🎉 **Complete Features**
161
+ - ✅ **LaTeX → MDX Pipeline**: Full end-to-end functional conversion
162
+ - ✅ **Cross-document references**: Perfectly functional internal links
163
+ - ✅ **Interactive equations**: KaTeX support with clickable IDs
164
+ - ✅ **Automatic styling**: Highlights and Astro components
165
+ - ✅ **Robustness**: Automatic cleanup of all escaping
166
+ - ✅ **Optimization**: Clean code without unnecessary elements
167
+
168
+ ### 🚀 **Production Ready**
169
+ The toolkit is now **100% operational** for converting complex scientific LaTeX documents to MDX/Astro with all advanced features (references, interactive equations, styling).
app/scripts/latex-importer/bib-cleaner.mjs ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
4
+ import { join, dirname, basename } from 'path';
5
+
6
+ /**
7
+ * Clean a BibTeX file by removing local file references and paths
8
+ * @param {string} inputBibFile - Path to the input .bib file
9
+ * @param {string} outputBibFile - Path to the output cleaned .bib file
10
+ * @returns {boolean} - Success status
11
+ */
12
+ export function cleanBibliography(inputBibFile, outputBibFile) {
13
+ if (!existsSync(inputBibFile)) {
14
+ console.log(' ⚠️ No bibliography file found:', inputBibFile);
15
+ return false;
16
+ }
17
+
18
+ console.log('📚 Cleaning bibliography...');
19
+ let bibContent = readFileSync(inputBibFile, 'utf8');
20
+
21
+ // Remove file paths and local references
22
+ bibContent = bibContent.replace(/file = \{[^}]+\}/g, '');
23
+
24
+ // Remove empty lines created by file removal
25
+ bibContent = bibContent.replace(/,\s*\n\s*\n/g, '\n\n');
26
+ bibContent = bibContent.replace(/,\s*\}/g, '\n}');
27
+
28
+ // Clean up double commas
29
+ bibContent = bibContent.replace(/,,/g, ',');
30
+
31
+ // Remove trailing commas before closing braces
32
+ bibContent = bibContent.replace(/,(\s*\n\s*)\}/g, '$1}');
33
+
34
+ writeFileSync(outputBibFile, bibContent);
35
+ console.log(` 📄 Clean bibliography saved: ${outputBibFile}`);
36
+
37
+ return true;
38
+ }
39
+
40
+ /**
41
+ * CLI for bibliography cleaning
42
+ */
43
+ function main() {
44
+ const args = process.argv.slice(2);
45
+
46
+ if (args.includes('--help') || args.includes('-h')) {
47
+ console.log(`
48
+ 📚 BibTeX Bibliography Cleaner
49
+
50
+ Usage:
51
+ node bib-cleaner.mjs [input.bib] [output.bib]
52
+ node bib-cleaner.mjs --input=input.bib --output=output.bib
53
+
54
+ Options:
55
+ --input=FILE Input .bib file
56
+ --output=FILE Output cleaned .bib file
57
+ --help, -h Show this help
58
+
59
+ Examples:
60
+ # Clean main.bib to clean.bib
61
+ node bib-cleaner.mjs main.bib clean.bib
62
+
63
+ # Using flags
64
+ node bib-cleaner.mjs --input=references.bib --output=clean-refs.bib
65
+ `);
66
+ process.exit(0);
67
+ }
68
+
69
+ let inputFile, outputFile;
70
+
71
+ // Parse command line arguments
72
+ if (args.length >= 2 && !args[0].startsWith('--')) {
73
+ // Positional arguments
74
+ inputFile = args[0];
75
+ outputFile = args[1];
76
+ } else {
77
+ // Named arguments
78
+ for (const arg of args) {
79
+ if (arg.startsWith('--input=')) {
80
+ inputFile = arg.split('=')[1];
81
+ } else if (arg.startsWith('--output=')) {
82
+ outputFile = arg.split('=')[1];
83
+ }
84
+ }
85
+ }
86
+
87
+ if (!inputFile || !outputFile) {
88
+ console.error('❌ Both input and output files are required');
89
+ console.log('Use --help for usage information');
90
+ process.exit(1);
91
+ }
92
+
93
+ const success = cleanBibliography(inputFile, outputFile);
94
+ if (success) {
95
+ console.log('🎉 Bibliography cleaning completed!');
96
+ } else {
97
+ process.exit(1);
98
+ }
99
+ }
100
+
101
+ // Run CLI if called directly
102
+ if (import.meta.url === `file://${process.argv[1]}`) {
103
+ main();
104
+ }
app/scripts/latex-importer/filters/equation-ids.lua ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ --[[
2
+ Pandoc Lua filter to add IDs to equations using KaTeX \htmlId syntax
3
+
4
+ This filter processes display math equations and inline math that contain
5
+ \label{} commands, and wraps them with \htmlId{clean-id}{content} for KaTeX.
6
+
7
+ Requirements:
8
+ - KaTeX renderer with trust: true option
9
+ - Equations with \label{} commands in LaTeX
10
+ --]]
11
+
12
+ -- Function to clean identifier strings (remove prefixes and colons)
13
+ function clean_identifier(id_str)
14
+ if id_str and type(id_str) == "string" then
15
+ -- Remove common prefixes and replace colons with dashes
16
+ local clean = id_str
17
+ :gsub("^(eq|equation):", "") -- Remove eq: prefix
18
+ :gsub(":", "-") -- Replace colons with dashes
19
+ :gsub("[^a-zA-Z0-9_-]", "-") -- Replace other problematic chars
20
+ :gsub("-+", "-") -- Collapse multiple dashes
21
+ :gsub("^-", "") -- Remove leading dash
22
+ :gsub("-$", "") -- Remove trailing dash
23
+
24
+ -- Ensure we don't have empty identifiers
25
+ if clean == "" then
26
+ clean = id_str:gsub(":", "-")
27
+ end
28
+
29
+ return clean
30
+ end
31
+ return id_str
32
+ end
33
+
34
+ -- Process Math elements (both inline and display)
35
+ function Math(el)
36
+ local math_content = el.text
37
+
38
+ -- Look for \label{...} commands in the math content
39
+ local label_match = math_content:match("\\label%{([^}]+)%}")
40
+
41
+ if label_match then
42
+ -- Clean the identifier
43
+ local clean_id = clean_identifier(label_match)
44
+
45
+ -- Remove the \label{} command from the math content
46
+ local clean_math = math_content:gsub("\\label%{[^}]+%}", "")
47
+
48
+ -- Clean up any extra whitespace or line breaks that might remain
49
+ clean_math = clean_math:gsub("%s*$", ""):gsub("^%s*", "")
50
+
51
+ -- Handle different equation environments appropriately
52
+ -- For align environments, preserve them as they work with KaTeX
53
+ local has_align = clean_math:match("\\begin%{align%}")
54
+
55
+ if has_align then
56
+ -- For align environments, we keep the structure and add ID as an attribute
57
+ -- KaTeX supports align environments natively
58
+ clean_math = clean_math:gsub("\\begin%{align%}", "\\begin{align}")
59
+ clean_math = clean_math:gsub("\\end%{align%}", "\\end{align}")
60
+ else
61
+ -- Remove other equation environments that don't work well with \htmlId
62
+ clean_math = clean_math:gsub("\\begin%{equation%}", ""):gsub("\\end%{equation%}", "")
63
+ clean_math = clean_math:gsub("\\begin%{equation%*%}", ""):gsub("\\end%{equation%*%}", "")
64
+ clean_math = clean_math:gsub("\\begin%{align%*%}", ""):gsub("\\end%{align%*%}", "")
65
+ end
66
+
67
+ -- Clean up any remaining whitespace
68
+ clean_math = clean_math:gsub("%s*$", ""):gsub("^%s*", "")
69
+
70
+ local new_math
71
+ if has_align then
72
+ -- For align environments, KaTeX doesn't support \htmlId with align
73
+ -- Instead, we add a special marker that the post-processor will convert to a span
74
+ -- This span will serve as an anchor for references
75
+ new_math = "%%ALIGN_ANCHOR_ID{" .. clean_id .. "}%%\n" .. clean_math
76
+ else
77
+ -- For other math, wrap with \htmlId{}
78
+ new_math = "\\htmlId{" .. clean_id .. "}{" .. clean_math .. "}"
79
+ end
80
+
81
+ -- Return new Math element with the updated content
82
+ return pandoc.Math(el.mathtype, new_math)
83
+ end
84
+
85
+ -- Return unchanged if no label found
86
+ return el
87
+ end
88
+
89
+ -- Optional: Process RawInline elements that might contain LaTeX math
90
+ function RawInline(el)
91
+ if el.format == "latex" or el.format == "tex" then
92
+ local content = el.text
93
+
94
+ -- Look for equation environments with labels
95
+ local label_match = content:match("\\label%{([^}]+)%}")
96
+
97
+ if label_match then
98
+ local clean_id = clean_identifier(label_match)
99
+
100
+ -- For raw LaTeX, we might need different handling
101
+ -- This is a simplified approach - adjust based on your needs
102
+ local clean_content = content:gsub("\\label%{[^}]+%}", "")
103
+
104
+ if clean_content:match("\\begin%{equation") or clean_content:match("\\begin%{align") then
105
+ -- For equation environments, we might need to wrap differently
106
+ -- This depends on how your KaTeX setup handles equation environments
107
+ return pandoc.RawInline(el.format, clean_content)
108
+ end
109
+ end
110
+ end
111
+
112
+ return el
113
+ end
114
+
115
+ -- Optional: Process RawBlock elements for display equations
116
+ function RawBlock(el)
117
+ if el.format == "latex" or el.format == "tex" then
118
+ local content = el.text
119
+
120
+ -- Look for equation environments with labels
121
+ local label_match = content:match("\\label%{([^}]+)%}")
122
+
123
+ if label_match then
124
+ local clean_id = clean_identifier(label_match)
125
+ local clean_content = content:gsub("\\label%{[^}]+%}", "")
126
+
127
+ -- For block equations, we might want to preserve the structure
128
+ -- but add the htmlId functionality
129
+ return pandoc.RawBlock(el.format, clean_content)
130
+ end
131
+ end
132
+
133
+ return el
134
+ end
app/scripts/latex-importer/index.mjs ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { join, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { copyFileSync } from 'fs';
6
+ import { convertLatexToMarkdown } from './latex-converter.mjs';
7
+ import { convertToMdx } from './mdx-converter.mjs';
8
+ import { cleanBibliography } from './bib-cleaner.mjs';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = dirname(__filename);
12
+
13
+ // Default configuration
14
+ const DEFAULT_INPUT = join(__dirname, 'input', 'main.tex');
15
+ const DEFAULT_OUTPUT = join(__dirname, 'output');
16
+ const ASTRO_CONTENT_PATH = join(__dirname, '..', '..', 'src', 'content', 'article.mdx');
17
+
18
+ function parseArgs() {
19
+ const args = process.argv.slice(2);
20
+ const config = {
21
+ input: DEFAULT_INPUT,
22
+ output: DEFAULT_OUTPUT,
23
+ clean: false,
24
+ bibOnly: false,
25
+ convertOnly: false,
26
+ mdx: false,
27
+ };
28
+
29
+ for (const arg of args) {
30
+ if (arg.startsWith('--input=')) {
31
+ config.input = arg.split('=')[1];
32
+ } else if (arg.startsWith('--output=')) {
33
+ config.output = arg.split('=')[1];
34
+ } else if (arg === '--clean') {
35
+ config.clean = true;
36
+ } else if (arg === '--bib-only') {
37
+ config.bibOnly = true;
38
+ } else if (arg === '--convert-only') {
39
+ config.convertOnly = true;
40
+ }
41
+ }
42
+
43
+ return config;
44
+ }
45
+
46
+ function showHelp() {
47
+ console.log(`
48
+ 🚀 LaTeX to Markdown Toolkit
49
+
50
+ Usage:
51
+ node index.mjs [options]
52
+
53
+ Options:
54
+ --input=PATH Input LaTeX file (default: input/main.tex)
55
+ --output=PATH Output directory (default: output/)
56
+ --clean Clean output directory before processing
57
+ --bib-only Only clean bibliography file
58
+ --convert-only Only convert LaTeX to Markdown (skip bib cleaning)
59
+ --help, -h Show this help
60
+
61
+ Examples:
62
+ # Full conversion with bibliography cleaning
63
+ node index.mjs --clean
64
+
65
+ # Only clean bibliography
66
+ node index.mjs --bib-only --input=paper.tex --output=clean/
67
+
68
+ # Only convert LaTeX (use existing clean bibliography)
69
+ node index.mjs --convert-only
70
+
71
+ # Custom paths
72
+ node index.mjs --input=../paper/main.tex --output=../results/ --clean
73
+ `);
74
+ }
75
+
76
+ function main() {
77
+ const args = process.argv.slice(2);
78
+
79
+ if (args.includes('--help') || args.includes('-h')) {
80
+ showHelp();
81
+ process.exit(0);
82
+ }
83
+
84
+ const config = parseArgs();
85
+
86
+ console.log('🚀 LaTeX to Markdown Toolkit');
87
+ console.log('==============================');
88
+
89
+ try {
90
+ if (config.bibOnly) {
91
+ // Only clean bibliography
92
+ console.log('📚 Bibliography cleaning mode');
93
+ const bibInput = config.input.replace('.tex', '.bib');
94
+ const bibOutput = join(config.output, 'main.bib');
95
+
96
+ cleanBibliography(bibInput, bibOutput);
97
+ console.log('🎉 Bibliography cleaning completed!');
98
+
99
+ } else if (config.convertOnly) {
100
+ // Only convert LaTeX
101
+ console.log('📄 Conversion only mode');
102
+ convertLatexToMarkdown(config.input, config.output);
103
+
104
+ } else {
105
+ // Full workflow
106
+ console.log('🔄 Full conversion workflow');
107
+ convertLatexToMarkdown(config.input, config.output);
108
+
109
+ // Convert to MDX if requested
110
+ const markdownFile = join(config.output, 'main.md');
111
+ const mdxFile = join(config.output, 'main.mdx');
112
+
113
+ console.log('📝 Converting Markdown to MDX...');
114
+ convertToMdx(markdownFile, mdxFile);
115
+
116
+ // Copy MDX to Astro content directory
117
+ console.log('📋 Copying MDX to Astro content directory...');
118
+ try {
119
+ copyFileSync(mdxFile, ASTRO_CONTENT_PATH);
120
+ console.log(` ✅ Copied to ${ASTRO_CONTENT_PATH}`);
121
+ } catch (error) {
122
+ console.warn(` ⚠️ Failed to copy MDX to Astro: ${error.message}`);
123
+ }
124
+ }
125
+
126
+ } catch (error) {
127
+ console.error('❌ Error:', error.message);
128
+ process.exit(1);
129
+ }
130
+ }
131
+
132
+ // Export functions for use as module
133
+ export { convertLatexToMarkdown, cleanBibliography };
134
+
135
+ // Run CLI if called directly
136
+ if (import.meta.url === `file://${process.argv[1]}`) {
137
+ main();
138
+ }
app/scripts/latex-importer/latex-converter.mjs ADDED
@@ -0,0 +1,330 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from 'child_process';
4
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
5
+ import { join, dirname, basename } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import { cleanBibliography } from './bib-cleaner.mjs';
8
+ import { postProcessMarkdown } from './post-processor.mjs';
9
+ import { preprocessLatexReferences } from './reference-preprocessor.mjs';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+
14
+ // Configuration
15
+ const DEFAULT_INPUT = join(__dirname, 'input', 'main.tex');
16
+ const DEFAULT_OUTPUT = join(__dirname, 'output');
17
+
18
+ function parseArgs() {
19
+ const args = process.argv.slice(2);
20
+ const config = {
21
+ input: DEFAULT_INPUT,
22
+ output: DEFAULT_OUTPUT,
23
+ clean: false
24
+ };
25
+
26
+ for (const arg of args) {
27
+ if (arg.startsWith('--input=')) {
28
+ config.input = arg.split('=')[1];
29
+ } else if (arg.startsWith('--output=')) {
30
+ config.output = arg.split('=')[1];
31
+ } else if (arg === '--clean') {
32
+ config.clean = true;
33
+ }
34
+ }
35
+
36
+ return config;
37
+ }
38
+
39
+ function ensureDirectory(dir) {
40
+ if (!existsSync(dir)) {
41
+ mkdirSync(dir, { recursive: true });
42
+ }
43
+ }
44
+
45
+ function cleanDirectory(dir) {
46
+ if (existsSync(dir)) {
47
+ execSync(`rm -rf "${dir}"/*`, { stdio: 'inherit' });
48
+ }
49
+ }
50
+
51
+ function preprocessLatexFile(inputFile, outputDir) {
52
+ const inputDir = dirname(inputFile);
53
+ const tempFile = join(outputDir, 'temp_main.tex');
54
+
55
+ console.log('🔄 Preprocessing LaTeX file to resolve \\input commands...');
56
+
57
+ let content = readFileSync(inputFile, 'utf8');
58
+
59
+ // Remove problematic commands that break pandoc
60
+ console.log('🧹 Cleaning problematic LaTeX constructs...');
61
+
62
+ // Fix citation issues - but not in citation keys
63
+ content = content.replace(/\$p_0\$(?![A-Za-z])/g, 'p0');
64
+
65
+ // Convert complex math environments to simple delimiters
66
+ content = content.replace(/\$\$\\begin\{equation\*\}/g, '$$');
67
+ content = content.replace(/\\end\{equation\*\}\$\$/g, '$$');
68
+ content = content.replace(/\\begin\{equation\*\}/g, '$$');
69
+ content = content.replace(/\\end\{equation\*\}/g, '$$');
70
+ // Keep align environments intact for KaTeX support
71
+ // Protect align environments by temporarily replacing them before cleaning & operators
72
+ const alignBlocks = [];
73
+ content = content.replace(/\\begin\{align\}([\s\S]*?)\\end\{align\}/g, (match, alignContent) => {
74
+ alignBlocks.push(match);
75
+ return `__ALIGN_BLOCK_${alignBlocks.length - 1}__`;
76
+ });
77
+
78
+ // Now remove & operators from non-align content (outside align environments)
79
+ content = content.replace(/&=/g, '=');
80
+ content = content.replace(/&/g, '');
81
+
82
+ // Restore align blocks with their & operators intact
83
+ alignBlocks.forEach((block, index) => {
84
+ content = content.replace(`__ALIGN_BLOCK_${index}__`, block);
85
+ });
86
+
87
+ // Convert LaTeX citations to Pandoc format
88
+ content = content.replace(/\\cite[tp]?\{([^}]+)\}/g, (match, citations) => {
89
+ // Handle multiple citations separated by commas - all become simple @citations
90
+ return citations.split(',').map(cite => `@${cite.trim()}`).join(', ');
91
+ });
92
+
93
+ // Handle complex \textsc with nested math - extract and simplify (but not in command definitions)
94
+ content = content.replace(/\\textsc\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g, (match, content_inside, offset) => {
95
+ // Skip if this is inside a \newcommand or similar definition
96
+ const before = content.substring(Math.max(0, offset - 50), offset);
97
+ if (before.includes('\\newcommand') || before.includes('\\renewcommand') || before.includes('\\def')) {
98
+ return match; // Keep original
99
+ }
100
+
101
+ // Remove math delimiters inside textsc for simplification
102
+ const simplified = content_inside.replace(/\\\([^)]+\\\)/g, 'MATHEXPR');
103
+ return `\\text{${simplified}}`;
104
+ });
105
+
106
+ // Remove complex custom commands that pandoc can't handle
107
+ content = content.replace(/\\input\{snippets\/[^}]+\}/g, '% Code snippet removed');
108
+
109
+ // Find all \input{} commands (but skip commented ones)
110
+ const inputRegex = /^([^%]*?)\\input\{([^}]+)\}/gm;
111
+ let match;
112
+
113
+ while ((match = inputRegex.exec(content)) !== null) {
114
+ const beforeInput = match[1];
115
+ const inputPath = match[2];
116
+
117
+ // Skip if the \input is commented (% appears before \input on the line)
118
+ if (beforeInput.includes('%')) {
119
+ continue;
120
+ }
121
+ let fullPath;
122
+
123
+ // Skip only problematic files, let Pandoc handle macros
124
+ if (inputPath.includes('snippets/')) {
125
+ console.log(` Skipping: ${inputPath}`);
126
+ content = content.replace(`\\input{${inputPath}}`, `% Skipped: ${inputPath}`);
127
+ continue;
128
+ }
129
+
130
+ // Handle paths with or without .tex extension
131
+ if (inputPath.endsWith('.tex')) {
132
+ fullPath = join(inputDir, inputPath);
133
+ } else {
134
+ fullPath = join(inputDir, inputPath + '.tex');
135
+ }
136
+
137
+ if (existsSync(fullPath)) {
138
+ console.log(` Including: ${inputPath}`);
139
+ let includedContent = readFileSync(fullPath, 'utf8');
140
+
141
+ // Clean included content too
142
+ includedContent = includedContent.replace(/\$p_0\$/g, 'p0');
143
+ includedContent = includedContent.replace(/\\input\{snippets\/[^}]+\}/g, '% Code snippet removed');
144
+
145
+ // Handle complex \textsc in included content
146
+ includedContent = includedContent.replace(/\\textsc\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g, (match, content_inside, offset) => {
147
+ // Skip if this is inside a \newcommand or similar definition
148
+ const before = includedContent.substring(Math.max(0, offset - 50), offset);
149
+ if (before.includes('\\newcommand') || before.includes('\\renewcommand') || before.includes('\\def')) {
150
+ return match; // Keep original
151
+ }
152
+
153
+ const simplified = content_inside.replace(/\\\([^)]+\\\)/g, 'MATHEXPR');
154
+ return `\\text{${simplified}}`;
155
+ });
156
+
157
+ // Apply same align-preserving logic to included content
158
+ const alignBlocksIncluded = [];
159
+ includedContent = includedContent.replace(/\\begin\{align\}([\s\S]*?)\\end\{align\}/g, (match, alignContent) => {
160
+ alignBlocksIncluded.push(match);
161
+ return `__ALIGN_BLOCK_${alignBlocksIncluded.length - 1}__`;
162
+ });
163
+
164
+ // Remove alignment operators from non-align content in included files
165
+ includedContent = includedContent.replace(/&=/g, '=');
166
+ includedContent = includedContent.replace(/&/g, '');
167
+
168
+ // Restore align blocks with their & operators intact
169
+ alignBlocksIncluded.forEach((block, index) => {
170
+ includedContent = includedContent.replace(`__ALIGN_BLOCK_${index}__`, block);
171
+ });
172
+
173
+ // Convert math environments in included content
174
+ includedContent = includedContent.replace(/\$\$\\begin\{equation\*\}/g, '$$');
175
+ includedContent = includedContent.replace(/\\end\{equation\*\}\$\$/g, '$$');
176
+ includedContent = includedContent.replace(/\\begin\{equation\*\}/g, '$$');
177
+ includedContent = includedContent.replace(/\\end\{equation\*\}/g, '$$');
178
+
179
+ // Convert citations in included content
180
+ includedContent = includedContent.replace(/\\cite[tp]?\{([^}]+)\}/g, (match, citations) => {
181
+ return citations.split(',').map(cite => `@${cite.trim()}`).join(', ');
182
+ });
183
+
184
+ content = content.replace(`\\input{${inputPath}}`, includedContent);
185
+ } else {
186
+ console.log(` ⚠️ File not found: ${fullPath} (skipping)`);
187
+ content = content.replace(`\\input{${inputPath}}`, `% File not found: ${inputPath}`);
188
+ }
189
+ }
190
+
191
+ // Apply reference preprocessing AFTER input inclusion to ensure all references are captured
192
+ console.log('🔧 Preprocessing LaTeX references for MDX compatibility...');
193
+ const referenceResult = preprocessLatexReferences(content);
194
+ content = referenceResult.content;
195
+
196
+ // Write the preprocessed file
197
+ writeFileSync(tempFile, content);
198
+ return tempFile;
199
+ }
200
+
201
+ function processBibliography(inputFile, outputDir) {
202
+ const bibFile = join(dirname(inputFile), 'main.bib');
203
+ const outputBibFile = join(outputDir, 'main.bib');
204
+
205
+ if (!existsSync(bibFile)) {
206
+ console.log(' ⚠️ No bibliography file found');
207
+ return null;
208
+ }
209
+
210
+ const success = cleanBibliography(bibFile, outputBibFile);
211
+ return success ? outputBibFile : null;
212
+ }
213
+
214
+ export function convertLatexToMarkdown(inputFile, outputDir) {
215
+ console.log('🚀 Simple LaTeX to Markdown Converter');
216
+ console.log(`📁 Input: ${inputFile}`);
217
+ console.log(`📁 Output: ${outputDir}`);
218
+
219
+ // Check if input file exists
220
+ if (!existsSync(inputFile)) {
221
+ console.error(`❌ Input file not found: ${inputFile}`);
222
+ process.exit(1);
223
+ }
224
+
225
+ // Ensure output directory exists
226
+ ensureDirectory(outputDir);
227
+
228
+ try {
229
+ // Check if pandoc is available
230
+ execSync('pandoc --version', { stdio: 'pipe' });
231
+ } catch (error) {
232
+ console.error('❌ Pandoc not found. Please install it: brew install pandoc');
233
+ process.exit(1);
234
+ }
235
+
236
+ // Clean and copy bibliography
237
+ const cleanBibFile = processBibliography(inputFile, outputDir);
238
+
239
+ // Preprocess the LaTeX file to resolve \input commands
240
+ const preprocessedFile = preprocessLatexFile(inputFile, outputDir);
241
+
242
+ const inputFileName = basename(inputFile, '.tex');
243
+ const outputFile = join(outputDir, `${inputFileName}.md`);
244
+
245
+ try {
246
+ console.log('📄 Converting with Pandoc...');
247
+
248
+ // Enhanced pandoc conversion - use tex_math_dollars for KaTeX compatibility
249
+ const bibOption = cleanBibFile ? `--bibliography="${cleanBibFile}"` : '';
250
+
251
+ // Use gfm+tex_math_dollars for simple $ delimiters compatible with KaTeX
252
+ const mediaDir = join(outputDir, 'assets', 'image');
253
+ ensureDirectory(mediaDir);
254
+ const inputDir = dirname(inputFile);
255
+ const equationFilterPath = join(__dirname, 'filters', 'equation-ids.lua');
256
+ const pandocCommand = `pandoc "${preprocessedFile}" -f latex+latex_macros -t gfm+tex_math_dollars+raw_html --shift-heading-level-by=1 --wrap=none ${bibOption} --extract-media="${mediaDir}" --resource-path="${inputDir}" --lua-filter="${equationFilterPath}" -o "${outputFile}"`;
257
+
258
+ console.log(` Running: ${pandocCommand}`);
259
+ execSync(pandocCommand, { stdio: 'pipe' });
260
+
261
+ // Clean up temp file
262
+ execSync(`rm "${preprocessedFile}"`, { stdio: 'pipe' });
263
+
264
+ // Post-processing to fix KaTeX incompatible constructions
265
+ let markdownContent = readFileSync(outputFile, 'utf8');
266
+
267
+ // Use modular post-processor with code injection
268
+ markdownContent = postProcessMarkdown(markdownContent, inputDir);
269
+
270
+ writeFileSync(outputFile, markdownContent);
271
+
272
+ console.log(`✅ Conversion completed: ${outputFile}`);
273
+
274
+ // Show file size
275
+ const stats = execSync(`wc -l "${outputFile}"`, { encoding: 'utf8' });
276
+ const lines = stats.trim().split(' ')[0];
277
+ console.log(`📊 Result: ${lines} lines written`);
278
+
279
+ } catch (error) {
280
+ console.error('❌ Pandoc conversion failed:');
281
+ console.error(error.message);
282
+ // Clean up temp file on error
283
+ try {
284
+ execSync(`rm "${preprocessedFile}"`, { stdio: 'pipe' });
285
+ } catch { }
286
+ process.exit(1);
287
+ }
288
+ }
289
+
290
+ function main() {
291
+ const config = parseArgs();
292
+
293
+ if (config.clean) {
294
+ console.log('🧹 Cleaning output directory...');
295
+ cleanDirectory(config.output);
296
+ }
297
+
298
+ convertLatexToMarkdown(config.input, config.output);
299
+
300
+ console.log('🎉 Simple conversion completed!');
301
+ }
302
+
303
+ // Show help if requested
304
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
305
+ console.log(`
306
+ 🚀 Simple LaTeX to Markdown Converter
307
+
308
+ Usage:
309
+ node scripts/simple-latex-to-markdown.mjs [options]
310
+
311
+ Options:
312
+ --input=PATH Input LaTeX file (default: latex-converter/input-example/main.tex)
313
+ --output=PATH Output directory (default: output/)
314
+ --clean Clean output directory before conversion
315
+ --help, -h Show this help
316
+
317
+ Examples:
318
+ # Basic conversion
319
+ node scripts/simple-latex-to-markdown.mjs
320
+
321
+ # Custom paths
322
+ node scripts/simple-latex-to-markdown.mjs --input=my-paper.tex --output=converted/
323
+
324
+ # Clean output first
325
+ node scripts/simple-latex-to-markdown.mjs --clean
326
+ `);
327
+ process.exit(0);
328
+ }
329
+
330
+ main();
app/scripts/latex-importer/mdx-converter.mjs ADDED
@@ -0,0 +1,896 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
4
+ import { join, dirname, basename, extname } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+ import { extractAndGenerateFrontmatter } from './metadata-extractor.mjs';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+
11
+ // Configuration
12
+ const DEFAULT_INPUT = join(__dirname, 'output', 'main.md');
13
+ const DEFAULT_OUTPUT = join(__dirname, 'output', 'main.mdx');
14
+
15
+ function parseArgs() {
16
+ const args = process.argv.slice(2);
17
+ const config = {
18
+ input: DEFAULT_INPUT,
19
+ output: DEFAULT_OUTPUT,
20
+ };
21
+
22
+ for (const arg of args) {
23
+ if (arg.startsWith('--input=')) {
24
+ config.input = arg.substring('--input='.length);
25
+ } else if (arg.startsWith('--output=')) {
26
+ config.output = arg.substring('--output='.length);
27
+ } else if (arg === '--help' || arg === '-h') {
28
+ console.log(`
29
+ 📝 Markdown to MDX Converter
30
+
31
+ Usage:
32
+ node mdx-converter.mjs [options]
33
+
34
+ Options:
35
+ --input=PATH Input Markdown file (default: ${DEFAULT_INPUT})
36
+ --output=PATH Output MDX file (default: ${DEFAULT_OUTPUT})
37
+ --help, -h Show this help
38
+
39
+ Examples:
40
+ # Basic conversion
41
+ node mdx-converter.mjs
42
+
43
+ # Custom paths
44
+ node mdx-converter.mjs --input=article.md --output=article.mdx
45
+ `);
46
+ process.exit(0);
47
+ } else if (!config.input) {
48
+ config.input = arg;
49
+ } else if (!config.output) {
50
+ config.output = arg;
51
+ }
52
+ }
53
+ return config;
54
+ }
55
+
56
+ /**
57
+ * Modular MDX post-processing functions for Astro compatibility
58
+ * Each function handles a specific type of transformation
59
+ */
60
+
61
+ /**
62
+ * Track which Astro components are used during transformations
63
+ */
64
+ const usedComponents = new Set();
65
+
66
+ /**
67
+ * Track individual image imports needed
68
+ */
69
+ const imageImports = new Map(); // src -> varName
70
+
71
+ /**
72
+ * Add required component imports to the frontmatter
73
+ * @param {string} content - MDX content
74
+ * @returns {string} - Content with component imports
75
+ */
76
+ /**
77
+ * Generate a variable name from image path
78
+ * @param {string} src - Image source path
79
+ * @returns {string} - Valid variable name
80
+ */
81
+ function generateImageVarName(src) {
82
+ // Extract filename without extension and make it a valid JS variable
83
+ const filename = src.split('/').pop().replace(/\.[^.]+$/, '');
84
+ return filename.replace(/[^a-zA-Z0-9]/g, '_').replace(/^[0-9]/, 'img_$&');
85
+ }
86
+
87
+ function addComponentImports(content) {
88
+ console.log(' 📦 Adding component and image imports...');
89
+
90
+ let imports = [];
91
+
92
+ // Add component imports
93
+ if (usedComponents.size > 0) {
94
+ const componentImports = Array.from(usedComponents)
95
+ .map(component => `import ${component} from '../components/${component}.astro';`);
96
+ imports.push(...componentImports);
97
+ console.log(` ✅ Importing components: ${Array.from(usedComponents).join(', ')}`);
98
+ }
99
+
100
+ // Add image imports
101
+ if (imageImports.size > 0) {
102
+ const imageImportStatements = Array.from(imageImports.entries())
103
+ .map(([src, varName]) => `import ${varName} from '${src}';`);
104
+ imports.push(...imageImportStatements);
105
+ console.log(` ✅ Importing ${imageImports.size} image(s)`);
106
+ }
107
+
108
+ if (imports.length === 0) {
109
+ console.log(' ℹ️ No imports needed');
110
+ return content;
111
+ }
112
+
113
+ const importBlock = imports.join('\n');
114
+
115
+ // Insert imports after frontmatter
116
+ const frontmatterEnd = content.indexOf('---', 3) + 3;
117
+ if (frontmatterEnd > 2) {
118
+ return content.slice(0, frontmatterEnd) + '\n\n' + importBlock + '\n' + content.slice(frontmatterEnd);
119
+ } else {
120
+ // No frontmatter, add at beginning
121
+ return importBlock + '\n\n' + content;
122
+ }
123
+ }
124
+
125
+
126
+ /**
127
+ * Convert grouped figures (subfigures) to MultiFigure components
128
+ * @param {string} content - MDX content
129
+ * @returns {string} - Content with MultiFigure components for grouped figures
130
+ */
131
+ function convertSubfiguresToMultiFigure(content) {
132
+ console.log(' 🖼️✨ Converting subfigures to MultiFigure components...');
133
+
134
+ let convertedCount = 0;
135
+
136
+ // Pattern to match: <figure> containing multiple <figure> elements with a global caption
137
+ // This matches the LaTeX subfigure pattern that gets converted by Pandoc
138
+ const subfigureGroupPattern = /<figure>\s*((?:<figure>[\s\S]*?<\/figure>\s*){2,})<figcaption>([\s\S]*?)<\/figcaption>\s*<\/figure>/g;
139
+
140
+ const convertedContent = content.replace(subfigureGroupPattern, (match, figuresMatch, globalCaption) => {
141
+ convertedCount++;
142
+
143
+ // Extract individual figures within the group
144
+ // This pattern is more flexible to handle variations in HTML structure
145
+ const individualFigurePattern = /<figure>\s*<img src="([^"]*)"[^>]*\/>\s*<p>&lt;span id="([^"]*)"[^&]*&gt;&lt;\/span&gt;<\/p>\s*<figcaption>([\s\S]*?)<\/figcaption>\s*<\/figure>/g;
146
+
147
+ const images = [];
148
+ let figureMatch;
149
+
150
+ while ((figureMatch = individualFigurePattern.exec(figuresMatch)) !== null) {
151
+ const [, src, id, caption] = figureMatch;
152
+
153
+ // Clean the source path (similar to existing transformImages function)
154
+ const cleanSrc = src.replace(/.*\/output\/assets\//, './assets/')
155
+ .replace(/\/Users\/[^\/]+\/[^\/]+\/[^\/]+\/[^\/]+\/[^\/]+\/app\/scripts\/latex-to-markdown\/output\/assets\//, './assets/');
156
+
157
+ // Clean caption text (remove HTML, normalize whitespace)
158
+ const cleanCaption = caption
159
+ .replace(/<[^>]*>/g, '')
160
+ .replace(/\n/g, ' ')
161
+ .replace(/\s+/g, ' ')
162
+ .replace(/'/g, "\\'")
163
+ .trim();
164
+
165
+ // Generate alt text from caption
166
+ const altText = cleanCaption.length > 100
167
+ ? cleanCaption.substring(0, 100) + '...'
168
+ : cleanCaption;
169
+
170
+ // Generate variable name for import
171
+ const varName = generateImageVarName(cleanSrc);
172
+ imageImports.set(cleanSrc, varName);
173
+
174
+ images.push({
175
+ src: varName,
176
+ alt: altText,
177
+ caption: cleanCaption,
178
+ id: id
179
+ });
180
+ }
181
+
182
+ // Clean global caption
183
+ const cleanGlobalCaption = globalCaption
184
+ .replace(/<[^>]*>/g, '')
185
+ .replace(/\n/g, ' ')
186
+ .replace(/\s+/g, ' ')
187
+ .replace(/'/g, "\\'")
188
+ .trim();
189
+
190
+ // Mark MultiFigure component as used
191
+ usedComponents.add('MultiFigure');
192
+
193
+ // Determine layout based on number of images
194
+ let layout = 'auto';
195
+ if (images.length === 2) layout = '2-column';
196
+ else if (images.length === 3) layout = '3-column';
197
+ else if (images.length === 4) layout = '4-column';
198
+
199
+ // Generate MultiFigure component
200
+ const imagesJson = images.map(img =>
201
+ ` {\n src: ${img.src},\n alt: "${img.alt}",\n caption: "${img.caption}",\n id: "${img.id}"\n }`
202
+ ).join(',\n');
203
+
204
+ return `<MultiFigure
205
+ images={[
206
+ ${imagesJson}
207
+ ]}
208
+ layout="${layout}"
209
+ zoomable
210
+ downloadable
211
+ caption="${cleanGlobalCaption}"
212
+ />`;
213
+ });
214
+
215
+ if (convertedCount > 0) {
216
+ console.log(` ✅ Converted ${convertedCount} subfigure group(s) to MultiFigure component(s)`);
217
+ } else {
218
+ console.log(' ℹ️ No subfigure groups found');
219
+ }
220
+
221
+ return convertedContent;
222
+ }
223
+
224
+ /**
225
+ * Transform images to Figure components
226
+ * @param {string} content - MDX content
227
+ * @returns {string} - Content with Figure components
228
+ */
229
+ /**
230
+ * Create Figure component with import
231
+ * @param {string} src - Clean image source
232
+ * @param {string} alt - Alt text
233
+ * @param {string} id - Element ID
234
+ * @param {string} caption - Figure caption
235
+ * @param {string} width - Optional width
236
+ * @returns {string} - Figure component markup
237
+ */
238
+ function createFigureComponent(src, alt = '', id = '', caption = '', width = '') {
239
+ const varName = generateImageVarName(src);
240
+ imageImports.set(src, varName);
241
+ usedComponents.add('Figure');
242
+
243
+ const props = [];
244
+ props.push(`src={${varName}}`);
245
+ props.push('zoomable');
246
+ props.push('downloadable');
247
+ if (id) props.push(`id="${id}"`);
248
+ props.push('layout="fixed"');
249
+ if (alt) props.push(`alt="${alt}"`);
250
+ if (caption) props.push(`caption={'${caption}'}`);
251
+
252
+ return `<Figure\n ${props.join('\n ')}\n/>`;
253
+ }
254
+
255
+ function transformImages(content) {
256
+ console.log(' 🖼️ Transforming images to Figure components with imports...');
257
+
258
+ let hasImages = false;
259
+
260
+ // Helper function to clean source paths
261
+ const cleanSrcPath = (src) => {
262
+ return src.replace(/.*\/output\/assets\//, './assets/')
263
+ .replace(/\/Users\/[^\/]+\/[^\/]+\/[^\/]+\/[^\/]+\/[^\/]+\/app\/scripts\/latex-to-markdown\/output\/assets\//, './assets/');
264
+ };
265
+
266
+ // Helper to clean caption text
267
+ const cleanCaption = (caption) => {
268
+ return caption
269
+ .replace(/<[^>]*>/g, '') // Remove HTML tags
270
+ .replace(/\n/g, ' ') // Replace newlines with spaces
271
+ .replace(/\r/g, ' ') // Replace carriage returns with spaces
272
+ .replace(/\s+/g, ' ') // Replace multiple spaces with single space
273
+ .replace(/'/g, "\\'") // Escape quotes
274
+ .trim(); // Trim whitespace
275
+ };
276
+
277
+ // Helper to clean alt text
278
+ const cleanAltText = (alt, maxLength = 100) => {
279
+ const cleaned = alt
280
+ .replace(/<[^>]*>/g, '') // Remove HTML tags
281
+ .replace(/\n/g, ' ') // Replace newlines with spaces
282
+ .replace(/\r/g, ' ') // Replace carriage returns with spaces
283
+ .replace(/\s+/g, ' ') // Replace multiple spaces with single space
284
+ .trim(); // Trim whitespace
285
+
286
+ return cleaned.length > maxLength
287
+ ? cleaned.substring(0, maxLength) + '...'
288
+ : cleaned;
289
+ };
290
+
291
+ // 1. Transform complex HTML figures with style attributes
292
+ content = content.replace(
293
+ /<figure id="([^"]*)">\s*<img src="([^"]*)"(?:\s+style="([^"]*)")?\s*\/>\s*<figcaption>\s*(.*?)\s*<\/figcaption>\s*<\/figure>/gs,
294
+ (match, id, src, style, caption) => {
295
+ const cleanSrc = cleanSrcPath(src);
296
+ const cleanCap = cleanCaption(caption);
297
+ const altText = cleanAltText(cleanCap);
298
+ hasImages = true;
299
+
300
+ return createFigureComponent(cleanSrc, altText, id, cleanCap);
301
+ }
302
+ );
303
+
304
+ // 2. Transform standalone img tags with style
305
+ content = content.replace(
306
+ /<img src="([^"]*)"(?:\s+style="([^"]*)")?\s*(?:alt="([^"]*)")?\s*\/>/g,
307
+ (match, src, style, alt) => {
308
+ const cleanSrc = cleanSrcPath(src);
309
+ const cleanAlt = cleanAltText(alt || 'Figure');
310
+ hasImages = true;
311
+
312
+ return createFigureComponent(cleanSrc, cleanAlt);
313
+ }
314
+ );
315
+
316
+ // 3. Transform images within wrapfigure divs
317
+ content = content.replace(
318
+ /<div class="wrapfigure">\s*r[\d.]+\s*<img src="([^"]*)"[^>]*\/>\s*<\/div>/gs,
319
+ (match, src) => {
320
+ const cleanSrc = cleanSrcPath(src);
321
+ hasImages = true;
322
+
323
+ return createFigureComponent(cleanSrc, 'Figure');
324
+ }
325
+ );
326
+
327
+ // 4. Transform simple HTML figure/img without style
328
+ content = content.replace(
329
+ /<figure id="([^"]*)">\s*<img src="([^"]*)" \/>\s*<figcaption>\s*(.*?)\s*<\/figcaption>\s*<\/figure>/gs,
330
+ (match, id, src, caption) => {
331
+ const cleanSrc = cleanSrcPath(src);
332
+ const cleanCap = cleanCaption(caption);
333
+ const altText = cleanAltText(cleanCap);
334
+ hasImages = true;
335
+
336
+ return createFigureComponent(cleanSrc, altText, id, cleanCap);
337
+ }
338
+ );
339
+
340
+ // 5. Clean up figures with minipage divs
341
+ content = content.replace(
342
+ /<figure id="([^"]*)">\s*<div class="minipage">\s*<img src="([^"]*)"[^>]*\/>\s*<\/div>\s*<figcaption[^>]*>(.*?)<\/figcaption>\s*<\/figure>/gs,
343
+ (match, id, src, caption) => {
344
+ const cleanSrc = cleanSrcPath(src);
345
+ const cleanCap = cleanCaption(caption);
346
+ const altText = cleanAltText(cleanCap);
347
+ hasImages = true;
348
+
349
+ return createFigureComponent(cleanSrc, altText, id, cleanCap);
350
+ }
351
+ );
352
+
353
+ // 6. Transform Pandoc-style images: ![alt](src){#id attr="value"}
354
+ content = content.replace(
355
+ /!\[([^\]]*)\]\(([^)]+)\)(?:\{([^}]+)\})?/g,
356
+ (match, alt, src, attributes) => {
357
+ const cleanSrc = cleanSrcPath(src);
358
+ const cleanAlt = cleanAltText(alt || 'Figure');
359
+ hasImages = true;
360
+
361
+ let id = '';
362
+ if (attributes) {
363
+ const idMatch = attributes.match(/#([\w-]+)/);
364
+ if (idMatch) id = idMatch[1];
365
+ }
366
+
367
+ return createFigureComponent(cleanSrc, cleanAlt, id);
368
+ }
369
+ );
370
+
371
+ if (hasImages) {
372
+ console.log(' ✅ Figure components with imports will be created');
373
+ }
374
+
375
+ return content;
376
+ }
377
+
378
+ /**
379
+ * Transform HTML spans with style attributes to appropriate components
380
+ * @param {string} content - MDX content
381
+ * @returns {string} - Content with transformed spans
382
+ */
383
+ function transformStyledSpans(content) {
384
+ console.log(' 🎨 Transforming styled spans...');
385
+
386
+ // Transform HTML spans with style attributes
387
+ content = content.replace(
388
+ /<span style="color: ([^"]+)">(.*?)<\/span>/g,
389
+ (match, color, text) => {
390
+ // Map colors to semantic classes or components
391
+ const colorMap = {
392
+ 'hf2': 'text-hf-secondary',
393
+ 'hf1': 'text-hf-primary'
394
+ };
395
+
396
+ const className = colorMap[color] || `text-${color}`;
397
+ return `<span class="${className}">${text}</span>`;
398
+ }
399
+ );
400
+
401
+ // Transform markdown spans with style attributes: [text]{style="color: color"}
402
+ content = content.replace(
403
+ /\[([^\]]+)\]\{style="color: ([^"]+)"\}/g,
404
+ (match, text, color) => {
405
+ // Map colors to semantic classes or components
406
+ const colorMap = {
407
+ 'hf2': 'text-hf-secondary',
408
+ 'hf1': 'text-hf-primary'
409
+ };
410
+
411
+ const className = colorMap[color] || `text-${color}`;
412
+ return `<span class="${className}">${text}</span>`;
413
+ }
414
+ );
415
+
416
+ return content;
417
+ }
418
+
419
+ /**
420
+ * Transform reference links to proper Astro internal links
421
+ * @param {string} content - MDX content
422
+ * @returns {string} - Content with transformed links
423
+ */
424
+ function fixHtmlEscaping(content) {
425
+ console.log(' 🔧 Fixing HTML escaping in spans...');
426
+
427
+ let fixedCount = 0;
428
+
429
+ // Pattern 1: \<span id="..." style="..."\>\</span\>
430
+ content = content.replace(/\\<span id="([^"]*)" style="([^"]*)"\\>\\<\/span\\>/g, (match, id, style) => {
431
+ fixedCount++;
432
+ // Fix common style issues like "position- absolute;" -> "position: absolute;"
433
+ const cleanStyle = style.replace('position- absolute;', 'position: absolute;');
434
+ return `<span id="${id}" style="${cleanStyle}"></span>`;
435
+ });
436
+
437
+ // Pattern 2: \<span class="..."\>...\</span\>
438
+ content = content.replace(/\\<span class="([^"]*)"\\>([^\\]+)\\<\/span\\>/g, (match, className, text) => {
439
+ fixedCount++;
440
+ // Remove numbering like (1), (2), (3) from highlight spans
441
+ let cleanText = text;
442
+ if (className === 'highlight') {
443
+ cleanText = text.replace(/^\(\d+\)\s*/, '');
444
+ }
445
+ return `<span class="${className}">${cleanText}</span>`;
446
+ });
447
+
448
+ // Pattern 3: HTML-encoded spans in paragraph tags
449
+ // <p>&lt;span id="..." style="..."&gt;&lt;/span&gt;</p>
450
+ content = content.replace(/<p>&lt;span id="([^"]*)" style="([^"]*)"&gt;&lt;\/span&gt;<\/p>/g, (match, id, style) => {
451
+ fixedCount++;
452
+ // Fix common style issues like "position- absolute;" -> "position: absolute;"
453
+ const cleanStyle = style.replace('position- absolute;', 'position: absolute;');
454
+ return `<span id="${id}" style="${cleanStyle}"></span>`;
455
+ });
456
+
457
+ // Pattern 4: HTML-encoded spans with class in paragraph tags
458
+ // <p>&lt;span class="..."&gt;...&lt;/span&gt;</p>
459
+ content = content.replace(/<p>&lt;span class="([^"]*)"&gt;([^&]*)&lt;\/span&gt;<\/p>/g, (match, className, text) => {
460
+ fixedCount++;
461
+ // Remove numbering like (1), (2), (3) from highlight spans
462
+ let cleanText = text;
463
+ if (className === 'highlight') {
464
+ cleanText = text.replace(/^\(\d+\)\s*/, '');
465
+ }
466
+ return `<span class="${className}">${cleanText}</span>`;
467
+ });
468
+
469
+ if (fixedCount > 0) {
470
+ console.log(` ✅ Fixed ${fixedCount} escaped span(s)`);
471
+ }
472
+
473
+ return content;
474
+ }
475
+
476
+ function cleanHighlightNumbering(content) {
477
+ console.log(' 🔢 Removing numbering from highlight spans...');
478
+
479
+ let cleanedCount = 0;
480
+ // Clean numbering from non-escaped highlight spans too
481
+ content = content.replace(/<span class="highlight">(\(\d+\)\s*)([^<]+)<\/span>/g, (match, numbering, text) => {
482
+ cleanedCount++;
483
+ return `<span class="highlight">${text}</span>`;
484
+ });
485
+
486
+ if (cleanedCount > 0) {
487
+ console.log(` ✅ Removed numbering from ${cleanedCount} highlight span(s)`);
488
+ }
489
+
490
+ return content;
491
+ }
492
+
493
+ function transformReferenceLinks(content) {
494
+ console.log(' 🔗 Transforming reference links...');
495
+
496
+ // Transform Pandoc reference links: [text](#ref){reference-type="ref" reference="ref"}
497
+ return content.replace(
498
+ /\[([^\]]+)\]\((#[^)]+)\)\{[^}]*reference[^}]*\}/g,
499
+ (match, text, href) => {
500
+ return `[${text}](${href})`;
501
+ }
502
+ );
503
+ }
504
+
505
+
506
+ /**
507
+ * Fix frontmatter and ensure proper MDX format
508
+ * @param {string} content - MDX content
509
+ * @param {string} latexContent - Original LaTeX content for metadata extraction
510
+ * @returns {string} - Content with proper frontmatter
511
+ */
512
+ function ensureFrontmatter(content, latexContent = '') {
513
+ console.log(' 📄 Ensuring proper frontmatter...');
514
+
515
+ if (!content.startsWith('---')) {
516
+ let frontmatter;
517
+
518
+ if (latexContent) {
519
+ // Extract metadata from LaTeX using dedicated module
520
+ frontmatter = extractAndGenerateFrontmatter(latexContent);
521
+ console.log(' ✅ Generated frontmatter from LaTeX metadata');
522
+ } else {
523
+ // Fallback frontmatter
524
+ const currentDate = new Date().toLocaleDateString('en-US', {
525
+ year: 'numeric',
526
+ month: 'short',
527
+ day: '2-digit'
528
+ });
529
+ frontmatter = `---
530
+ title: "Research Article"
531
+ published: "${currentDate}"
532
+ tableOfContentsAutoCollapse: true
533
+ ---
534
+
535
+ `;
536
+ console.log(' ✅ Generated basic frontmatter');
537
+ }
538
+
539
+ return frontmatter + content;
540
+ }
541
+
542
+ return content;
543
+ }
544
+
545
+ /**
546
+ * Fix mixed math delimiters like $`...`$ or `...`$
547
+ * @param {string} content - MDX content
548
+ * @returns {string} - Content with fixed math delimiters
549
+ */
550
+ function fixMixedMathDelimiters(content) {
551
+ console.log(' 🔧 Fixing mixed math delimiters...');
552
+
553
+ let fixedCount = 0;
554
+
555
+ // Fix patterns like $`...`$ (mixed delimiters)
556
+ content = content.replace(/\$`([^`]*)`\$/g, (match, mathContent) => {
557
+ fixedCount++;
558
+ return `$${mathContent}$`;
559
+ });
560
+
561
+ // Fix patterns like `...`$ (backtick start, dollar end)
562
+ content = content.replace(/`([^`]*)`\$/g, (match, mathContent) => {
563
+ fixedCount++;
564
+ return `$${mathContent}$`;
565
+ });
566
+
567
+ // Fix patterns like $`...` (dollar start, backtick end - less common)
568
+ content = content.replace(/\$`([^`]*)`(?!\$)/g, (match, mathContent) => {
569
+ fixedCount++;
570
+ return `$${mathContent}$`;
571
+ });
572
+
573
+ if (fixedCount > 0) {
574
+ console.log(` ✅ Fixed ${fixedCount} mixed math delimiter(s)`);
575
+ }
576
+
577
+ return content;
578
+ }
579
+
580
+ /**
581
+ * Clean up orphaned math delimiters and fix mixed content
582
+ * @param {string} content - MDX content
583
+ * @returns {string} - Content with cleaned math blocks
584
+ */
585
+ function cleanOrphanedMathDelimiters(content) {
586
+ console.log(' 🧹 Cleaning orphaned math delimiters...');
587
+ console.log(' 🔍 Content length:', content.length, 'chars');
588
+
589
+ let fixedCount = 0;
590
+
591
+ // Fix orphaned $$ that are alone on lines (but not part of display math blocks)
592
+ // Only remove $$ that appear alone without corresponding closing $$
593
+ content = content.replace(/^\$\$\s*$(?!\s*[\s\S]*?\$\$)/gm, () => {
594
+ fixedCount++;
595
+ return '';
596
+ });
597
+
598
+ // Fix backticks inside $$....$$ blocks (Pandoc artifact)
599
+ const mathMatches = content.match(/\$\$([\s\S]*?)\$\$/g);
600
+ console.log(` 🔍 Found ${mathMatches ? mathMatches.length : 0} math blocks`);
601
+
602
+ content = content.replace(/\$\$([\s\S]*?)\$\$/g, (match, mathContent) => {
603
+ // More aggressive: remove ALL single backticks in math blocks (they shouldn't be there)
604
+ let cleanedMath = mathContent;
605
+
606
+ // Count backticks before
607
+ const backticksBefore = (mathContent.match(/`/g) || []).length;
608
+
609
+ if (backticksBefore > 0) {
610
+ console.log(` 🔧 Found math block with ${backticksBefore} backtick(s)`);
611
+ }
612
+
613
+ // Remove all isolated backticks (not in pairs)
614
+ cleanedMath = cleanedMath.replace(/`/g, '');
615
+
616
+ const backticksAfter = (cleanedMath.match(/`/g) || []).length;
617
+
618
+ if (backticksBefore > 0) {
619
+ fixedCount++;
620
+ console.log(` 🔧 Removed ${backticksBefore} backtick(s) from math block`);
621
+ return `$$${cleanedMath}$$`;
622
+ }
623
+ return match;
624
+ });
625
+
626
+ // Fix escaped align in math blocks: \begin{align} -> \begin{align}
627
+ content = content.replace(/\\begin\{align\}/g, (match) => {
628
+ fixedCount++;
629
+ return '\\begin{align}';
630
+ });
631
+
632
+ content = content.replace(/\\end\{align\}/g, (match) => {
633
+ fixedCount++;
634
+ return '\\end{align}';
635
+ });
636
+
637
+ // Fix cases where text gets mixed with math blocks
638
+ // Pattern: ``` math ... ``` text ``` math
639
+ content = content.replace(/``` math\s*\n([\s\S]*?)\n```\s*([^`\n]*?)\s*``` math/g, (match, math1, text, math2) => {
640
+ if (text.trim().length > 0 && !text.includes('```')) {
641
+ fixedCount++;
642
+ return '```' + ' math\n' + math1 + '\n```\n\n' + text.trim() + '\n\n```' + ' math';
643
+ }
644
+ return match;
645
+ });
646
+
647
+ if (fixedCount > 0) {
648
+ console.log(` ✅ Fixed ${fixedCount} orphaned math delimiter(s)`);
649
+ }
650
+
651
+ return content;
652
+ }
653
+
654
+ /**
655
+ * Clean newlines from single-dollar math blocks ($...$) ONLY
656
+ * @param {string} content - MDX content
657
+ * @returns {string} - Content with cleaned math blocks
658
+ */
659
+ function cleanSingleLineMathNewlines(content) {
660
+ console.log(' 🔢 Cleaning newlines in single-dollar math blocks ($...$)...');
661
+
662
+ let cleanedCount = 0;
663
+
664
+ // ULTRA STRICT: Only target single dollar blocks ($...$) that contain newlines
665
+ // Use dotall flag (s) to match newlines with .*, and ensure we don't match $$
666
+ const cleanedContent = content.replace(/\$(?!\$)([\s\S]*?)\$(?!\$)/g, (match, mathContent) => {
667
+ // Only process if the content contains newlines
668
+ if (mathContent.includes('\n')) {
669
+ cleanedCount++;
670
+
671
+ // Remove ALL newlines and carriage returns, normalize whitespace
672
+ const cleanedMath = mathContent
673
+ .replace(/\n+/g, ' ') // Replace all newlines with spaces
674
+ .replace(/\r+/g, ' ') // Replace carriage returns with spaces
675
+ .replace(/\s+/g, ' ') // Normalize multiple spaces to single
676
+ .trim(); // Remove leading/trailing spaces
677
+
678
+ return `$${cleanedMath}$`;
679
+ }
680
+ return match; // Keep original if no newlines
681
+ });
682
+
683
+ if (cleanedCount > 0) {
684
+ console.log(` ✅ Cleaned ${cleanedCount} single-dollar math block(s) with newlines`);
685
+ }
686
+
687
+ return cleanedContent;
688
+ }
689
+
690
+ /**
691
+ * Add proper line breaks around display math blocks ($$...$$)
692
+ * @param {string} content - MDX content
693
+ * @returns {string} - Content with properly spaced display math
694
+ */
695
+ function formatDisplayMathBlocks(content) {
696
+ console.log(' 📐 Formatting display math blocks with proper spacing...');
697
+
698
+ let formattedCount = 0;
699
+
700
+ // Find all $$...$$$ blocks (display math) and ensure proper line breaks
701
+ // Very strict: only matches exactly $$ followed by content followed by $$
702
+ const formattedContent = content.replace(/\$\$([\s\S]*?)\$\$/g, (match, mathContent) => {
703
+ formattedCount++;
704
+
705
+ // Clean up the math content - trim whitespace but preserve structure
706
+ const cleanedMath = mathContent.trim();
707
+
708
+ // Return with proper line breaks before and after
709
+ return `\n$$\n${cleanedMath}\n$$\n`;
710
+ });
711
+
712
+ if (formattedCount > 0) {
713
+ console.log(` ✅ Formatted ${formattedCount} display math block(s) with proper spacing`);
714
+ }
715
+
716
+ return formattedContent;
717
+ }
718
+
719
+ /**
720
+ * Clean newlines from figcaption content
721
+ * @param {string} content - MDX content
722
+ * @returns {string} - Content with cleaned figcaptions
723
+ */
724
+ function cleanFigcaptionNewlines(content) {
725
+ console.log(' 📝 Cleaning newlines in figcaption elements...');
726
+
727
+ let cleanedCount = 0;
728
+
729
+ // Find all <figcaption>...</figcaption> blocks and remove internal newlines
730
+ const cleanedContent = content.replace(/<figcaption([^>]*)>([\s\S]*?)<\/figcaption>/g, (match, attributes, captionContent) => {
731
+ // Only process if the content contains newlines
732
+ if (captionContent.includes('\n')) {
733
+ cleanedCount++;
734
+
735
+ // Remove newlines and normalize whitespace
736
+ const cleanedCaption = captionContent
737
+ .replace(/\n+/g, ' ') // Replace newlines with spaces
738
+ .replace(/\s+/g, ' ') // Normalize multiple spaces
739
+ .trim(); // Trim whitespace
740
+
741
+ return `<figcaption${attributes}>${cleanedCaption}</figcaption>`;
742
+ }
743
+
744
+ return match; // Return unchanged if no newlines
745
+ });
746
+
747
+ if (cleanedCount > 0) {
748
+ console.log(` ✅ Cleaned ${cleanedCount} figcaption element(s)`);
749
+ } else {
750
+ console.log(` ℹ️ No figcaption elements with newlines found`);
751
+ }
752
+
753
+ return cleanedContent;
754
+ }
755
+
756
+ /**
757
+ * Remove HTML comments from MDX content
758
+ * @param {string} content - MDX content
759
+ * @returns {string} - Content without HTML comments
760
+ */
761
+ function removeHtmlComments(content) {
762
+ console.log(' 🗑️ Removing HTML comments...');
763
+
764
+ let removedCount = 0;
765
+
766
+ // Remove all HTML comments <!-- ... -->
767
+ const cleanedContent = content.replace(/<!--[\s\S]*?-->/g, () => {
768
+ removedCount++;
769
+ return '';
770
+ });
771
+
772
+ if (removedCount > 0) {
773
+ console.log(` ✅ Removed ${removedCount} HTML comment(s)`);
774
+ }
775
+
776
+ return cleanedContent;
777
+ }
778
+
779
+ /**
780
+ * Clean up MDX-incompatible syntax
781
+ * @param {string} content - MDX content
782
+ * @returns {string} - Cleaned content
783
+ */
784
+ function cleanMdxSyntax(content) {
785
+ console.log(' 🧹 Cleaning MDX syntax...');
786
+
787
+ return content
788
+ // NOTE: Math delimiter fixing is now handled by fixMixedMathDelimiters()
789
+ // Ensure proper spacing around JSX-like constructs
790
+ .replace(/>\s*</g, '>\n<')
791
+ // Remove problematic heading attributes - be more specific to avoid matching \begin{align}
792
+ .replace(/^(#{1,6}\s+[^{#\n]+)\{[^}]+\}$/gm, '$1')
793
+ // Fix escaped quotes in text
794
+ .replace(/\\("|')/g, '$1');
795
+ }
796
+
797
+ /**
798
+ * Main MDX processing function that applies all transformations
799
+ * @param {string} content - Raw Markdown content
800
+ * @param {string} latexContent - Original LaTeX content for metadata extraction
801
+ * @returns {string} - Processed MDX content compatible with Astro
802
+ */
803
+ function processMdxContent(content, latexContent = '') {
804
+ console.log('🔧 Processing for Astro MDX compatibility...');
805
+
806
+ // Clear previous tracking
807
+ usedComponents.clear();
808
+ imageImports.clear();
809
+
810
+ let processedContent = content;
811
+
812
+ // Apply each transformation step sequentially
813
+ processedContent = ensureFrontmatter(processedContent, latexContent);
814
+ processedContent = fixMixedMathDelimiters(processedContent);
815
+
816
+ // Debug: check for $$ blocks after fixMixedMathDelimiters
817
+ const mathBlocksAfterMixed = (processedContent.match(/\$\$([\s\S]*?)\$\$/g) || []).length;
818
+ console.log(` 📊 Math blocks after mixed delimiters fix: ${mathBlocksAfterMixed}`);
819
+
820
+ processedContent = cleanOrphanedMathDelimiters(processedContent);
821
+ processedContent = cleanSingleLineMathNewlines(processedContent);
822
+ processedContent = formatDisplayMathBlocks(processedContent);
823
+ processedContent = removeHtmlComments(processedContent);
824
+ processedContent = cleanMdxSyntax(processedContent);
825
+ processedContent = convertSubfiguresToMultiFigure(processedContent);
826
+ processedContent = transformImages(processedContent);
827
+ processedContent = transformStyledSpans(processedContent);
828
+ processedContent = transformReferenceLinks(processedContent);
829
+ processedContent = fixHtmlEscaping(processedContent);
830
+ processedContent = cleanHighlightNumbering(processedContent);
831
+ processedContent = cleanFigcaptionNewlines(processedContent);
832
+
833
+ // Add component imports at the end
834
+ processedContent = addComponentImports(processedContent);
835
+
836
+ return processedContent;
837
+ }
838
+
839
+ function convertToMdx(inputFile, outputFile) {
840
+ console.log('📝 Modular Markdown to Astro MDX Converter');
841
+ console.log(`📁 Input: ${inputFile}`);
842
+ console.log(`📁 Output: ${outputFile}`);
843
+
844
+ // Check if input file exists
845
+ if (!existsSync(inputFile)) {
846
+ console.error(`❌ Input file not found: ${inputFile}`);
847
+ process.exit(1);
848
+ }
849
+
850
+ try {
851
+ console.log('🔄 Reading Markdown file...');
852
+ const markdownContent = readFileSync(inputFile, 'utf8');
853
+
854
+ // Try to read original LaTeX file for metadata extraction
855
+ let latexContent = '';
856
+ try {
857
+ const inputDir = dirname(inputFile);
858
+ const latexFile = join(inputDir, '..', 'input', 'main.tex');
859
+ if (existsSync(latexFile)) {
860
+ latexContent = readFileSync(latexFile, 'utf8');
861
+ }
862
+ } catch (error) {
863
+ // Ignore LaTeX reading errors - we'll use fallback frontmatter
864
+ }
865
+
866
+ // Apply modular MDX processing
867
+ const mdxContent = processMdxContent(markdownContent, latexContent);
868
+
869
+ console.log('💾 Writing MDX file...');
870
+ writeFileSync(outputFile, mdxContent);
871
+
872
+ console.log(`✅ Conversion completed: ${outputFile}`);
873
+
874
+ // Show file size
875
+ const inputSize = Math.round(markdownContent.length / 1024);
876
+ const outputSize = Math.round(mdxContent.length / 1024);
877
+ console.log(`📊 Input: ${inputSize}KB → Output: ${outputSize}KB`);
878
+
879
+ } catch (error) {
880
+ console.error('❌ Conversion failed:');
881
+ console.error(error.message);
882
+ process.exit(1);
883
+ }
884
+ }
885
+
886
+ export { convertToMdx };
887
+
888
+ function main() {
889
+ const config = parseArgs();
890
+ convertToMdx(config.input, config.output);
891
+ console.log('🎉 MDX conversion completed!');
892
+ }
893
+
894
+ if (import.meta.url === `file://${process.argv[1]}`) {
895
+ main();
896
+ }
app/scripts/latex-importer/metadata-extractor.mjs ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * LaTeX Metadata Extractor
3
+ * Extracts document metadata from LaTeX files for frontmatter generation
4
+ */
5
+
6
+ /**
7
+ * Extract metadata from LaTeX content
8
+ * @param {string} latexContent - Raw LaTeX content
9
+ * @returns {object} - Extracted metadata object
10
+ */
11
+ export function extractLatexMetadata(latexContent) {
12
+ const metadata = {};
13
+
14
+ // Extract title
15
+ const titleMatch = latexContent.match(/\\title\s*\{\s*([^}]+)\s*\}/s);
16
+ if (titleMatch) {
17
+ metadata.title = titleMatch[1]
18
+ .replace(/\n/g, ' ')
19
+ .trim();
20
+ }
21
+
22
+ // Extract authors with their specific affiliations
23
+ const authors = [];
24
+ const authorMatches = latexContent.matchAll(/\\authorOne\[[^\]]*\]\{([^}]+)\}/g);
25
+
26
+ for (const match of authorMatches) {
27
+ const fullAuthorInfo = match[1];
28
+
29
+ // Determine affiliations based on macros present
30
+ const affiliations = [];
31
+ if (fullAuthorInfo.includes('\\ensps')) {
32
+ affiliations.push(1); // École Normale Supérieure
33
+ }
34
+ if (fullAuthorInfo.includes('\\hf')) {
35
+ affiliations.push(2); // Hugging Face
36
+ }
37
+
38
+ // Clean author name by removing macros
39
+ let authorName = fullAuthorInfo
40
+ .replace(/\\ensps/g, '') // Remove École macro
41
+ .replace(/\\hf/g, '') // Remove Hugging Face macro
42
+ .replace(/\s+/g, ' ') // Normalize whitespace
43
+ .trim();
44
+
45
+ // Skip empty authors or placeholder entries
46
+ if (authorName && authorName !== '...') {
47
+ authors.push({
48
+ name: authorName,
49
+ affiliations: affiliations.length > 0 ? affiliations : [2] // Default to HF if no macro
50
+ });
51
+ }
52
+ }
53
+
54
+ if (authors.length > 0) {
55
+ metadata.authors = authors;
56
+ }
57
+
58
+ // Extract affiliations - create the two distinct affiliations
59
+ metadata.affiliations = [
60
+ {
61
+ name: "École Normale Supérieure Paris-Saclay"
62
+ },
63
+ {
64
+ name: "Hugging Face"
65
+ }
66
+ ];
67
+
68
+ // Extract date if available (common LaTeX patterns)
69
+ const datePatterns = [
70
+ /\\date\s*\{([^}]+)\}/,
71
+ /\\newcommand\s*\{\\date\}\s*\{([^}]+)\}/,
72
+ ];
73
+
74
+ for (const pattern of datePatterns) {
75
+ const dateMatch = latexContent.match(pattern);
76
+ if (dateMatch) {
77
+ metadata.published = dateMatch[1].trim();
78
+ break;
79
+ }
80
+ }
81
+
82
+ // Fallback to current date if no date found
83
+ if (!metadata.published) {
84
+ metadata.published = new Date().toLocaleDateString('en-US', {
85
+ year: 'numeric',
86
+ month: 'short',
87
+ day: '2-digit'
88
+ });
89
+ }
90
+
91
+ return metadata;
92
+ }
93
+
94
+ /**
95
+ * Generate YAML frontmatter from metadata object
96
+ * @param {object} metadata - Metadata object
97
+ * @returns {string} - YAML frontmatter string
98
+ */
99
+ export function generateFrontmatter(metadata) {
100
+ let frontmatter = '---\n';
101
+
102
+ // Title
103
+ if (metadata.title) {
104
+ frontmatter += `title: "${metadata.title}"\n`;
105
+ }
106
+
107
+ // Authors
108
+ if (metadata.authors && metadata.authors.length > 0) {
109
+ frontmatter += 'authors:\n';
110
+ metadata.authors.forEach(author => {
111
+ frontmatter += ` - name: "${author.name}"\n`;
112
+ if (author.url) {
113
+ frontmatter += ` url: "${author.url}"\n`;
114
+ }
115
+ frontmatter += ` affiliations: [${author.affiliations.join(', ')}]\n`;
116
+ });
117
+ }
118
+
119
+ // Affiliations
120
+ if (metadata.affiliations && metadata.affiliations.length > 0) {
121
+ frontmatter += 'affiliations:\n';
122
+ metadata.affiliations.forEach((affiliation, index) => {
123
+ frontmatter += ` - name: "${affiliation.name}"\n`;
124
+ if (affiliation.url) {
125
+ frontmatter += ` url: "${affiliation.url}"\n`;
126
+ }
127
+ });
128
+ }
129
+
130
+ // Publication date
131
+ if (metadata.published) {
132
+ frontmatter += `published: "${metadata.published}"\n`;
133
+ }
134
+
135
+ // Additional metadata
136
+ if (metadata.doi) {
137
+ frontmatter += `doi: "${metadata.doi}"\n`;
138
+ }
139
+
140
+ if (metadata.description) {
141
+ frontmatter += `description: "${metadata.description}"\n`;
142
+ }
143
+
144
+ if (metadata.licence) {
145
+ frontmatter += `licence: >\n ${metadata.licence}\n`;
146
+ }
147
+
148
+ if (metadata.tags && metadata.tags.length > 0) {
149
+ frontmatter += 'tags:\n';
150
+ metadata.tags.forEach(tag => {
151
+ frontmatter += ` - ${tag}\n`;
152
+ });
153
+ }
154
+
155
+ // Default Astro configuration
156
+ frontmatter += 'tableOfContentsAutoCollapse: true\n';
157
+ frontmatter += '---\n\n';
158
+
159
+ return frontmatter;
160
+ }
161
+
162
+ /**
163
+ * Extract and generate frontmatter from LaTeX content
164
+ * @param {string} latexContent - Raw LaTeX content
165
+ * @returns {string} - Complete YAML frontmatter
166
+ */
167
+ export function extractAndGenerateFrontmatter(latexContent) {
168
+ const metadata = extractLatexMetadata(latexContent);
169
+ return generateFrontmatter(metadata);
170
+ }
app/scripts/latex-importer/package-lock.json ADDED
@@ -0,0 +1,1272 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "latex-to-mdx-toolkit",
3
+ "version": "2.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "latex-to-mdx-toolkit",
9
+ "version": "2.0.0",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "remark-mdx": "^3.0.0",
13
+ "remark-parse": "^11.0.0",
14
+ "remark-stringify": "^11.0.0",
15
+ "unified": "^11.0.4"
16
+ }
17
+ },
18
+ "node_modules/@types/debug": {
19
+ "version": "4.1.12",
20
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
21
+ "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+ "@types/ms": "*"
25
+ }
26
+ },
27
+ "node_modules/@types/estree": {
28
+ "version": "1.0.8",
29
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
30
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
31
+ "license": "MIT"
32
+ },
33
+ "node_modules/@types/estree-jsx": {
34
+ "version": "1.0.5",
35
+ "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
36
+ "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
37
+ "license": "MIT",
38
+ "dependencies": {
39
+ "@types/estree": "*"
40
+ }
41
+ },
42
+ "node_modules/@types/hast": {
43
+ "version": "3.0.4",
44
+ "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
45
+ "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
46
+ "license": "MIT",
47
+ "dependencies": {
48
+ "@types/unist": "*"
49
+ }
50
+ },
51
+ "node_modules/@types/mdast": {
52
+ "version": "4.0.4",
53
+ "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
54
+ "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
55
+ "license": "MIT",
56
+ "dependencies": {
57
+ "@types/unist": "*"
58
+ }
59
+ },
60
+ "node_modules/@types/ms": {
61
+ "version": "2.1.0",
62
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
63
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
64
+ "license": "MIT"
65
+ },
66
+ "node_modules/@types/unist": {
67
+ "version": "3.0.3",
68
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
69
+ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
70
+ "license": "MIT"
71
+ },
72
+ "node_modules/acorn": {
73
+ "version": "8.15.0",
74
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
75
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
76
+ "license": "MIT",
77
+ "bin": {
78
+ "acorn": "bin/acorn"
79
+ },
80
+ "engines": {
81
+ "node": ">=0.4.0"
82
+ }
83
+ },
84
+ "node_modules/acorn-jsx": {
85
+ "version": "5.3.2",
86
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
87
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
88
+ "license": "MIT",
89
+ "peerDependencies": {
90
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
91
+ }
92
+ },
93
+ "node_modules/bail": {
94
+ "version": "2.0.2",
95
+ "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
96
+ "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
97
+ "license": "MIT",
98
+ "funding": {
99
+ "type": "github",
100
+ "url": "https://github.com/sponsors/wooorm"
101
+ }
102
+ },
103
+ "node_modules/ccount": {
104
+ "version": "2.0.1",
105
+ "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
106
+ "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
107
+ "license": "MIT",
108
+ "funding": {
109
+ "type": "github",
110
+ "url": "https://github.com/sponsors/wooorm"
111
+ }
112
+ },
113
+ "node_modules/character-entities": {
114
+ "version": "2.0.2",
115
+ "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
116
+ "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
117
+ "license": "MIT",
118
+ "funding": {
119
+ "type": "github",
120
+ "url": "https://github.com/sponsors/wooorm"
121
+ }
122
+ },
123
+ "node_modules/character-entities-html4": {
124
+ "version": "2.1.0",
125
+ "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
126
+ "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
127
+ "license": "MIT",
128
+ "funding": {
129
+ "type": "github",
130
+ "url": "https://github.com/sponsors/wooorm"
131
+ }
132
+ },
133
+ "node_modules/character-entities-legacy": {
134
+ "version": "3.0.0",
135
+ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
136
+ "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
137
+ "license": "MIT",
138
+ "funding": {
139
+ "type": "github",
140
+ "url": "https://github.com/sponsors/wooorm"
141
+ }
142
+ },
143
+ "node_modules/character-reference-invalid": {
144
+ "version": "2.0.1",
145
+ "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
146
+ "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
147
+ "license": "MIT",
148
+ "funding": {
149
+ "type": "github",
150
+ "url": "https://github.com/sponsors/wooorm"
151
+ }
152
+ },
153
+ "node_modules/debug": {
154
+ "version": "4.4.3",
155
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
156
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
157
+ "license": "MIT",
158
+ "dependencies": {
159
+ "ms": "^2.1.3"
160
+ },
161
+ "engines": {
162
+ "node": ">=6.0"
163
+ },
164
+ "peerDependenciesMeta": {
165
+ "supports-color": {
166
+ "optional": true
167
+ }
168
+ }
169
+ },
170
+ "node_modules/decode-named-character-reference": {
171
+ "version": "1.2.0",
172
+ "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
173
+ "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==",
174
+ "license": "MIT",
175
+ "dependencies": {
176
+ "character-entities": "^2.0.0"
177
+ },
178
+ "funding": {
179
+ "type": "github",
180
+ "url": "https://github.com/sponsors/wooorm"
181
+ }
182
+ },
183
+ "node_modules/dequal": {
184
+ "version": "2.0.3",
185
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
186
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
187
+ "license": "MIT",
188
+ "engines": {
189
+ "node": ">=6"
190
+ }
191
+ },
192
+ "node_modules/devlop": {
193
+ "version": "1.1.0",
194
+ "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
195
+ "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
196
+ "license": "MIT",
197
+ "dependencies": {
198
+ "dequal": "^2.0.0"
199
+ },
200
+ "funding": {
201
+ "type": "github",
202
+ "url": "https://github.com/sponsors/wooorm"
203
+ }
204
+ },
205
+ "node_modules/estree-util-is-identifier-name": {
206
+ "version": "3.0.0",
207
+ "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
208
+ "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
209
+ "license": "MIT",
210
+ "funding": {
211
+ "type": "opencollective",
212
+ "url": "https://opencollective.com/unified"
213
+ }
214
+ },
215
+ "node_modules/estree-util-visit": {
216
+ "version": "2.0.0",
217
+ "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz",
218
+ "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==",
219
+ "license": "MIT",
220
+ "dependencies": {
221
+ "@types/estree-jsx": "^1.0.0",
222
+ "@types/unist": "^3.0.0"
223
+ },
224
+ "funding": {
225
+ "type": "opencollective",
226
+ "url": "https://opencollective.com/unified"
227
+ }
228
+ },
229
+ "node_modules/extend": {
230
+ "version": "3.0.2",
231
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
232
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
233
+ "license": "MIT"
234
+ },
235
+ "node_modules/is-alphabetical": {
236
+ "version": "2.0.1",
237
+ "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
238
+ "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
239
+ "license": "MIT",
240
+ "funding": {
241
+ "type": "github",
242
+ "url": "https://github.com/sponsors/wooorm"
243
+ }
244
+ },
245
+ "node_modules/is-alphanumerical": {
246
+ "version": "2.0.1",
247
+ "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
248
+ "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
249
+ "license": "MIT",
250
+ "dependencies": {
251
+ "is-alphabetical": "^2.0.0",
252
+ "is-decimal": "^2.0.0"
253
+ },
254
+ "funding": {
255
+ "type": "github",
256
+ "url": "https://github.com/sponsors/wooorm"
257
+ }
258
+ },
259
+ "node_modules/is-decimal": {
260
+ "version": "2.0.1",
261
+ "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
262
+ "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
263
+ "license": "MIT",
264
+ "funding": {
265
+ "type": "github",
266
+ "url": "https://github.com/sponsors/wooorm"
267
+ }
268
+ },
269
+ "node_modules/is-hexadecimal": {
270
+ "version": "2.0.1",
271
+ "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
272
+ "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
273
+ "license": "MIT",
274
+ "funding": {
275
+ "type": "github",
276
+ "url": "https://github.com/sponsors/wooorm"
277
+ }
278
+ },
279
+ "node_modules/is-plain-obj": {
280
+ "version": "4.1.0",
281
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
282
+ "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
283
+ "license": "MIT",
284
+ "engines": {
285
+ "node": ">=12"
286
+ },
287
+ "funding": {
288
+ "url": "https://github.com/sponsors/sindresorhus"
289
+ }
290
+ },
291
+ "node_modules/longest-streak": {
292
+ "version": "3.1.0",
293
+ "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
294
+ "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
295
+ "license": "MIT",
296
+ "funding": {
297
+ "type": "github",
298
+ "url": "https://github.com/sponsors/wooorm"
299
+ }
300
+ },
301
+ "node_modules/mdast-util-from-markdown": {
302
+ "version": "2.0.2",
303
+ "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
304
+ "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==",
305
+ "license": "MIT",
306
+ "dependencies": {
307
+ "@types/mdast": "^4.0.0",
308
+ "@types/unist": "^3.0.0",
309
+ "decode-named-character-reference": "^1.0.0",
310
+ "devlop": "^1.0.0",
311
+ "mdast-util-to-string": "^4.0.0",
312
+ "micromark": "^4.0.0",
313
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
314
+ "micromark-util-decode-string": "^2.0.0",
315
+ "micromark-util-normalize-identifier": "^2.0.0",
316
+ "micromark-util-symbol": "^2.0.0",
317
+ "micromark-util-types": "^2.0.0",
318
+ "unist-util-stringify-position": "^4.0.0"
319
+ },
320
+ "funding": {
321
+ "type": "opencollective",
322
+ "url": "https://opencollective.com/unified"
323
+ }
324
+ },
325
+ "node_modules/mdast-util-mdx": {
326
+ "version": "3.0.0",
327
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz",
328
+ "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==",
329
+ "license": "MIT",
330
+ "dependencies": {
331
+ "mdast-util-from-markdown": "^2.0.0",
332
+ "mdast-util-mdx-expression": "^2.0.0",
333
+ "mdast-util-mdx-jsx": "^3.0.0",
334
+ "mdast-util-mdxjs-esm": "^2.0.0",
335
+ "mdast-util-to-markdown": "^2.0.0"
336
+ },
337
+ "funding": {
338
+ "type": "opencollective",
339
+ "url": "https://opencollective.com/unified"
340
+ }
341
+ },
342
+ "node_modules/mdast-util-mdx-expression": {
343
+ "version": "2.0.1",
344
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
345
+ "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
346
+ "license": "MIT",
347
+ "dependencies": {
348
+ "@types/estree-jsx": "^1.0.0",
349
+ "@types/hast": "^3.0.0",
350
+ "@types/mdast": "^4.0.0",
351
+ "devlop": "^1.0.0",
352
+ "mdast-util-from-markdown": "^2.0.0",
353
+ "mdast-util-to-markdown": "^2.0.0"
354
+ },
355
+ "funding": {
356
+ "type": "opencollective",
357
+ "url": "https://opencollective.com/unified"
358
+ }
359
+ },
360
+ "node_modules/mdast-util-mdx-jsx": {
361
+ "version": "3.2.0",
362
+ "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
363
+ "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
364
+ "license": "MIT",
365
+ "dependencies": {
366
+ "@types/estree-jsx": "^1.0.0",
367
+ "@types/hast": "^3.0.0",
368
+ "@types/mdast": "^4.0.0",
369
+ "@types/unist": "^3.0.0",
370
+ "ccount": "^2.0.0",
371
+ "devlop": "^1.1.0",
372
+ "mdast-util-from-markdown": "^2.0.0",
373
+ "mdast-util-to-markdown": "^2.0.0",
374
+ "parse-entities": "^4.0.0",
375
+ "stringify-entities": "^4.0.0",
376
+ "unist-util-stringify-position": "^4.0.0",
377
+ "vfile-message": "^4.0.0"
378
+ },
379
+ "funding": {
380
+ "type": "opencollective",
381
+ "url": "https://opencollective.com/unified"
382
+ }
383
+ },
384
+ "node_modules/mdast-util-mdxjs-esm": {
385
+ "version": "2.0.1",
386
+ "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
387
+ "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
388
+ "license": "MIT",
389
+ "dependencies": {
390
+ "@types/estree-jsx": "^1.0.0",
391
+ "@types/hast": "^3.0.0",
392
+ "@types/mdast": "^4.0.0",
393
+ "devlop": "^1.0.0",
394
+ "mdast-util-from-markdown": "^2.0.0",
395
+ "mdast-util-to-markdown": "^2.0.0"
396
+ },
397
+ "funding": {
398
+ "type": "opencollective",
399
+ "url": "https://opencollective.com/unified"
400
+ }
401
+ },
402
+ "node_modules/mdast-util-phrasing": {
403
+ "version": "4.1.0",
404
+ "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
405
+ "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
406
+ "license": "MIT",
407
+ "dependencies": {
408
+ "@types/mdast": "^4.0.0",
409
+ "unist-util-is": "^6.0.0"
410
+ },
411
+ "funding": {
412
+ "type": "opencollective",
413
+ "url": "https://opencollective.com/unified"
414
+ }
415
+ },
416
+ "node_modules/mdast-util-to-markdown": {
417
+ "version": "2.1.2",
418
+ "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
419
+ "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
420
+ "license": "MIT",
421
+ "dependencies": {
422
+ "@types/mdast": "^4.0.0",
423
+ "@types/unist": "^3.0.0",
424
+ "longest-streak": "^3.0.0",
425
+ "mdast-util-phrasing": "^4.0.0",
426
+ "mdast-util-to-string": "^4.0.0",
427
+ "micromark-util-classify-character": "^2.0.0",
428
+ "micromark-util-decode-string": "^2.0.0",
429
+ "unist-util-visit": "^5.0.0",
430
+ "zwitch": "^2.0.0"
431
+ },
432
+ "funding": {
433
+ "type": "opencollective",
434
+ "url": "https://opencollective.com/unified"
435
+ }
436
+ },
437
+ "node_modules/mdast-util-to-string": {
438
+ "version": "4.0.0",
439
+ "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
440
+ "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
441
+ "license": "MIT",
442
+ "dependencies": {
443
+ "@types/mdast": "^4.0.0"
444
+ },
445
+ "funding": {
446
+ "type": "opencollective",
447
+ "url": "https://opencollective.com/unified"
448
+ }
449
+ },
450
+ "node_modules/micromark": {
451
+ "version": "4.0.2",
452
+ "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
453
+ "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
454
+ "funding": [
455
+ {
456
+ "type": "GitHub Sponsors",
457
+ "url": "https://github.com/sponsors/unifiedjs"
458
+ },
459
+ {
460
+ "type": "OpenCollective",
461
+ "url": "https://opencollective.com/unified"
462
+ }
463
+ ],
464
+ "license": "MIT",
465
+ "dependencies": {
466
+ "@types/debug": "^4.0.0",
467
+ "debug": "^4.0.0",
468
+ "decode-named-character-reference": "^1.0.0",
469
+ "devlop": "^1.0.0",
470
+ "micromark-core-commonmark": "^2.0.0",
471
+ "micromark-factory-space": "^2.0.0",
472
+ "micromark-util-character": "^2.0.0",
473
+ "micromark-util-chunked": "^2.0.0",
474
+ "micromark-util-combine-extensions": "^2.0.0",
475
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
476
+ "micromark-util-encode": "^2.0.0",
477
+ "micromark-util-normalize-identifier": "^2.0.0",
478
+ "micromark-util-resolve-all": "^2.0.0",
479
+ "micromark-util-sanitize-uri": "^2.0.0",
480
+ "micromark-util-subtokenize": "^2.0.0",
481
+ "micromark-util-symbol": "^2.0.0",
482
+ "micromark-util-types": "^2.0.0"
483
+ }
484
+ },
485
+ "node_modules/micromark-core-commonmark": {
486
+ "version": "2.0.3",
487
+ "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
488
+ "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
489
+ "funding": [
490
+ {
491
+ "type": "GitHub Sponsors",
492
+ "url": "https://github.com/sponsors/unifiedjs"
493
+ },
494
+ {
495
+ "type": "OpenCollective",
496
+ "url": "https://opencollective.com/unified"
497
+ }
498
+ ],
499
+ "license": "MIT",
500
+ "dependencies": {
501
+ "decode-named-character-reference": "^1.0.0",
502
+ "devlop": "^1.0.0",
503
+ "micromark-factory-destination": "^2.0.0",
504
+ "micromark-factory-label": "^2.0.0",
505
+ "micromark-factory-space": "^2.0.0",
506
+ "micromark-factory-title": "^2.0.0",
507
+ "micromark-factory-whitespace": "^2.0.0",
508
+ "micromark-util-character": "^2.0.0",
509
+ "micromark-util-chunked": "^2.0.0",
510
+ "micromark-util-classify-character": "^2.0.0",
511
+ "micromark-util-html-tag-name": "^2.0.0",
512
+ "micromark-util-normalize-identifier": "^2.0.0",
513
+ "micromark-util-resolve-all": "^2.0.0",
514
+ "micromark-util-subtokenize": "^2.0.0",
515
+ "micromark-util-symbol": "^2.0.0",
516
+ "micromark-util-types": "^2.0.0"
517
+ }
518
+ },
519
+ "node_modules/micromark-extension-mdx-expression": {
520
+ "version": "3.0.1",
521
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz",
522
+ "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==",
523
+ "funding": [
524
+ {
525
+ "type": "GitHub Sponsors",
526
+ "url": "https://github.com/sponsors/unifiedjs"
527
+ },
528
+ {
529
+ "type": "OpenCollective",
530
+ "url": "https://opencollective.com/unified"
531
+ }
532
+ ],
533
+ "license": "MIT",
534
+ "dependencies": {
535
+ "@types/estree": "^1.0.0",
536
+ "devlop": "^1.0.0",
537
+ "micromark-factory-mdx-expression": "^2.0.0",
538
+ "micromark-factory-space": "^2.0.0",
539
+ "micromark-util-character": "^2.0.0",
540
+ "micromark-util-events-to-acorn": "^2.0.0",
541
+ "micromark-util-symbol": "^2.0.0",
542
+ "micromark-util-types": "^2.0.0"
543
+ }
544
+ },
545
+ "node_modules/micromark-extension-mdx-jsx": {
546
+ "version": "3.0.2",
547
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz",
548
+ "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==",
549
+ "license": "MIT",
550
+ "dependencies": {
551
+ "@types/estree": "^1.0.0",
552
+ "devlop": "^1.0.0",
553
+ "estree-util-is-identifier-name": "^3.0.0",
554
+ "micromark-factory-mdx-expression": "^2.0.0",
555
+ "micromark-factory-space": "^2.0.0",
556
+ "micromark-util-character": "^2.0.0",
557
+ "micromark-util-events-to-acorn": "^2.0.0",
558
+ "micromark-util-symbol": "^2.0.0",
559
+ "micromark-util-types": "^2.0.0",
560
+ "vfile-message": "^4.0.0"
561
+ },
562
+ "funding": {
563
+ "type": "opencollective",
564
+ "url": "https://opencollective.com/unified"
565
+ }
566
+ },
567
+ "node_modules/micromark-extension-mdx-md": {
568
+ "version": "2.0.0",
569
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz",
570
+ "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==",
571
+ "license": "MIT",
572
+ "dependencies": {
573
+ "micromark-util-types": "^2.0.0"
574
+ },
575
+ "funding": {
576
+ "type": "opencollective",
577
+ "url": "https://opencollective.com/unified"
578
+ }
579
+ },
580
+ "node_modules/micromark-extension-mdxjs": {
581
+ "version": "3.0.0",
582
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz",
583
+ "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==",
584
+ "license": "MIT",
585
+ "dependencies": {
586
+ "acorn": "^8.0.0",
587
+ "acorn-jsx": "^5.0.0",
588
+ "micromark-extension-mdx-expression": "^3.0.0",
589
+ "micromark-extension-mdx-jsx": "^3.0.0",
590
+ "micromark-extension-mdx-md": "^2.0.0",
591
+ "micromark-extension-mdxjs-esm": "^3.0.0",
592
+ "micromark-util-combine-extensions": "^2.0.0",
593
+ "micromark-util-types": "^2.0.0"
594
+ },
595
+ "funding": {
596
+ "type": "opencollective",
597
+ "url": "https://opencollective.com/unified"
598
+ }
599
+ },
600
+ "node_modules/micromark-extension-mdxjs-esm": {
601
+ "version": "3.0.0",
602
+ "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz",
603
+ "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==",
604
+ "license": "MIT",
605
+ "dependencies": {
606
+ "@types/estree": "^1.0.0",
607
+ "devlop": "^1.0.0",
608
+ "micromark-core-commonmark": "^2.0.0",
609
+ "micromark-util-character": "^2.0.0",
610
+ "micromark-util-events-to-acorn": "^2.0.0",
611
+ "micromark-util-symbol": "^2.0.0",
612
+ "micromark-util-types": "^2.0.0",
613
+ "unist-util-position-from-estree": "^2.0.0",
614
+ "vfile-message": "^4.0.0"
615
+ },
616
+ "funding": {
617
+ "type": "opencollective",
618
+ "url": "https://opencollective.com/unified"
619
+ }
620
+ },
621
+ "node_modules/micromark-factory-destination": {
622
+ "version": "2.0.1",
623
+ "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
624
+ "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
625
+ "funding": [
626
+ {
627
+ "type": "GitHub Sponsors",
628
+ "url": "https://github.com/sponsors/unifiedjs"
629
+ },
630
+ {
631
+ "type": "OpenCollective",
632
+ "url": "https://opencollective.com/unified"
633
+ }
634
+ ],
635
+ "license": "MIT",
636
+ "dependencies": {
637
+ "micromark-util-character": "^2.0.0",
638
+ "micromark-util-symbol": "^2.0.0",
639
+ "micromark-util-types": "^2.0.0"
640
+ }
641
+ },
642
+ "node_modules/micromark-factory-label": {
643
+ "version": "2.0.1",
644
+ "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
645
+ "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
646
+ "funding": [
647
+ {
648
+ "type": "GitHub Sponsors",
649
+ "url": "https://github.com/sponsors/unifiedjs"
650
+ },
651
+ {
652
+ "type": "OpenCollective",
653
+ "url": "https://opencollective.com/unified"
654
+ }
655
+ ],
656
+ "license": "MIT",
657
+ "dependencies": {
658
+ "devlop": "^1.0.0",
659
+ "micromark-util-character": "^2.0.0",
660
+ "micromark-util-symbol": "^2.0.0",
661
+ "micromark-util-types": "^2.0.0"
662
+ }
663
+ },
664
+ "node_modules/micromark-factory-mdx-expression": {
665
+ "version": "2.0.3",
666
+ "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz",
667
+ "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==",
668
+ "funding": [
669
+ {
670
+ "type": "GitHub Sponsors",
671
+ "url": "https://github.com/sponsors/unifiedjs"
672
+ },
673
+ {
674
+ "type": "OpenCollective",
675
+ "url": "https://opencollective.com/unified"
676
+ }
677
+ ],
678
+ "license": "MIT",
679
+ "dependencies": {
680
+ "@types/estree": "^1.0.0",
681
+ "devlop": "^1.0.0",
682
+ "micromark-factory-space": "^2.0.0",
683
+ "micromark-util-character": "^2.0.0",
684
+ "micromark-util-events-to-acorn": "^2.0.0",
685
+ "micromark-util-symbol": "^2.0.0",
686
+ "micromark-util-types": "^2.0.0",
687
+ "unist-util-position-from-estree": "^2.0.0",
688
+ "vfile-message": "^4.0.0"
689
+ }
690
+ },
691
+ "node_modules/micromark-factory-space": {
692
+ "version": "2.0.1",
693
+ "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
694
+ "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
695
+ "funding": [
696
+ {
697
+ "type": "GitHub Sponsors",
698
+ "url": "https://github.com/sponsors/unifiedjs"
699
+ },
700
+ {
701
+ "type": "OpenCollective",
702
+ "url": "https://opencollective.com/unified"
703
+ }
704
+ ],
705
+ "license": "MIT",
706
+ "dependencies": {
707
+ "micromark-util-character": "^2.0.0",
708
+ "micromark-util-types": "^2.0.0"
709
+ }
710
+ },
711
+ "node_modules/micromark-factory-title": {
712
+ "version": "2.0.1",
713
+ "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
714
+ "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
715
+ "funding": [
716
+ {
717
+ "type": "GitHub Sponsors",
718
+ "url": "https://github.com/sponsors/unifiedjs"
719
+ },
720
+ {
721
+ "type": "OpenCollective",
722
+ "url": "https://opencollective.com/unified"
723
+ }
724
+ ],
725
+ "license": "MIT",
726
+ "dependencies": {
727
+ "micromark-factory-space": "^2.0.0",
728
+ "micromark-util-character": "^2.0.0",
729
+ "micromark-util-symbol": "^2.0.0",
730
+ "micromark-util-types": "^2.0.0"
731
+ }
732
+ },
733
+ "node_modules/micromark-factory-whitespace": {
734
+ "version": "2.0.1",
735
+ "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
736
+ "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
737
+ "funding": [
738
+ {
739
+ "type": "GitHub Sponsors",
740
+ "url": "https://github.com/sponsors/unifiedjs"
741
+ },
742
+ {
743
+ "type": "OpenCollective",
744
+ "url": "https://opencollective.com/unified"
745
+ }
746
+ ],
747
+ "license": "MIT",
748
+ "dependencies": {
749
+ "micromark-factory-space": "^2.0.0",
750
+ "micromark-util-character": "^2.0.0",
751
+ "micromark-util-symbol": "^2.0.0",
752
+ "micromark-util-types": "^2.0.0"
753
+ }
754
+ },
755
+ "node_modules/micromark-util-character": {
756
+ "version": "2.1.1",
757
+ "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
758
+ "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
759
+ "funding": [
760
+ {
761
+ "type": "GitHub Sponsors",
762
+ "url": "https://github.com/sponsors/unifiedjs"
763
+ },
764
+ {
765
+ "type": "OpenCollective",
766
+ "url": "https://opencollective.com/unified"
767
+ }
768
+ ],
769
+ "license": "MIT",
770
+ "dependencies": {
771
+ "micromark-util-symbol": "^2.0.0",
772
+ "micromark-util-types": "^2.0.0"
773
+ }
774
+ },
775
+ "node_modules/micromark-util-chunked": {
776
+ "version": "2.0.1",
777
+ "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
778
+ "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
779
+ "funding": [
780
+ {
781
+ "type": "GitHub Sponsors",
782
+ "url": "https://github.com/sponsors/unifiedjs"
783
+ },
784
+ {
785
+ "type": "OpenCollective",
786
+ "url": "https://opencollective.com/unified"
787
+ }
788
+ ],
789
+ "license": "MIT",
790
+ "dependencies": {
791
+ "micromark-util-symbol": "^2.0.0"
792
+ }
793
+ },
794
+ "node_modules/micromark-util-classify-character": {
795
+ "version": "2.0.1",
796
+ "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
797
+ "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
798
+ "funding": [
799
+ {
800
+ "type": "GitHub Sponsors",
801
+ "url": "https://github.com/sponsors/unifiedjs"
802
+ },
803
+ {
804
+ "type": "OpenCollective",
805
+ "url": "https://opencollective.com/unified"
806
+ }
807
+ ],
808
+ "license": "MIT",
809
+ "dependencies": {
810
+ "micromark-util-character": "^2.0.0",
811
+ "micromark-util-symbol": "^2.0.0",
812
+ "micromark-util-types": "^2.0.0"
813
+ }
814
+ },
815
+ "node_modules/micromark-util-combine-extensions": {
816
+ "version": "2.0.1",
817
+ "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
818
+ "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
819
+ "funding": [
820
+ {
821
+ "type": "GitHub Sponsors",
822
+ "url": "https://github.com/sponsors/unifiedjs"
823
+ },
824
+ {
825
+ "type": "OpenCollective",
826
+ "url": "https://opencollective.com/unified"
827
+ }
828
+ ],
829
+ "license": "MIT",
830
+ "dependencies": {
831
+ "micromark-util-chunked": "^2.0.0",
832
+ "micromark-util-types": "^2.0.0"
833
+ }
834
+ },
835
+ "node_modules/micromark-util-decode-numeric-character-reference": {
836
+ "version": "2.0.2",
837
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
838
+ "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
839
+ "funding": [
840
+ {
841
+ "type": "GitHub Sponsors",
842
+ "url": "https://github.com/sponsors/unifiedjs"
843
+ },
844
+ {
845
+ "type": "OpenCollective",
846
+ "url": "https://opencollective.com/unified"
847
+ }
848
+ ],
849
+ "license": "MIT",
850
+ "dependencies": {
851
+ "micromark-util-symbol": "^2.0.0"
852
+ }
853
+ },
854
+ "node_modules/micromark-util-decode-string": {
855
+ "version": "2.0.1",
856
+ "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
857
+ "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
858
+ "funding": [
859
+ {
860
+ "type": "GitHub Sponsors",
861
+ "url": "https://github.com/sponsors/unifiedjs"
862
+ },
863
+ {
864
+ "type": "OpenCollective",
865
+ "url": "https://opencollective.com/unified"
866
+ }
867
+ ],
868
+ "license": "MIT",
869
+ "dependencies": {
870
+ "decode-named-character-reference": "^1.0.0",
871
+ "micromark-util-character": "^2.0.0",
872
+ "micromark-util-decode-numeric-character-reference": "^2.0.0",
873
+ "micromark-util-symbol": "^2.0.0"
874
+ }
875
+ },
876
+ "node_modules/micromark-util-encode": {
877
+ "version": "2.0.1",
878
+ "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
879
+ "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
880
+ "funding": [
881
+ {
882
+ "type": "GitHub Sponsors",
883
+ "url": "https://github.com/sponsors/unifiedjs"
884
+ },
885
+ {
886
+ "type": "OpenCollective",
887
+ "url": "https://opencollective.com/unified"
888
+ }
889
+ ],
890
+ "license": "MIT"
891
+ },
892
+ "node_modules/micromark-util-events-to-acorn": {
893
+ "version": "2.0.3",
894
+ "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz",
895
+ "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==",
896
+ "funding": [
897
+ {
898
+ "type": "GitHub Sponsors",
899
+ "url": "https://github.com/sponsors/unifiedjs"
900
+ },
901
+ {
902
+ "type": "OpenCollective",
903
+ "url": "https://opencollective.com/unified"
904
+ }
905
+ ],
906
+ "license": "MIT",
907
+ "dependencies": {
908
+ "@types/estree": "^1.0.0",
909
+ "@types/unist": "^3.0.0",
910
+ "devlop": "^1.0.0",
911
+ "estree-util-visit": "^2.0.0",
912
+ "micromark-util-symbol": "^2.0.0",
913
+ "micromark-util-types": "^2.0.0",
914
+ "vfile-message": "^4.0.0"
915
+ }
916
+ },
917
+ "node_modules/micromark-util-html-tag-name": {
918
+ "version": "2.0.1",
919
+ "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
920
+ "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
921
+ "funding": [
922
+ {
923
+ "type": "GitHub Sponsors",
924
+ "url": "https://github.com/sponsors/unifiedjs"
925
+ },
926
+ {
927
+ "type": "OpenCollective",
928
+ "url": "https://opencollective.com/unified"
929
+ }
930
+ ],
931
+ "license": "MIT"
932
+ },
933
+ "node_modules/micromark-util-normalize-identifier": {
934
+ "version": "2.0.1",
935
+ "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
936
+ "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
937
+ "funding": [
938
+ {
939
+ "type": "GitHub Sponsors",
940
+ "url": "https://github.com/sponsors/unifiedjs"
941
+ },
942
+ {
943
+ "type": "OpenCollective",
944
+ "url": "https://opencollective.com/unified"
945
+ }
946
+ ],
947
+ "license": "MIT",
948
+ "dependencies": {
949
+ "micromark-util-symbol": "^2.0.0"
950
+ }
951
+ },
952
+ "node_modules/micromark-util-resolve-all": {
953
+ "version": "2.0.1",
954
+ "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
955
+ "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
956
+ "funding": [
957
+ {
958
+ "type": "GitHub Sponsors",
959
+ "url": "https://github.com/sponsors/unifiedjs"
960
+ },
961
+ {
962
+ "type": "OpenCollective",
963
+ "url": "https://opencollective.com/unified"
964
+ }
965
+ ],
966
+ "license": "MIT",
967
+ "dependencies": {
968
+ "micromark-util-types": "^2.0.0"
969
+ }
970
+ },
971
+ "node_modules/micromark-util-sanitize-uri": {
972
+ "version": "2.0.1",
973
+ "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
974
+ "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
975
+ "funding": [
976
+ {
977
+ "type": "GitHub Sponsors",
978
+ "url": "https://github.com/sponsors/unifiedjs"
979
+ },
980
+ {
981
+ "type": "OpenCollective",
982
+ "url": "https://opencollective.com/unified"
983
+ }
984
+ ],
985
+ "license": "MIT",
986
+ "dependencies": {
987
+ "micromark-util-character": "^2.0.0",
988
+ "micromark-util-encode": "^2.0.0",
989
+ "micromark-util-symbol": "^2.0.0"
990
+ }
991
+ },
992
+ "node_modules/micromark-util-subtokenize": {
993
+ "version": "2.1.0",
994
+ "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
995
+ "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
996
+ "funding": [
997
+ {
998
+ "type": "GitHub Sponsors",
999
+ "url": "https://github.com/sponsors/unifiedjs"
1000
+ },
1001
+ {
1002
+ "type": "OpenCollective",
1003
+ "url": "https://opencollective.com/unified"
1004
+ }
1005
+ ],
1006
+ "license": "MIT",
1007
+ "dependencies": {
1008
+ "devlop": "^1.0.0",
1009
+ "micromark-util-chunked": "^2.0.0",
1010
+ "micromark-util-symbol": "^2.0.0",
1011
+ "micromark-util-types": "^2.0.0"
1012
+ }
1013
+ },
1014
+ "node_modules/micromark-util-symbol": {
1015
+ "version": "2.0.1",
1016
+ "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
1017
+ "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
1018
+ "funding": [
1019
+ {
1020
+ "type": "GitHub Sponsors",
1021
+ "url": "https://github.com/sponsors/unifiedjs"
1022
+ },
1023
+ {
1024
+ "type": "OpenCollective",
1025
+ "url": "https://opencollective.com/unified"
1026
+ }
1027
+ ],
1028
+ "license": "MIT"
1029
+ },
1030
+ "node_modules/micromark-util-types": {
1031
+ "version": "2.0.2",
1032
+ "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
1033
+ "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
1034
+ "funding": [
1035
+ {
1036
+ "type": "GitHub Sponsors",
1037
+ "url": "https://github.com/sponsors/unifiedjs"
1038
+ },
1039
+ {
1040
+ "type": "OpenCollective",
1041
+ "url": "https://opencollective.com/unified"
1042
+ }
1043
+ ],
1044
+ "license": "MIT"
1045
+ },
1046
+ "node_modules/ms": {
1047
+ "version": "2.1.3",
1048
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1049
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1050
+ "license": "MIT"
1051
+ },
1052
+ "node_modules/parse-entities": {
1053
+ "version": "4.0.2",
1054
+ "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
1055
+ "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
1056
+ "license": "MIT",
1057
+ "dependencies": {
1058
+ "@types/unist": "^2.0.0",
1059
+ "character-entities-legacy": "^3.0.0",
1060
+ "character-reference-invalid": "^2.0.0",
1061
+ "decode-named-character-reference": "^1.0.0",
1062
+ "is-alphanumerical": "^2.0.0",
1063
+ "is-decimal": "^2.0.0",
1064
+ "is-hexadecimal": "^2.0.0"
1065
+ },
1066
+ "funding": {
1067
+ "type": "github",
1068
+ "url": "https://github.com/sponsors/wooorm"
1069
+ }
1070
+ },
1071
+ "node_modules/parse-entities/node_modules/@types/unist": {
1072
+ "version": "2.0.11",
1073
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
1074
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
1075
+ "license": "MIT"
1076
+ },
1077
+ "node_modules/remark-mdx": {
1078
+ "version": "3.1.1",
1079
+ "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz",
1080
+ "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==",
1081
+ "license": "MIT",
1082
+ "dependencies": {
1083
+ "mdast-util-mdx": "^3.0.0",
1084
+ "micromark-extension-mdxjs": "^3.0.0"
1085
+ },
1086
+ "funding": {
1087
+ "type": "opencollective",
1088
+ "url": "https://opencollective.com/unified"
1089
+ }
1090
+ },
1091
+ "node_modules/remark-parse": {
1092
+ "version": "11.0.0",
1093
+ "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
1094
+ "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
1095
+ "license": "MIT",
1096
+ "dependencies": {
1097
+ "@types/mdast": "^4.0.0",
1098
+ "mdast-util-from-markdown": "^2.0.0",
1099
+ "micromark-util-types": "^2.0.0",
1100
+ "unified": "^11.0.0"
1101
+ },
1102
+ "funding": {
1103
+ "type": "opencollective",
1104
+ "url": "https://opencollective.com/unified"
1105
+ }
1106
+ },
1107
+ "node_modules/remark-stringify": {
1108
+ "version": "11.0.0",
1109
+ "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
1110
+ "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
1111
+ "license": "MIT",
1112
+ "dependencies": {
1113
+ "@types/mdast": "^4.0.0",
1114
+ "mdast-util-to-markdown": "^2.0.0",
1115
+ "unified": "^11.0.0"
1116
+ },
1117
+ "funding": {
1118
+ "type": "opencollective",
1119
+ "url": "https://opencollective.com/unified"
1120
+ }
1121
+ },
1122
+ "node_modules/stringify-entities": {
1123
+ "version": "4.0.4",
1124
+ "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
1125
+ "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
1126
+ "license": "MIT",
1127
+ "dependencies": {
1128
+ "character-entities-html4": "^2.0.0",
1129
+ "character-entities-legacy": "^3.0.0"
1130
+ },
1131
+ "funding": {
1132
+ "type": "github",
1133
+ "url": "https://github.com/sponsors/wooorm"
1134
+ }
1135
+ },
1136
+ "node_modules/trough": {
1137
+ "version": "2.2.0",
1138
+ "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
1139
+ "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
1140
+ "license": "MIT",
1141
+ "funding": {
1142
+ "type": "github",
1143
+ "url": "https://github.com/sponsors/wooorm"
1144
+ }
1145
+ },
1146
+ "node_modules/unified": {
1147
+ "version": "11.0.5",
1148
+ "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
1149
+ "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
1150
+ "license": "MIT",
1151
+ "dependencies": {
1152
+ "@types/unist": "^3.0.0",
1153
+ "bail": "^2.0.0",
1154
+ "devlop": "^1.0.0",
1155
+ "extend": "^3.0.0",
1156
+ "is-plain-obj": "^4.0.0",
1157
+ "trough": "^2.0.0",
1158
+ "vfile": "^6.0.0"
1159
+ },
1160
+ "funding": {
1161
+ "type": "opencollective",
1162
+ "url": "https://opencollective.com/unified"
1163
+ }
1164
+ },
1165
+ "node_modules/unist-util-is": {
1166
+ "version": "6.0.0",
1167
+ "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz",
1168
+ "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==",
1169
+ "license": "MIT",
1170
+ "dependencies": {
1171
+ "@types/unist": "^3.0.0"
1172
+ },
1173
+ "funding": {
1174
+ "type": "opencollective",
1175
+ "url": "https://opencollective.com/unified"
1176
+ }
1177
+ },
1178
+ "node_modules/unist-util-position-from-estree": {
1179
+ "version": "2.0.0",
1180
+ "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz",
1181
+ "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==",
1182
+ "license": "MIT",
1183
+ "dependencies": {
1184
+ "@types/unist": "^3.0.0"
1185
+ },
1186
+ "funding": {
1187
+ "type": "opencollective",
1188
+ "url": "https://opencollective.com/unified"
1189
+ }
1190
+ },
1191
+ "node_modules/unist-util-stringify-position": {
1192
+ "version": "4.0.0",
1193
+ "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
1194
+ "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
1195
+ "license": "MIT",
1196
+ "dependencies": {
1197
+ "@types/unist": "^3.0.0"
1198
+ },
1199
+ "funding": {
1200
+ "type": "opencollective",
1201
+ "url": "https://opencollective.com/unified"
1202
+ }
1203
+ },
1204
+ "node_modules/unist-util-visit": {
1205
+ "version": "5.0.0",
1206
+ "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
1207
+ "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
1208
+ "license": "MIT",
1209
+ "dependencies": {
1210
+ "@types/unist": "^3.0.0",
1211
+ "unist-util-is": "^6.0.0",
1212
+ "unist-util-visit-parents": "^6.0.0"
1213
+ },
1214
+ "funding": {
1215
+ "type": "opencollective",
1216
+ "url": "https://opencollective.com/unified"
1217
+ }
1218
+ },
1219
+ "node_modules/unist-util-visit-parents": {
1220
+ "version": "6.0.1",
1221
+ "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz",
1222
+ "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==",
1223
+ "license": "MIT",
1224
+ "dependencies": {
1225
+ "@types/unist": "^3.0.0",
1226
+ "unist-util-is": "^6.0.0"
1227
+ },
1228
+ "funding": {
1229
+ "type": "opencollective",
1230
+ "url": "https://opencollective.com/unified"
1231
+ }
1232
+ },
1233
+ "node_modules/vfile": {
1234
+ "version": "6.0.3",
1235
+ "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
1236
+ "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
1237
+ "license": "MIT",
1238
+ "dependencies": {
1239
+ "@types/unist": "^3.0.0",
1240
+ "vfile-message": "^4.0.0"
1241
+ },
1242
+ "funding": {
1243
+ "type": "opencollective",
1244
+ "url": "https://opencollective.com/unified"
1245
+ }
1246
+ },
1247
+ "node_modules/vfile-message": {
1248
+ "version": "4.0.3",
1249
+ "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
1250
+ "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
1251
+ "license": "MIT",
1252
+ "dependencies": {
1253
+ "@types/unist": "^3.0.0",
1254
+ "unist-util-stringify-position": "^4.0.0"
1255
+ },
1256
+ "funding": {
1257
+ "type": "opencollective",
1258
+ "url": "https://opencollective.com/unified"
1259
+ }
1260
+ },
1261
+ "node_modules/zwitch": {
1262
+ "version": "2.0.4",
1263
+ "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
1264
+ "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
1265
+ "license": "MIT",
1266
+ "funding": {
1267
+ "type": "github",
1268
+ "url": "https://github.com/sponsors/wooorm"
1269
+ }
1270
+ }
1271
+ }
1272
+ }
app/scripts/latex-importer/package.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "latex-to-mdx-toolkit",
3
+ "version": "2.0.0",
4
+ "description": "Complete LaTeX to MDX conversion toolkit with advanced references, interactive equations, and Astro components",
5
+ "type": "module",
6
+ "scripts": {
7
+ "convert": "node index.mjs",
8
+ "convert:clean": "node index.mjs --clean",
9
+ "convert:mdx": "node mdx-converter.mjs",
10
+ "clean:bib": "node bib-cleaner.mjs",
11
+ "postprocess": "node post-processor.mjs",
12
+ "postprocess:verbose": "node post-processor.mjs --verbose"
13
+ },
14
+ "dependencies": {
15
+ "unified": "^11.0.4",
16
+ "remark-parse": "^11.0.0",
17
+ "remark-mdx": "^3.0.0",
18
+ "remark-stringify": "^11.0.0"
19
+ },
20
+ "keywords": [
21
+ "latex",
22
+ "mdx",
23
+ "astro",
24
+ "katex",
25
+ "references",
26
+ "equations",
27
+ "pandoc",
28
+ "conversion",
29
+ "scientific-writing"
30
+ ],
31
+ "author": "LaTeX-to-MDX Toolkit",
32
+ "license": "MIT"
33
+ }
app/scripts/latex-importer/post-processor.mjs ADDED
@@ -0,0 +1,439 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from 'fs';
4
+ import { join, dirname } from 'path';
5
+ import { fileURLToPath } from 'url';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ /**
11
+ * Post-processor for cleaning Markdown content from LaTeX conversion
12
+ * Each function handles a specific type of cleanup for maintainability
13
+ */
14
+
15
+ /**
16
+ * Remove TeX low-level grouping commands that break KaTeX
17
+ * @param {string} content - Markdown content
18
+ * @returns {string} - Cleaned content
19
+ */
20
+ function removeTexGroupingCommands(content) {
21
+ console.log(' 🧹 Removing TeX grouping commands...');
22
+
23
+ return content
24
+ .replace(/\\mathopen\{\}\\mathclose\\bgroup/g, '')
25
+ .replace(/\\aftergroup\\egroup/g, '')
26
+ .replace(/\\bgroup/g, '')
27
+ .replace(/\\egroup/g, '');
28
+ }
29
+
30
+ /**
31
+ * Simplify LaTeX delimiter constructions
32
+ * @param {string} content - Markdown content
33
+ * @returns {string} - Cleaned content
34
+ */
35
+ function simplifyLatexDelimiters(content) {
36
+ console.log(' 🔧 Simplifying LaTeX delimiters...');
37
+
38
+ return content
39
+ .replace(/\\left\[\s*/g, '[')
40
+ .replace(/\s*\\right\]/g, ']');
41
+ }
42
+
43
+ /**
44
+ * Remove orphaned LaTeX labels
45
+ * @param {string} content - Markdown content
46
+ * @returns {string} - Cleaned content
47
+ */
48
+ function removeOrphanedLabels(content) {
49
+ console.log(' 🏷️ Removing orphaned labels...');
50
+
51
+ return content
52
+ .replace(/^\s*\\label\{[^}]+\}\s*$/gm, '')
53
+ .replace(/\\label\{[^}]+\}/g, '');
54
+ }
55
+
56
+ /**
57
+ * Fix KaTeX-incompatible math commands
58
+ * @param {string} content - Markdown content
59
+ * @returns {string} - Cleaned content
60
+ */
61
+ function fixMathCommands(content) {
62
+ console.log(' 📐 Fixing KaTeX-incompatible math commands...');
63
+
64
+ return content
65
+ // Replace \hdots with \ldots (KaTeX compatible)
66
+ .replace(/\\hdots/g, '\\ldots')
67
+ // Add more math command fixes here as needed
68
+ .replace(/\\vdots/g, '\\vdots'); // This one should be fine, but kept for consistency
69
+ }
70
+
71
+ /**
72
+ * Convert LaTeX matrix commands to KaTeX-compatible environments
73
+ * @param {string} content - Markdown content
74
+ * @returns {string} - Content with fixed matrix commands
75
+ */
76
+ function fixMatrixCommands(content) {
77
+ console.log(' 🔢 Converting matrix commands to KaTeX format...');
78
+
79
+ let fixedCount = 0;
80
+
81
+ // Convert \pmatrix{...} to \begin{pmatrix}...\end{pmatrix}
82
+ content = content.replace(/\\pmatrix\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g, (match, matrixContent) => {
83
+ fixedCount++;
84
+ // Split by \\ for rows, handle nested braces
85
+ const rows = matrixContent.split('\\\\').map(row => row.trim()).filter(row => row);
86
+ return `\\begin{pmatrix}\n${rows.join(' \\\\\n')}\n\\end{pmatrix}`;
87
+ });
88
+
89
+ // Convert \bmatrix{...} to \begin{bmatrix}...\end{bmatrix}
90
+ content = content.replace(/\\bmatrix\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g, (match, matrixContent) => {
91
+ fixedCount++;
92
+ const rows = matrixContent.split('\\\\').map(row => row.trim()).filter(row => row);
93
+ return `\\begin{bmatrix}\n${rows.join(' \\\\\n')}\n\\end{bmatrix}`;
94
+ });
95
+
96
+ // Convert \vmatrix{...} to \begin{vmatrix}...\end{vmatrix}
97
+ content = content.replace(/\\vmatrix\{([^{}]*(?:\{[^{}]*\}[^{}]*)*)\}/g, (match, matrixContent) => {
98
+ fixedCount++;
99
+ const rows = matrixContent.split('\\\\').map(row => row.trim()).filter(row => row);
100
+ return `\\begin{vmatrix}\n${rows.join(' \\\\\n')}\n\\end{vmatrix}`;
101
+ });
102
+
103
+ if (fixedCount > 0) {
104
+ console.log(` ✅ Fixed ${fixedCount} matrix command(s)`);
105
+ }
106
+
107
+ return content;
108
+ }
109
+
110
+ /**
111
+ * Fix Unicode characters that break MDX/JSX parsing
112
+ * @param {string} content - Markdown content
113
+ * @returns {string} - Cleaned content
114
+ */
115
+ function fixUnicodeIssues(content) {
116
+ console.log(' 🌐 Fixing Unicode characters for MDX compatibility...');
117
+
118
+ return content
119
+ // Replace Unicode middle dot (·) with \cdot in math expressions
120
+ .replace(/\$([^$]*?)·([^$]*?)\$/g, (match, before, after) => {
121
+ return `$${before}\\cdot${after}$`;
122
+ })
123
+ // Replace Unicode middle dot in display math
124
+ .replace(/\$\$([^$]*?)·([^$]*?)\$\$/g, (match, before, after) => {
125
+ return `$$${before}\\cdot${after}$$`;
126
+ })
127
+ // Replace other problematic Unicode characters
128
+ .replace(/[""]/g, '"') // Smart quotes to regular quotes
129
+ .replace(/['']/g, "'") // Smart apostrophes to regular apostrophes
130
+ .replace(/…/g, '...') // Ellipsis to three dots
131
+ .replace(/–/g, '-') // En dash to hyphen
132
+ .replace(/—/g, '--'); // Em dash to double hyphen
133
+ }
134
+
135
+ /**
136
+ * Fix multiline math expressions for MDX compatibility
137
+ * @param {string} content - Markdown content
138
+ * @returns {string} - Cleaned content
139
+ */
140
+ function fixMultilineMath(content) {
141
+ console.log(' 📏 Fixing multiline math expressions for MDX...');
142
+
143
+ return content
144
+ // Convert multiline inline math to display math blocks (more precise regex)
145
+ // Only match if the content is a self-contained math expression within a single line
146
+ .replace(/\$([^$\n]*\\\\[^$\n]*)\$/g, (match, mathContent) => {
147
+ // Only convert if it contains actual math operators and line breaks
148
+ if (mathContent.includes('\\\\') && /[=+\-*/^_{}]/.test(mathContent)) {
149
+ // Remove leading/trailing whitespace and normalize newlines
150
+ const cleanedMath = mathContent
151
+ .replace(/^\s+|\s+$/g, '')
152
+ .replace(/\s*\\\\\s*/g, '\\\\\n ');
153
+ return `$$\n${cleanedMath}\n$$`;
154
+ }
155
+ return match; // Keep original if it doesn't look like multiline math
156
+ })
157
+ // Ensure display math blocks are properly separated
158
+ .replace(/\$\$\s*\n\s*([^$]+?)\s*\n\s*\$\$/g, (match, mathContent) => {
159
+ return `\n$$\n${mathContent.trim()}\n$$\n`;
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Inject code snippets into empty code blocks
165
+ * @param {string} content - Markdown content
166
+ * @param {string} inputDir - Directory containing the LaTeX source and snippets
167
+ * @returns {string} - Content with injected code snippets
168
+ */
169
+ function injectCodeSnippets(content, inputDir = null) {
170
+ console.log(' 💻 Injecting code snippets...');
171
+
172
+ if (!inputDir) {
173
+ console.log(' ⚠️ No input directory provided, skipping code injection');
174
+ return content;
175
+ }
176
+
177
+ const snippetsDir = join(inputDir, 'snippets');
178
+
179
+ if (!existsSync(snippetsDir)) {
180
+ console.log(' ⚠️ Snippets directory not found, skipping code injection');
181
+ return content;
182
+ }
183
+
184
+ // Get all available snippet files
185
+ let availableSnippets = [];
186
+ try {
187
+ availableSnippets = readdirSync(snippetsDir);
188
+ console.log(` 📁 Found ${availableSnippets.length} snippet file(s): ${availableSnippets.join(', ')}`);
189
+ } catch (error) {
190
+ console.log(` ❌ Error reading snippets directory: ${error.message}`);
191
+ return content;
192
+ }
193
+
194
+ // Find all empty code blocks
195
+ const emptyCodeBlockPattern = /```\s*(\w+)\s*\n\s*```/g;
196
+
197
+ let processedContent = content;
198
+ let injectionCount = 0;
199
+
200
+ processedContent = processedContent.replace(emptyCodeBlockPattern, (match, language) => {
201
+ // Map language names to file extensions
202
+ const extensionMap = {
203
+ 'python': 'py',
204
+ 'javascript': 'js',
205
+ 'typescript': 'ts',
206
+ 'bash': 'sh',
207
+ 'shell': 'sh'
208
+ };
209
+
210
+ const fileExtension = extensionMap[language] || language;
211
+
212
+ // Try to find a matching snippet file for this language
213
+ const matchingFiles = availableSnippets.filter(file =>
214
+ file.endsWith(`.${fileExtension}`)
215
+ );
216
+
217
+ if (matchingFiles.length === 0) {
218
+ console.log(` ⚠️ No ${language} snippet found (looking for .${fileExtension})`);
219
+ return match;
220
+ }
221
+
222
+ // Use the first matching file (could be made smarter with context analysis)
223
+ const selectedFile = matchingFiles[0];
224
+ const snippetPath = join(snippetsDir, selectedFile);
225
+
226
+ try {
227
+ const snippetContent = readFileSync(snippetPath, 'utf8');
228
+ injectionCount++;
229
+ console.log(` ✅ Injected: ${selectedFile}`);
230
+ return `\`\`\`${language}\n${snippetContent.trim()}\n\`\`\``;
231
+ } catch (error) {
232
+ console.log(` ❌ Error reading ${selectedFile}: ${error.message}`);
233
+ return match;
234
+ }
235
+ });
236
+
237
+ if (injectionCount > 0) {
238
+ console.log(` 📊 Injected ${injectionCount} code snippet(s)`);
239
+ }
240
+
241
+ return processedContent;
242
+ }
243
+
244
+ /**
245
+ * Fix all attributes that still contain colons (href, data-reference, id)
246
+ * @param {string} content - Markdown content
247
+ * @returns {string} - Cleaned content
248
+ */
249
+ function fixAllAttributes(content) {
250
+ console.log(' 🔗 Fixing all attributes with colons...');
251
+
252
+ let fixedCount = 0;
253
+
254
+ // Fix href attributes containing colons
255
+ content = content.replace(/href="([^"]*):([^"]*)"/g, (match, before, after) => {
256
+ fixedCount++;
257
+ return `href="${before}-${after}"`;
258
+ });
259
+
260
+ // Fix data-reference attributes containing colons
261
+ content = content.replace(/data-reference="([^"]*):([^"]*)"/g, (match, before, after) => {
262
+ fixedCount++;
263
+ return `data-reference="${before}-${after}"`;
264
+ });
265
+
266
+ // Fix id attributes containing colons (like in Figure components)
267
+ content = content.replace(/id="([^"]*):([^"]*)"/g, (match, before, after) => {
268
+ fixedCount++;
269
+ return `id="${before}-${after}"`;
270
+ });
271
+
272
+ if (fixedCount > 0) {
273
+ console.log(` ✅ Fixed ${fixedCount} attribute(s) with colons`);
274
+ }
275
+
276
+ return content;
277
+ }
278
+
279
+ /**
280
+ * Fix link text content that still contains colons
281
+ * @param {string} content - Markdown content
282
+ * @returns {string} - Cleaned content
283
+ */
284
+ function fixLinkTextContent(content) {
285
+ console.log(' 📝 Fixing link text content with colons...');
286
+
287
+ let fixedCount = 0;
288
+
289
+ // Fix text content within links that contain references with colons
290
+ // Pattern: <a ...>[text:content]</a>
291
+ const cleanedContent = content.replace(/<a([^>]*)>\[([^:]*):([^\]]*)\]<\/a>/g, (match, attributes, before, after) => {
292
+ fixedCount++;
293
+ return `<a${attributes}>[${before}-${after}]</a>`;
294
+ });
295
+
296
+ if (fixedCount > 0) {
297
+ console.log(` ✅ Fixed ${fixedCount} link text(s) with colons`);
298
+ }
299
+
300
+ return cleanedContent;
301
+ }
302
+
303
+ /**
304
+ * Convert align anchor markers to proper HTML spans outside math blocks
305
+ * @param {string} content - Markdown content
306
+ * @returns {string} - Content with converted anchor spans
307
+ */
308
+ function convertAlignAnchors(content) {
309
+ console.log(' 🏷️ Converting align anchor markers to HTML spans...');
310
+
311
+ let convertedCount = 0;
312
+
313
+ // Find and replace align anchor markers with proper spans outside math blocks
314
+ content = content.replace(/``` math\n%%ALIGN_ANCHOR_ID\{([^}]+)\}%%\n([\s\S]*?)\n```/g, (match, anchorId, mathContent) => {
315
+ convertedCount++;
316
+ return `<span id="${anchorId}" style="position: absolute;"></span>\n\n\`\`\` math\n${mathContent}\n\`\`\``;
317
+ });
318
+
319
+ if (convertedCount > 0) {
320
+ console.log(` ✅ Converted ${convertedCount} align anchor marker(s) to spans`);
321
+ }
322
+
323
+ return content;
324
+ }
325
+
326
+ /**
327
+ * Main post-processing function that applies all cleanup steps
328
+ * @param {string} content - Raw Markdown content from Pandoc
329
+ * @param {string} inputDir - Optional: Directory containing LaTeX source for code injection
330
+ * @returns {string} - Cleaned Markdown content
331
+ */
332
+ export function postProcessMarkdown(content, inputDir = null) {
333
+ console.log('🔧 Post-processing for KaTeX compatibility...');
334
+
335
+ let processedContent = content;
336
+
337
+ // Apply each cleanup step sequentially
338
+ processedContent = removeTexGroupingCommands(processedContent);
339
+ processedContent = simplifyLatexDelimiters(processedContent);
340
+ processedContent = removeOrphanedLabels(processedContent);
341
+ processedContent = convertAlignAnchors(processedContent);
342
+ processedContent = fixMathCommands(processedContent);
343
+ processedContent = fixMatrixCommands(processedContent);
344
+ processedContent = fixUnicodeIssues(processedContent);
345
+ processedContent = fixMultilineMath(processedContent);
346
+ processedContent = fixAllAttributes(processedContent);
347
+ processedContent = fixLinkTextContent(processedContent);
348
+
349
+ // Inject code snippets if input directory is provided
350
+ if (inputDir) {
351
+ processedContent = injectCodeSnippets(processedContent, inputDir);
352
+ }
353
+
354
+ return processedContent;
355
+ }
356
+
357
+ /**
358
+ * CLI interface for standalone usage
359
+ */
360
+ function parseArgs() {
361
+ const args = process.argv.slice(2);
362
+ const config = {
363
+ input: join(__dirname, 'output', 'main.md'),
364
+ output: null, // Will default to input if not specified
365
+ verbose: false,
366
+ };
367
+
368
+ for (const arg of args) {
369
+ if (arg.startsWith('--input=')) {
370
+ config.input = arg.substring('--input='.length);
371
+ } else if (arg.startsWith('--output=')) {
372
+ config.output = arg.substring('--output='.length);
373
+ } else if (arg === '--verbose') {
374
+ config.verbose = true;
375
+ } else if (arg === '--help' || arg === '-h') {
376
+ console.log(`
377
+ 🔧 Markdown Post-Processor
378
+
379
+ Usage:
380
+ node post-processor.mjs [options]
381
+
382
+ Options:
383
+ --input=PATH Input Markdown file (default: output/main.md)
384
+ --output=PATH Output file (default: overwrites input)
385
+ --verbose Verbose output
386
+ --help, -h Show this help
387
+
388
+ Examples:
389
+ # Process main.md in-place
390
+ node post-processor.mjs
391
+
392
+ # Process with custom paths
393
+ node post-processor.mjs --input=raw.md --output=clean.md
394
+ `);
395
+ process.exit(0);
396
+ }
397
+ }
398
+
399
+ // Default output to input if not specified
400
+ if (!config.output) {
401
+ config.output = config.input;
402
+ }
403
+
404
+ return config;
405
+ }
406
+
407
+ function main() {
408
+ const config = parseArgs();
409
+
410
+ console.log('🔧 Markdown Post-Processor');
411
+ console.log(`📁 Input: ${config.input}`);
412
+ console.log(`📁 Output: ${config.output}`);
413
+
414
+ try {
415
+ const content = readFileSync(config.input, 'utf8');
416
+ const processedContent = postProcessMarkdown(content);
417
+
418
+ writeFileSync(config.output, processedContent);
419
+
420
+ console.log(`✅ Post-processing completed: ${config.output}`);
421
+
422
+ // Show stats if verbose
423
+ if (config.verbose) {
424
+ const originalLines = content.split('\n').length;
425
+ const processedLines = processedContent.split('\n').length;
426
+ console.log(`📊 Lines: ${originalLines} → ${processedLines}`);
427
+ }
428
+
429
+ } catch (error) {
430
+ console.error('❌ Post-processing failed:');
431
+ console.error(error.message);
432
+ process.exit(1);
433
+ }
434
+ }
435
+
436
+ // Run CLI if called directly
437
+ if (import.meta.url === `file://${process.argv[1]}`) {
438
+ main();
439
+ }
app/scripts/latex-importer/reference-preprocessor.mjs ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * LaTeX Reference Preprocessor
5
+ *
6
+ * This module cleans up LaTeX references BEFORE Pandoc conversion to ensure
7
+ * consistent, MDX-compatible identifiers throughout the document.
8
+ *
9
+ * What it does:
10
+ * - Removes prefixes from labels: \label{sec:intro} → \label{sec-intro}
11
+ * - Updates corresponding refs: \ref{sec:intro} → \ref{sec-intro}
12
+ * - Handles all reference types: sec:, fig:, eq:, table:, etc.
13
+ * - Maintains consistency between labels and references
14
+ */
15
+
16
+ /**
17
+ * Extract all references from LaTeX content
18
+ * @param {string} content - LaTeX content
19
+ * @returns {Object} - Object with labels and refs arrays
20
+ */
21
+ function extractReferences(content) {
22
+ const references = {
23
+ labels: new Set(),
24
+ refs: new Set(),
25
+ cites: new Set()
26
+ };
27
+
28
+ // Find all \label{...} commands
29
+ const labelMatches = content.matchAll(/\\label\{([^}]+)\}/g);
30
+ for (const match of labelMatches) {
31
+ references.labels.add(match[1]);
32
+ }
33
+
34
+ // Find all \ref{...} commands
35
+ const refMatches = content.matchAll(/\\ref\{([^}]+)\}/g);
36
+ for (const match of refMatches) {
37
+ references.refs.add(match[1]);
38
+ }
39
+
40
+ // Find all \cite{...} commands (already handled in existing code but included for completeness)
41
+ const citeMatches = content.matchAll(/\\cite[tp]?\{([^}]+)\}/g);
42
+ for (const match of citeMatches) {
43
+ // Handle multiple citations: \cite{ref1,ref2,ref3}
44
+ const citations = match[1].split(',').map(cite => cite.trim());
45
+ citations.forEach(cite => references.cites.add(cite));
46
+ }
47
+
48
+ return references;
49
+ }
50
+
51
+ /**
52
+ * Create clean identifier mapping
53
+ * @param {Object} references - References object from extractReferences
54
+ * @returns {Map} - Mapping from original to clean identifiers
55
+ */
56
+ function createCleanMapping(references) {
57
+ const mapping = new Map();
58
+
59
+ // Create mapping for all unique identifiers
60
+ const allIdentifiers = new Set([
61
+ ...references.labels,
62
+ ...references.refs
63
+ ]);
64
+
65
+ for (const id of allIdentifiers) {
66
+ // Remove common prefixes and replace colons with dashes
67
+ let cleanId = id
68
+ .replace(/^(sec|section|ch|chapter|fig|figure|eq|equation|tab|table|lst|listing|app|appendix):/gi, '')
69
+ .replace(/:/g, '-')
70
+ .replace(/[^a-zA-Z0-9_-]/g, '-') // Replace any other problematic characters
71
+ .replace(/-+/g, '-') // Collapse multiple dashes
72
+ .replace(/^-|-$/g, ''); // Remove leading/trailing dashes
73
+
74
+ // Ensure we don't have empty identifiers
75
+ if (!cleanId) {
76
+ cleanId = id.replace(/:/g, '-');
77
+ }
78
+
79
+ mapping.set(id, cleanId);
80
+ }
81
+
82
+ return mapping;
83
+ }
84
+
85
+ /**
86
+ * Convert labels to HTML anchor spans for better MDX compatibility
87
+ * @param {string} content - LaTeX content
88
+ * @param {Map} mapping - Identifier mapping (original -> clean)
89
+ * @returns {Object} - Result with content and count of conversions
90
+ */
91
+ function convertLabelsToAnchors(content, mapping) {
92
+ let processedContent = content;
93
+ let anchorsCreated = 0;
94
+
95
+ // Replace \label{...} with HTML anchor spans, but SKIP labels inside math environments
96
+ for (const [original, clean] of mapping) {
97
+ // Skip equation labels (they will be handled by the Lua filter)
98
+ if (original.startsWith('eq:')) {
99
+ continue;
100
+ }
101
+
102
+ const labelRegex = new RegExp(`\\\\label\\{${escapeRegex(original)}\\}`, 'g');
103
+ const labelMatches = processedContent.match(labelRegex);
104
+
105
+ if (labelMatches) {
106
+ // Replace \label{original} with HTML span anchor (invisible but accessible)
107
+ processedContent = processedContent.replace(labelRegex, `\n\n<span id="${clean}" style="position: absolute;"></span>\n\n`);
108
+ anchorsCreated += labelMatches.length;
109
+ }
110
+ }
111
+
112
+ return { content: processedContent, anchorsCreated };
113
+ }
114
+
115
+ /**
116
+ * Convert \highlight{...} commands to HTML spans with CSS class
117
+ * @param {string} content - LaTeX content
118
+ * @returns {Object} - Result with content and count of conversions
119
+ */
120
+ function convertHighlightCommands(content) {
121
+ let processedContent = content;
122
+ let highlightsConverted = 0;
123
+
124
+ // Replace \highlight{...} with <span class="highlight">...</span>
125
+ processedContent = processedContent.replace(/\\highlight\{([^}]+)\}/g, (match, text) => {
126
+ highlightsConverted++;
127
+ return `<span class="highlight">${text}</span>`;
128
+ });
129
+
130
+ return { content: processedContent, highlightsConverted };
131
+ }
132
+
133
+ /**
134
+ * Apply mapping to LaTeX content
135
+ * @param {string} content - Original LaTeX content
136
+ * @param {Map} mapping - Identifier mapping
137
+ * @returns {string} - Cleaned LaTeX content
138
+ */
139
+ function applyMapping(content, mapping) {
140
+ let cleanedContent = content;
141
+ let changesCount = 0;
142
+
143
+ // First, convert labels to anchor spans
144
+ const anchorResult = convertLabelsToAnchors(cleanedContent, mapping);
145
+ cleanedContent = anchorResult.content;
146
+ const anchorsCreated = anchorResult.anchorsCreated;
147
+
148
+ // Convert \highlight{} commands to spans
149
+ const highlightResult = convertHighlightCommands(cleanedContent);
150
+ cleanedContent = highlightResult.content;
151
+ const highlightsConverted = highlightResult.highlightsConverted;
152
+
153
+ // Then apply mapping to remaining references and equation labels
154
+ for (const [original, clean] of mapping) {
155
+ if (original !== clean) {
156
+ // Replace \ref{original} with \ref{clean}
157
+ const refRegex = new RegExp(`\\\\ref\\{${escapeRegex(original)}\\}`, 'g');
158
+ const refMatches = cleanedContent.match(refRegex);
159
+ if (refMatches) {
160
+ cleanedContent = cleanedContent.replace(refRegex, `\\ref{${clean}}`);
161
+ changesCount += refMatches.length;
162
+ }
163
+
164
+ // For equation labels, still clean the labels themselves (for the Lua filter)
165
+ if (original.startsWith('eq:')) {
166
+ const labelRegex = new RegExp(`\\\\label\\{${escapeRegex(original)}\\}`, 'g');
167
+ const labelMatches = cleanedContent.match(labelRegex);
168
+ if (labelMatches) {
169
+ cleanedContent = cleanedContent.replace(labelRegex, `\\label{${clean}}`);
170
+ changesCount += labelMatches.length;
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ return {
177
+ content: cleanedContent,
178
+ changesCount: changesCount + anchorsCreated,
179
+ highlightsConverted: highlightsConverted
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Escape special regex characters
185
+ * @param {string} string - String to escape
186
+ * @returns {string} - Escaped string
187
+ */
188
+ function escapeRegex(string) {
189
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
190
+ }
191
+
192
+ /**
193
+ * Main preprocessing function
194
+ * @param {string} latexContent - Original LaTeX content
195
+ * @returns {Object} - Result with cleaned content and statistics
196
+ */
197
+ export function preprocessLatexReferences(latexContent) {
198
+ console.log('🔧 Preprocessing LaTeX references for MDX compatibility...');
199
+
200
+ // 1. Extract all references
201
+ const references = extractReferences(latexContent);
202
+
203
+ console.log(` 📊 Found: ${references.labels.size} labels, ${references.refs.size} refs`);
204
+
205
+ // 2. Create clean mapping
206
+ const mapping = createCleanMapping(references);
207
+
208
+ // 3. Apply mapping
209
+ const result = applyMapping(latexContent, mapping);
210
+
211
+ if (result.changesCount > 0) {
212
+ console.log(` ✅ Processed ${result.changesCount} reference(s) and created anchor spans`);
213
+
214
+ // Show some examples of changes
215
+ let exampleCount = 0;
216
+ for (const [original, clean] of mapping) {
217
+ if (original !== clean && exampleCount < 3) {
218
+ console.log(` ${original} → ${clean} (span + refs)`);
219
+ exampleCount++;
220
+ }
221
+ }
222
+ if (mapping.size > 3) {
223
+ console.log(` ... and ${mapping.size - 3} more anchor spans created`);
224
+ }
225
+ } else {
226
+ console.log(' ℹ️ No reference cleanup needed');
227
+ }
228
+
229
+ if (result.highlightsConverted > 0) {
230
+ console.log(` ✨ Converted ${result.highlightsConverted} \\highlight{} command(s) to <span class="highlight">`);
231
+ }
232
+
233
+ return {
234
+ content: result.content,
235
+ changesCount: result.changesCount,
236
+ mapping: mapping,
237
+ references: references
238
+ };
239
+ }
app/scripts/notion-importer/.cursorignore ADDED
@@ -0,0 +1 @@
 
 
1
+ .env