# Semantic Markdown Parser This document describes the reusable parser contract for the blog-family Markdown pipeline. It is intentionally project-independent: the same ideas should be portable to future site families such as `neeldhara.courses` and `books.neeldhara.com`. The design goal is a single Markdown source that renders predictably to: - Astro HTML, with React interactives where needed. - PDF, through Pandoc and LuaLaTeX. - Obsidian preview, with syntax that remains legible even when Obsidian does not understand a custom semantic feature. ## Principles The source format is mostly ordinary Markdown. Custom syntax is introduced only when the output needs semantic information that plain Markdown cannot carry. Use these rules when adding new syntax: - Prefer fenced attributes for semantic blocks. Pandoc understands them, and they are easy to parse in Astro. - Keep labels explicit. A label such as `thm:hall` should map to a LaTeX `\label{thm:hall}` and an HTML `id="thm:hall"`. - Use normal Markdown links for references. `[Hall's theorem](#thm:hall)` becomes an HTML anchor in Astro and `\cref{thm:hall}` in PDF. - Make HTML and PDF fallbacks explicit for interactive material. - Keep Obsidian-readable text in the source. A reader opening the vault should understand the note even without the final renderers. ## Current Implementation The parser is split across an HTML path and a PDF path. HTML path: ```text Astro Markdown/MDX -> remark-gfm -> remarkInlineFootnotes -> remarkCodeFenceLanguages -> remarkSemanticBlocks -> remark-math -> remarkTypography -> remarkCallouts -> remarkSymbols -> rehype-katex -> Astro pages ``` PDF path: ```text Markdown -> Pandoc markdown reader -> scripts/pandoc-filters/semantic-blocks.lua -> LuaLaTeX -> scripts/pandoc-templates/semantic-preamble.tex ``` The HTML parser lives per site at: ```text sites//src/remark/ ``` The current files are duplicated across the seven blog sites. When reusing this system in another project, prefer extracting these files into a small shared package or copying them together as a unit. The PDF parser lives at: ```text scripts/pandoc-filters/semantic-blocks.lua scripts/pandoc-templates/semantic-preamble.tex scripts/export-pdf.mjs ``` ## Dependencies Astro-side dependencies: ```text @astrojs/mdx @astrojs/react rehype-katex remark-gfm remark-math mdast-util-from-markdown ``` PDF-side dependencies: ```text pandoc lualatex amsmath amssymb amsthm cleveref tcolorbox emoji chessfss xcolor ``` For TeX Live installations, make sure the `emoji`, `chessfss`, `tcolorbox`, and `cleveref` packages are installed. ## Base Markdown The expected source is normal Markdown with YAML frontmatter: ```markdown --- title: "Example" description: "A short summary." pubDate: "2026-01-31" --- Body text. ``` Supported common Markdown features: - Headings. - Lists. - Tables via `remark-gfm` in HTML and Pandoc's Markdown reader in PDF. - Footnotes. - Inline math with `$...$`. - Display math with `$$...$$`. - Code fences. - Raw HTML for web-only fragments. Use colocated relative assets: ```markdown ![A small diagram](diagram.png) ``` Avoid raw LaTeX in source unless the post is intended only for PDF. ## Typography Write three hyphens for an em dash: ```markdown This is one thought --- and this is another. ``` HTML converts text-node `---` into `—` through `remarkTypography`. PDF leaves `---` in the generated LaTeX, where LuaLaTeX typesets it as an em dash. This means the source stays ASCII-friendly while both outputs display the proper dash. ## Callouts Callouts are portable blockquotes. The callout type controls styling but does not create a visible title by itself. Supported types: ```text note tip warning caution aside ``` Canonical titleless form: ```markdown > **Note** > > This is a note without a visible heading. ``` Obsidian-style titleless form: ```markdown > [!tip] > This is a titleless tip. ``` Explicit title form: ```markdown > [!warning: Check this assumption] > > This warning has a visible title. ``` The pipe spelling is equivalent: ```markdown > [!warning|Check this assumption] > > This warning has a visible title. ``` Do not use this form for a title: ```markdown > [!tip] Useful observation ``` That is intentionally interpreted as a titleless callout whose body begins with "Useful observation". This rule keeps parsing unambiguous across Obsidian, Astro, and Pandoc. Quick marker forms are supported: ```markdown > 📝 This becomes a note. > 💡 This becomes a tip. > ⚡ This becomes a tip. > ⚠️ This becomes a warning. ``` HTML output: ```html ``` If no explicit title is present, no `.callout-title` paragraph is emitted. PDF output: ```tex \begin{blogwarningbox}{Check this assumption} ... \end{blogwarningbox} ``` If no explicit title is present, the second environment argument is empty and the `tcolorbox` title is omitted. ## Semantic Environments Use semantic environments for theorem-like content, definitions, examples, and proofs. Canonical form: ````markdown ```{.env .theorem #thm:hall title="Hall's theorem" html="callout"} Every bipartite graph satisfying Hall's condition has a matching that covers the left side. ``` ```` Supported environment types: ```text theorem lemma proposition corollary definition example remark proof ``` Attributes: | Attribute | Required | Meaning | | ---------------- | ----------- | -------------------------------------------- | | `.env` | Recommended | Marks this as a semantic environment. | | `.` | Yes | One of the supported environment types. | | `#label` | Recommended | Cross-reference label and HTML id. | | `title="..."` | Optional | Environment title. | | `html="callout"` | Optional | Render as a styled block in HTML. | | `html="plain"` | Optional | Render as a quieter left-rule block in HTML. | Default HTML mode: - Theorem-like blocks default to `html="callout"`. - `proof` defaults to `html="plain"`. HTML output: ```html

Theorem (Hall's theorem)

...
``` PDF output: ```tex \begin{theorem}[Hall's theorem] \label{thm:hall} ... \end{theorem} ``` Short HTML-only form: ````markdown ```theorem id="thm:hall" title="Hall's theorem" Every bipartite graph satisfying Hall's condition has a matching... ``` ```` Use the canonical Pandoc-style form for any material that should export to PDF. ## Data Tables Use data tables when a post should keep rows in a colocated YAML or JSON file but render as a searchable, sortable table in Astro. Canonical form: ````markdown ```{.data-table #tbl:resources src="resources.yml" columns="type,title,summary" headers="type:Type,title:Resource,summary:Notes" link="title" number="slno" caption="Resources" search-placeholder="Search resources"} The HTML version renders this as a searchable, sortable table. In Obsidian, edit `resources.yml` to update the rows. ``` ```` Attributes: | Attribute | Required | Meaning | | ------------------------------ | -------- | ---------------------------------------------------------- | | `.data-table` | Yes | Marks this as a data-backed table. | | `#tbl:` | Optional | HTML id and future cross-reference label. | | `src="resources.yml"` | Yes | YAML or JSON file path, resolved relative to the Markdown. | | `columns="type,title,summary"` | Optional | Visible columns and their order. | | `headers="key:Label,..."` | Optional | Human-readable labels for selected columns. | | `link="title"` | Optional | Column whose value links through the row's URL field. | | `url="link"` | Optional | URL field used by the linked column; defaults to `link`. | | `number="slno"` | Optional | Prefix the linked column with a row number field. | | `caption="Resources"` | Optional | Visible toolbar title and accessible table caption. | | `search="false"` | Optional | Disable local table search; enabled by default. | | `sort="false"` | Optional | Disable sortable headers; enabled by default. | | `search-placeholder="Search"` | Optional | Placeholder text for the search input. | | `pdf="Fallback text"` | Optional | PDF-specific fallback text. | | `fallback="Fallback text"` | Optional | General fallback text when `pdf` is absent. | The data file should contain either a top-level array: ```yaml - slno: 01 title: "Cognitive Productivity" summary: "Book by Luc P. Beaudoin." link: https://leanpub.com/cognitiveproductivity/ type: book ``` or an object with an `items` array: ```yaml items: - title: "Cognitive Productivity" summary: "Book by Luc P. Beaudoin." link: https://leanpub.com/cognitiveproductivity/ type: book ``` HTML output: ```html

Resources

...
``` The generated table is static HTML. The inline script only adds local search and sort behavior; it does not fetch data at runtime. PDF output: ```tex \begin{blogasidebox}{Resources} The HTML version renders a searchable, sortable data table from `resources.yml`. \end{blogasidebox} ``` If the code fence has body text, or `pdf="..."`, that text is used as the PDF fallback. Full YAML-to-LaTeX table rendering is deliberately not implemented yet because long prose fields usually typeset better as curated PDF content. Obsidian behavior: - The fence stays readable in the note. - The YAML file remains a normal sidecar file you can edit directly. - Obsidian does not render the enhanced table unless a future vault plugin adds a preview renderer for `.data-table`. ## Interactive Blocks Interactive blocks reserve a place for a React component in HTML and provide a text fallback for PDF and non-JavaScript output. Canonical form: ````markdown ```{.interactive #int:tape-layout caption="Tape layout explorer"} Drag files along a tape and compare the access cost of each arrangement. ``` ```` Attributes: | Attribute | Required | Meaning | | ----------------- | -------- | ---------------------------------------------------- | | `.interactive` | Yes | Marks this as an interactive block. | | `#int:` | Yes | Semantic label. The `int:` prefix is for references. | | `caption="..."` | Optional | Figure caption in HTML and PDF. | | `title="..."` | Optional | Fallback caption when `caption` is absent. | | `component="..."` | Optional | Override the component id. | | `pdf="..."` | Optional | PDF-specific replacement text. | | `fallback="..."` | Optional | General fallback text. | Component resolution: ```text #int:tape-layout -> component id: tape-layout -> component path: sites//src/interactives/tape-layout/index.tsx ``` HTML output: ```html

...

Tape layout explorer
``` The runtime loaded by `InteractiveRuntime.astro` imports: ```text sites//src/interactives/runtime.tsx ``` The runtime uses `import.meta.glob("./*/index.tsx")`, so each interactive component should live in its own directory with an `index.tsx` entrypoint. Component props: ```ts type InteractiveProps = { id: string; label: string; caption: string; description: string; fallback: string; attrs: Record; }; ``` PDF output: ```tex \begin{figure}[htbp] \centering \fbox{\begin{minipage}{0.86\linewidth} Fallback text... \end{minipage}} \caption{Tape layout explorer} \label{int:tape-layout} \end{figure} ``` Short HTML-only form: ````markdown ```interactive id="tape-layout" caption="Tape layout explorer" Drag files along a tape and compare the access cost. ``` ```` Use the canonical Pandoc-style form for any material that should export to PDF. ## Cross-References Use ordinary Markdown links to ids: ```markdown The proof of [Hall's theorem](#thm:hall) is constructive. See the [tape explorer](#int:tape-layout) for the interactive version. ``` HTML keeps the link: ```html Hall's theorem ``` PDF converts the link to: ```tex \cref{thm:hall} ``` Recommended label prefixes: ```text thm: theorem lem: lemma prop: proposition cor: corollary def: definition ex: example rem: remark prf: proof int: interactive fig: figure tbl: table eq: equation ``` Do not reuse labels within one document. ## Inline Footnotes The HTML parser supports inline footnotes: ```markdown This is a sentence.^[This is the footnote.] ``` `remarkInlineFootnotes` rewrites these into normal Markdown footnote definitions before rendering. The parser also supports multi-node inline footnotes when formatting spans the inside of the footnote. For maximum PDF portability, ordinary footnotes are safest: ```markdown This is a sentence.[^note] [^note]: This is the footnote. ``` ## Math Use standard dollar math: ```markdown Inline math: $a^2 + b^2 = c^2$. $$ \sum_{i=1}^n i = \frac{n(n+1)}{2} $$ ``` HTML uses `remark-math` plus `rehype-katex`. PDF passes math through Pandoc to LuaLaTeX. Avoid math syntax that only one renderer understands. ## Symbols, Emoji, Chess, And Cards Write Unicode directly in Markdown: ```markdown The position after ♘f3 is pleasant. Hearts and diamonds are red: ♥ ♦. Spades and clubs are black: ♠ ♣. This note is important 🎯. ``` HTML wraps these glyphs in semantic spans: ```html 🎯 ``` PDF maps them to LaTeX macros: ```text emoji and emoji sequences -> \BlogEmojiText{...} ♔ ♕ ♖ ♗ ♘ ♙ -> chessfss white-piece macros ♚ ♛ ♜ ♝ ♞ ♟ -> chessfss black-piece macros ♥ ♡ ♦ ♢ -> red amssymb suit macros ♠ ♤ ♣ ♧ -> black amssymb suit macros playing-card emoji -> \BlogEmojiText{...} ``` Do not write LaTeX-only suit or chess macros in source unless the document is PDF-only. ## Code Fences The code-fence language parser normalizes a few imported Quarto forms: ````markdown ```{ojs} viewof x = Inputs.range([0, 10]) ``` ```` becomes a JavaScript-highlighted fence: ````markdown ```js viewof x = Inputs.range([0, 10]) ``` ```` This pass is intentionally conservative. Add aliases only when imported source material needs them. ## Parser Ordering Parser order matters. Use this order for Astro: ```js const remarkPlugins = [ remarkGfm, remarkInlineFootnotes, remarkCodeFenceLanguages, remarkSemanticBlocks, remarkMath, remarkTypography, remarkCallouts, remarkSymbols, ]; const rehypePlugins = [rehypeKatex]; ``` Reasons: - Inline footnotes should be normalized before other structure changes. - Code-fence languages should be normalized before semantic fences are read. - Semantic blocks should run before code fences become highlighted code. - Math should be parsed before typography and symbol wrapping can interfere. - Typography should run before symbol wrapping. - Callouts should run before symbol wrapping so marker detection sees plain text. - Symbols should run late because it emits raw HTML spans. ## Astro Integration Checklist Copy these files into the target Astro project: ```text src/remark/callouts.mjs src/remark/code-fence-languages.mjs src/remark/inline-footnotes.mjs src/remark/semantic-blocks.mjs src/remark/symbols.mjs src/remark/typography.mjs src/interactives/runtime.tsx src/components/InteractiveRuntime.astro src/styles/article.css ``` Wire the plugins in `astro.config.mjs`: ```js import mdx from "@astrojs/mdx"; import rehypeKatex from "rehype-katex"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import remarkCallouts from "./src/remark/callouts.mjs"; import remarkCodeFenceLanguages from "./src/remark/code-fence-languages.mjs"; import remarkInlineFootnotes from "./src/remark/inline-footnotes.mjs"; import remarkSemanticBlocks from "./src/remark/semantic-blocks.mjs"; import remarkSymbols from "./src/remark/symbols.mjs"; import remarkTypography from "./src/remark/typography.mjs"; const remarkPlugins = [ remarkGfm, remarkInlineFootnotes, remarkCodeFenceLanguages, remarkSemanticBlocks, remarkMath, remarkTypography, remarkCallouts, remarkSymbols, ]; const rehypePlugins = [rehypeKatex]; export default defineConfig({ integrations: [mdx({ remarkPlugins, rehypePlugins })], markdown: { remarkPlugins, rehypePlugins }, }); ``` Add the runtime to post pages: ```astro --- import InteractiveRuntime from "@/components/InteractiveRuntime.astro"; ---
``` Make sure the article body uses the CSS class that `article.css` targets. ## PDF Integration Checklist Copy these files: ```text scripts/export-pdf.mjs scripts/pandoc-filters/semantic-blocks.lua scripts/pandoc-templates/semantic-preamble.tex ``` Add a package script: ```json { "scripts": { "pdf:post": "node scripts/export-pdf.mjs" } } ``` Export a post: ```bash npm run pdf:post -- path/to/post/index.md --out /tmp/post.pdf ``` The export command currently uses: ```text markdown+fenced_code_attributes+tex_math_dollars+tex_math_single_backslash+footnotes+raw_html ``` and includes the shared preamble with: ```text --include-in-header scripts/pandoc-templates/semantic-preamble.tex ``` If LuaLaTeX cannot write its font cache in a sandboxed environment, set a writable TeX cache: ```bash TEXMFVAR=/tmp/texmf-var TEXMFCONFIG=/tmp/texmf-config \ npm run pdf:post -- path/to/post/index.md --out /tmp/post.pdf ``` ## Obsidian Behavior The syntax is chosen to remain readable in Obsidian: - Plain Markdown stays plain Markdown. - `> [!tip]` and `> [!tip: Title]` render as Obsidian callouts. - Fenced semantic blocks remain visible as code blocks when Obsidian does not know how to render them. - Interactive blocks show their fallback description in the source. Obsidian will not execute Astro interactives or render LaTeX environments as final theorem boxes. That is acceptable: Obsidian is the editing surface, not the final renderer. ## Adding A New Semantic Feature When adding a feature, update all three layers: 1. Source syntax in this document. 2. Astro parser and CSS. 3. Pandoc Lua filter and LaTeX preamble. Use this checklist: - Define the Markdown syntax and at least one canonical example. - Decide whether it needs a label and cross-reference behavior. - Decide the HTML output shape and CSS classes. - Decide the PDF output shape and required LaTeX packages. - Add an Obsidian-readable fallback. - Add a smoke-test Markdown file and render both HTML and PDF. - Document migration rules for old content, if any. ## Known Tradeoffs The current system is robust, but it has deliberate limits: - It is a convention layer over Markdown, not a full new markup language. - HTML and PDF are separate renderers. The contract is shared, but exact visual parity is not the goal. - Interactives are HTML-first. PDF receives a figure-like fallback. - Raw HTML is allowed for web-only material but should not be used for semantic features that need PDF output. - The blog repo currently duplicates parser files across seven sites. A future shared parser package would reduce drift. ## Minimal Smoke Test Use this sample when porting the parser to another project: ````markdown --- title: "Parser Smoke Test" description: "Checks callouts, environments, links, symbols, math, and interactives." pubDate: "2026-01-31" --- This sentence has an em dash --- and inline math $a^2+b^2=c^2$. > [!tip: Explicit title] > > This callout has a title, a chess piece ♘, a card suit ♥, and an emoji 🎯. ```{.env .definition #def:test title="Test object" html="callout"} A test object is something we can refer to later. ``` See [the definition](#def:test). ```{.interactive #int:test-widget caption="Test widget" pdf="The HTML version contains a small interactive test widget."} This fallback appears when the interactive component is unavailable. ``` ```` Expected HTML: - One em dash. - KaTeX-rendered math. - A titled tip callout. - A semantic definition block with `id="def:test"`. - A normal anchor link to `#def:test`. - A framed interactive fallback if no component exists. - Wrapped symbol spans for `♘`, `♥`, and `🎯`. Expected PDF: - A LuaLaTeX em dash. - Typeset math. - A titled `tcolorbox` tip. - A real `definition` environment with `\label{def:test}`. - A `\cref{def:test}` reference. - A figure fallback for `int:test-widget`.