orz-markdown 1.2.2 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: orz-markdown
3
- description: "orz-markdown usage skill. Use this skill whenever you need to render markdown with this parser, write markdown that uses {{...}} custom plugin syntax (mermaid, qrcode, youtube, smiles, toc, span, emoji, attrs, space, yaml, nyml, or their aliases mm/qr/yt/sm/sp/em), use :::container syntax (success/info/warning/danger/spoil/tabs/tab/cols/col/left/right/center), set up a complete HTML page to display parser output, choose or import one of the 10 bundled CSS themes, or create a custom theme stylesheet. Also invoke when asked about .markdown-body class, prepareSources, browser runtime scripts for QR codes or tabs, or any KaTeX math syntax in this project. ALWAYS invoke before editing documents whose headings carry stable block IDs ({{attrs[#blk-...]}})."
3
+ description: "orz-markdown usage skill. Use this skill whenever you need to render markdown with this parser, write markdown that uses {{...}} custom plugin syntax (mermaid, chart, qrcode, youtube, smiles, toc, span, emoji, attrs, space, yaml, nyml, or their aliases mm/qr/yt/sm/sp/em), use :::container syntax (success/info/warning/danger/spoil/tabs/tab/cols/col/left/right/center), set up a complete HTML page to display parser output, choose or import one of the 12 bundled CSS themes, or create a custom theme stylesheet. Also invoke when asked about .markdown-body class, prepareSources, browser runtime scripts for QR codes or tabs, or any KaTeX math syntax in this project. ALWAYS invoke before editing documents whose headings carry stable block IDs ({{attrs[#blk-...]}})."
4
4
  compatibility:
5
5
  runtime: "Node.js 20+, ESM"
6
6
  package: "orz-markdown"
@@ -8,7 +8,7 @@ compatibility:
8
8
 
9
9
  # orz-markdown
10
10
 
11
- A deeply customized `markdown-it` instance with 10+ plugins, 9 official plugin bundles, and 10 ready-to-use CSS themes. All rendered HTML lives inside `<article class="markdown-body">`.
11
+ A deeply customized `markdown-it` instance with 10+ custom plugins, 10 official plugin bundles, and 12 ready-to-use CSS themes. All rendered HTML lives inside `<article class="markdown-body">`.
12
12
 
13
13
  ## Rendering (Node.js / ESM)
14
14
 
@@ -38,14 +38,16 @@ const html = md.render(resolved, { markdownBasePath: '/local/base/path' });
38
38
 
39
39
  Every page that displays parser output needs **all five** of these:
40
40
 
41
- 1. **Theme stylesheet** — one of the 10 bundled themes, or `assets/minimal.css`
41
+ 1. **Theme stylesheet** — one of the 12 bundled themes, or `assets/minimal.css`
42
42
  2. **KaTeX CSS** — `https://cdn.jsdelivr.net/npm/katex@0.16.35/dist/katex.min.css`
43
43
  3. **Highlight.js CSS** — match light/dark to the chosen theme
44
- 4. **Three CDN scripts** — Highlight.js, Mermaid.js, SmilesDrawer (loaded in body)
44
+ 4. **Four CDN scripts** — Highlight.js, Mermaid.js, SmilesDrawer, Chart.js (loaded in body)
45
45
  5. **Two inline scripts** — tabs initializer and QR code runtime (also in body)
46
46
 
47
47
  **Use `assets/template.html`** — all CDN links, scripts, and the `.markdown-body` wrapper are pre-wired. Copy it and replace the `<!-- INSERT RENDERED HTML HERE -->` comment.
48
48
 
49
+ **Mounting output in an `<iframe>` (viewer / editor / slide or page engine)?** Call `getPreviewFrameAssets()` from `orz-markdown/preview-frame` — it returns the pinned CDN URLs, the runtime, `headLinks(scheme)` / `bodyScripts()` strings, and a `window.__orzEnhance()` that does all of items 3–5 (highlight code, draw mermaid/SMILES/charts, init tabs + QR). One call instead of re-deriving the wiring. See `references/embedding.md`.
50
+
49
51
  CDN URLs (pinned versions):
50
52
  | Library | URL |
51
53
  |---|---|
@@ -54,6 +56,7 @@ CDN URLs (pinned versions):
54
56
  | Highlight.js dark CSS | `.../styles/atom-one-dark.min.css` |
55
57
  | Mermaid.js | `https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js` |
56
58
  | SmilesDrawer | `https://unpkg.com/smiles-drawer@1.0.10/dist/smiles-drawer.min.js` |
59
+ | Chart.js | `https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js` |
57
60
 
58
61
  ### Browser runtime API
59
62
 
@@ -69,20 +72,22 @@ document.body.appendChild(script);
69
72
 
70
73
  The runtime also provides **copy-as-Markdown**: with it loaded, copying a selection inside `.markdown-body` puts Markdown source on the clipboard, not HTML (tables, lists, math, code, etc. are reconstructed). It skips selections inside `<input>`/`<textarea>`/`contenteditable`. Convert a node directly with `window.OrzMarkdownRuntime.elementToMarkdown(node)`.
71
74
 
72
- > **Do not strip `data-md` attributes.** `mermaid`, `smiles`, `qrcode`, and `youtube` output carry a `data-md` breadcrumb so copy recovers their source after client-side rendering (e.g. a copied QR yields `{{qr ...}}`, not its SVG). Preserve these attributes if you post-process the HTML.
75
+ > **Do not strip `data-md` attributes.** `mermaid`, `smiles`, `qrcode`, `youtube`, and `chart` output carry a `data-md` breadcrumb so copy recovers their source after client-side rendering (e.g. a copied QR yields `{{qr ...}}`, not its SVG). Preserve these attributes if you post-process the HTML.
73
76
 
74
77
  ---
75
78
 
76
79
  ## Themes
77
80
 
78
- Ten bundled themes — each auto-imports `common.css` (structural rules for tables, images, QR overlays, print).
81
+ Twelve bundled themes — each auto-imports `common.css` (structural rules for tables, images, QR overlays, print).
79
82
 
80
83
  | File | Style | Scheme |
81
84
  |---|---|---|
82
85
  | `dark-elegant-1.css` | Cinzel headings · scholarly serif | Dark |
83
86
  | `dark-elegant-2.css` | Dark elegant variant | Dark |
87
+ | `dark-elegant-3.css` | Lora · VS Code-dark, colourful headings "Nocturne" | Dark |
84
88
  | `light-neat-1.css` | Figtree · clean modern sans | Light |
85
89
  | `light-neat-2.css` | Light neat variant | Light |
90
+ | `light-neat-3.css` | Bricolage · calm green "Orchard" | Light |
86
91
  | `beige-decent-1.css` | Warm beige · print-like prose | Light |
87
92
  | `beige-decent-2.css` | Beige decent variant | Light |
88
93
  | `light-academic-1.css` | Alegreya · justified scholarly prose | Light |
@@ -98,7 +103,7 @@ import 'orz-markdown/themes/light-neat-1.css';
98
103
  // <link rel="stylesheet" href="node_modules/orz-markdown/themes/light-neat-1.css">
99
104
  ```
100
105
 
101
- For custom themes start from `assets/minimal.css` (structural only, no decoration) and add your visual layer. See `references/themes.md` for the design token pattern, element checklist, and design guidelines. See `references/css-classes.md` for the full list of every CSS class the parser emits.
106
+ For custom themes start from `assets/minimal.css` (structural only, no decoration) and add your visual layer. See `references/themes.md` for the design token pattern, element checklist, and design guidelines. See `references/css-classes.md` for the full list of every CSS class the parser emits. **Building a host app that supplies its own CSS and page shell (a slide engine, viewer, or editor)?** Read `references/embedding.md` — it gathers the CSS contract, the JS runtime + diagram-drawing duties, and the copy-as-Markdown requirements into one checklist (and the non-obvious gotchas, like a host CSS reset stripping bold/sub/sup).
102
107
 
103
108
  ---
104
109
 
@@ -115,11 +120,12 @@ Escape with backslash: `\{{name}}` renders as literal `{{name}}`.
115
120
  |---|---|---|
116
121
  | **span** | `sp` | `{{sp[red] colored}}` · `{{sp[success] ✓ Done}}` |
117
122
  | **emoji** | `em` | `{{emoji wave}}` · `{{em tada}}` |
118
- | **space** | — | `{{space 4}}` → 4 × `&nbsp;` |
123
+ | **space** | — | `{{space 4}}` → 4rem-wide inline-block spacer |
119
124
  | **qrcode** | `qr` | `{{qr https://example.com}}` (click-to-expand SVG) |
120
125
  | **youtube** | `yt` | `{{youtube dQw4w9WgXcQ}}` (responsive iframe) |
121
126
  | **mermaid** | `mm` | `{{mm\ngraph LR\nA-->B\n}}` |
122
127
  | **smiles** | `sm` | `{{smiles C1=CC=CC=C1}}` (chemical structure) |
128
+ | **chart** | — | `{{chart\ntype: bar\nlabels: A, B\ndata: 1, 2\n}}` (Chart.js bar/line/pie/doughnut) |
123
129
  | **toc** | — | `{{toc}}` or `{{toc 2,3}}` (heading levels) |
124
130
  | **attrs** | — | `# Title{{attrs[id="hero"]}}` |
125
131
  | **markdown** | `md`, `md-include` | `{{md ./path/to/file.md}}` |
@@ -173,7 +179,8 @@ Documents may carry stable block IDs on headings: `## Title{{attrs[#blk-abc12345
173
179
  |---|---|
174
180
  | `references/syntax.md` | Full syntax with examples for every plugin, container, and extended markdown feature |
175
181
  | `references/block-ids.md` | Stable block-ID preservation rules — REQUIRED reading before editing documents that use `{{attrs[#blk-...]}}` |
176
- | `references/themes.md` | Custom theme guide: design tokens, element checklist, 10 design guidelines |
182
+ | `references/themes.md` | Custom theme guide: design tokens, element checklist, 12 design guidelines |
177
183
  | `references/css-classes.md` | Every CSS class and HTML element the parser emits — the spec for theme authors |
184
+ | `references/embedding.md` | Host-integration guide for apps that bring their own CSS/shell (slide engines, viewers, editors): the CSS contract + reset gotchas, the JS runtime + diagram drawing, and what copy-as-Markdown needs |
178
185
  | `assets/template.html` | Ready-to-use HTML page template with all scripts and CDN links wired up |
179
186
  | `assets/minimal.css` | Structural-only stylesheet — starting point for custom themes |
@@ -19,8 +19,8 @@
19
19
 
20
20
  <!-- Parser stylesheet — choose one of the bundled themes or link your own.
21
21
  Bundled themes (each imports common.css automatically):
22
- dark-elegant-1.css dark-elegant-2.css
23
- light-neat-1.css light-neat-2.css
22
+ dark-elegant-1.css dark-elegant-2.css dark-elegant-3.css
23
+ light-neat-1.css light-neat-2.css light-neat-3.css
24
24
  beige-decent-1.css beige-decent-2.css
25
25
  light-academic-1.css light-academic-2.css
26
26
  light-playful-1.css light-playful-2.css
@@ -64,6 +64,26 @@ if (typeof SmilesDrawer !== 'undefined') {
64
64
  }
65
65
  </script>
66
66
 
67
+ <!-- Chart.js — renders {{chart}} canvases emitted by the parser. -->
68
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
69
+ <script>
70
+ if (typeof Chart !== 'undefined') {
71
+ document.querySelectorAll('canvas.orz-chart[data-chart]').forEach(function (canvas) {
72
+ var wrap = document.createElement('div');
73
+ wrap.style.cssText = 'position:relative;width:100%;max-width:520px;margin:.6em auto';
74
+ canvas.parentNode.insertBefore(wrap, canvas);
75
+ wrap.appendChild(canvas);
76
+ canvas.removeAttribute('width');
77
+ canvas.removeAttribute('height');
78
+ try {
79
+ var config = JSON.parse(canvas.getAttribute('data-chart') || '{}');
80
+ config.options = Object.assign({ responsive: true, maintainAspectRatio: true, animation: false }, config.options || {});
81
+ new Chart(canvas, config);
82
+ } catch (e) {}
83
+ });
84
+ }
85
+ </script>
86
+
67
87
  <!-- Tabs component — builds the .tabs-bar and activates .tab panels.
68
88
  Must run after the DOM is painted so .tabs elements are found. -->
69
89
  <script>
@@ -184,7 +204,7 @@ if (typeof SmilesDrawer !== 'undefined') {
184
204
  // ---- copy-as-markdown: DOM -> Markdown walker ---------------------------
185
205
  // Converts a selected DOM fragment back to Markdown so that copying rendered
186
206
  // content yields source, not HTML. Standard constructs are reconstructed from
187
- // their tags; generated constructs (math, mermaid, smiles, qr, youtube) carry
207
+ // their tags; generated constructs (math, mermaid, smiles, qr, youtube, chart) carry
188
208
  // a [data-md] breadcrumb that is emitted verbatim. Written without template
189
209
  // literals/backticks because this whole file is a String.raw template.
190
210
  var BT = String.fromCharCode(96);
@@ -193,23 +193,30 @@ span.qrcode { background: #fff; padding: 4px; /* or 6px */ vertical-align: middl
193
193
 
194
194
  Note: `span.success/info/warning/danger` also have `display: inline-flex; align-items: center; line-height: 1; white-space: nowrap` set by `common.css`. Add `padding`, `border-radius`, `font-size`, and colors in the theme.
195
195
 
196
+ ### Chart
197
+
198
+ | Class | Element | Description |
199
+ |---|---|---|
200
+ | `canvas.orz-chart` | `<canvas>` | Carries a Chart.js config as HTML-escaped JSON in `data-chart` (and a `data-md` breadcrumb). The host runtime reads `data-chart` and calls Chart.js. Cap its width. |
201
+
202
+ ```css
203
+ canvas.orz-chart { max-width: 100%; height: auto; }
204
+ ```
205
+
196
206
  ### Table of Contents
197
207
 
198
208
  | Class | Element | Description |
199
209
  |---|---|---|
200
- | `.toc-wrapper` | `<div>` | Outer wrapper (for positioning). |
201
- | `nav.toc` / `.toc-list` | `<nav>` | The `<ul>` tree of heading links. Reset list bullets in TOC context. |
210
+ | `.toc-list` | `<ul>` | The flat `<ul>` of heading links emitted by `{{toc}}`. Each `<li>` carries an inline `padding-left` for its depth. Reset list bullets. This is the **only** class the parser emits for the TOC. |
202
211
 
203
212
  ```css
204
- nav.toc, .toc-list {
205
- padding: ...;
206
- border: 1px solid var(--border);
207
- }
208
- nav.toc ul, .toc-list ul { margin: 0; padding-left: 1.1rem; }
209
- nav.toc li, .toc-list li { list-style: none; margin-bottom: 0.25rem; }
210
- nav.toc a, .toc-list a { text-decoration: none; color: var(--link); }
213
+ .toc-list { padding: ...; border: 1px solid var(--border); list-style: none; }
214
+ .toc-list li { list-style: none; margin-bottom: 0.25rem; }
215
+ .toc-list a { text-decoration: none; color: var(--link); }
211
216
  ```
212
217
 
218
+ > Note: the parser does **not** emit a `<nav class="toc">` wrapper or a `.toc-wrapper`. `{{toc}}` produces a single `<ul class="toc-list">`. If your host wants a wrapper, add it around the rendered TOC yourself.
219
+
213
220
  ### Test elements (development only)
214
221
 
215
222
  | Class | Element | Description |
@@ -0,0 +1,196 @@
1
+ # Embedding orz-markdown in a host app (custom stylesheet)
2
+
3
+ Read this when you call `md.render()` and supply **your own CSS and page shell**
4
+ instead of using a bundled theme — e.g. a slide engine, a doc viewer, an editor,
5
+ or any app that embeds rendered output. The bundled themes already do everything
6
+ below; this guide is the checklist for when you *don't* use them.
7
+
8
+ > The sibling `orz-slides` project hit a long tail of bugs (unstyled containers,
9
+ > bold that changed colour instead of weight, broken sub/sup, diagrams
10
+ > overflowing, copy losing `{{sp}}`/`:::` source) precisely because these points
11
+ > weren't gathered in one place. Follow this and you skip all of it.
12
+
13
+ There are **three** things to get right: CSS, JavaScript, and copy-as-Markdown.
14
+
15
+ > **Shortcut for iframe hosts — `getPreviewFrameAssets()`.** Most of the
16
+ > JavaScript below is the same everywhere, so it lives in one helper:
17
+ >
18
+ > ```js
19
+ > import { getPreviewFrameAssets } from 'orz-markdown/preview-frame';
20
+ > const pa = getPreviewFrameAssets();
21
+ > ```
22
+ >
23
+ > It returns the pinned CDN URLs (`pa.cdn`), the runtime (`pa.runtimeScript`), and
24
+ > ready-made strings: `pa.headLinks(scheme)` (KaTeX + highlight.js CSS, the hljs
25
+ > link carries `id="orz-hljs"`) for the iframe `<head>`, and `pa.bodyScripts()`
26
+ > (loads the libs + runtime and defines `window.__orzEnhance()`) for the `<body>`.
27
+ > After each render call `window.__orzEnhance()` — it highlights code, runs
28
+ > mermaid, draws SMILES (honoring `window.__orzSmilesTheme`) and charts, and
29
+ > re-inits the runtime (tabs + QR). On theme change swap
30
+ > `doc.getElementById('orz-hljs').href = pa.hljsCss(scheme)`. `__orzEnhance`
31
+ > scopes to `.markdown-body`, so it works whatever id/wrapper you use. The rest of
32
+ > this page is what that helper does under the hood — read it if you hand-roll the
33
+ > wiring or need to understand a failure.
34
+
35
+ ---
36
+
37
+ ## 1. CSS
38
+
39
+ The parser emits plain HTML plus plugin/container classes. Two reference docs
40
+ have the full detail:
41
+
42
+ - `references/css-classes.md` — every class and element the parser emits.
43
+ - `references/themes.md` — the element checklist and design guidance.
44
+
45
+ When you bring your own stylesheet, the four things that bite hardest:
46
+
47
+ ### 1a. Wrap output in `.markdown-body` and scope your rules to it
48
+
49
+ ```html
50
+ <article class="markdown-body"> …md.render() output… </article>
51
+ ```
52
+ Every theme rule is scoped under `.markdown-body`; copy-as-Markdown also keys off
53
+ this class (§3). For a region-based layout, each rendered region gets its own
54
+ `.markdown-body`.
55
+
56
+ ### 1b. A host CSS *reset* will strip inline semantics — restore them
57
+
58
+ This is the #1 non-obvious failure. Many app/framework resets (notably
59
+ **reveal.js**'s `reset.css`) apply to `strong, b, em, i, sub, sup, small, …`:
60
+
61
+ ```css
62
+ em, strong, sub, sup, b, i, small, … { font: inherit; font-size: 100%; vertical-align: baseline; }
63
+ ```
64
+
65
+ `font: inherit` wipes `font-weight`/`font-style`, and `vertical-align: baseline`
66
+ + `font-size: 100%` flattens sub/sup. The result: **bold isn't bold, italic
67
+ isn't italic, H~2~O / x^2^ render flat.** (A theme that "fixed" bold with
68
+ `strong { color: accent }` then makes bold *vanish* wherever the accent equals
69
+ the background.) If your host has any such reset, restore the semantics
70
+ explicitly — **bold is weight, not colour**:
71
+
72
+ ```css
73
+ .markdown-body strong, .markdown-body b { font-weight: 700; }
74
+ .markdown-body em, .markdown-body i { font-style: italic; }
75
+ .markdown-body s, .markdown-body del { text-decoration: line-through; }
76
+ .markdown-body ins,.markdown-body u { text-decoration: underline; }
77
+ .markdown-body sub, .markdown-body sup { font-size: 0.75em; line-height: 0; position: relative; vertical-align: baseline; }
78
+ .markdown-body sub { bottom: -0.3em; }
79
+ .markdown-body sup { top: -0.5em; }
80
+ ```
81
+
82
+ ### 1c. Style **every** plugin/container class, or it renders unstyled
83
+
84
+ If you don't ship the bundled `common.css`/theme, these have **no** styling and
85
+ look broken. Minimum set (see `css-classes.md` for the exact selectors):
86
+
87
+ | Construct | Selectors to style |
88
+ |---|---|
89
+ | Admonitions | `div.info`, `div.success`, `div.warning`, `div.danger` |
90
+ | Spans — colour | `span.red`, `span.green`, `span.blue`, `span.yellow` (colour only) |
91
+ | Spans — badge | `span.success/info/warning/danger` — **need** `display:inline-flex; align-items:center; line-height:1; white-space:nowrap` + padding/radius (normally in `common.css`) |
92
+ | Columns | `.cols` (CSS grid), `.col` |
93
+ | Tabs | `.tabs`, `.tabs-bar`, `.tabs-bar-btn`(`.active`), `.tab`; **`.tabs:not([data-js]) .tab { display:block }`** (no-JS fallback) and `.tabs[data-js] .tab{display:none}` / `.tab.active{display:block}` |
94
+ | Spoiler | `details.spoil`, `summary` |
95
+ | Alignment | `div.left`, `div.center`, `div.right` (`text-align`) |
96
+ | QR | `span.qrcode` — **must** have `background:#fff; padding` (the SVG is black-on-transparent) |
97
+ | YouTube | `.youtube-embed` — the iframe has **no width/height**; make it a 16:9 responsive box or it defaults to 300×150 |
98
+ | Math | `.katex`, `.katex-display` (load `katex.min.css`) |
99
+ | Diagrams/chem | `.mermaid svg`, `.smiles-render canvas` / `canvas[data-smiles]` |
100
+
101
+ ### 1d. Constrain generated graphics so they fit
102
+
103
+ Mermaid SVGs and smiles/chart canvases have intrinsic sizes and will overflow a
104
+ bounded container. At minimum cap the width; in a fixed-size layout (slides),
105
+ size them to the container in JS (measure the container, set explicit
106
+ width/height by the SVG's `viewBox` aspect; for Chart.js use `responsive:false`
107
+ + explicit `resize(w,h)` because it can't read a definite height through a CSS
108
+ grid). Mermaid also sets an inline `max-width` you may need to override.
109
+
110
+ ```css
111
+ .markdown-body .mermaid svg,
112
+ .markdown-body canvas[data-smiles],
113
+ .markdown-body canvas.orz-chart { max-width: 100%; height: auto; }
114
+ ```
115
+
116
+ ---
117
+
118
+ ## 2. JavaScript
119
+
120
+ Rendering is pure HTML, but several constructs need client-side JS **after mount**.
121
+
122
+ ### 2a. Load the runtime — tabs, QR expand, and copy
123
+
124
+ ```js
125
+ import { getBrowserRuntimeScript } from 'orz-markdown/runtime';
126
+ const s = document.createElement('script');
127
+ s.textContent = getBrowserRuntimeScript();
128
+ document.body.appendChild(s);
129
+ ```
130
+
131
+ It auto-runs `OrzMarkdownRuntime.init(document)` **once** on `DOMContentLoaded`
132
+ and wires the global copy handler. It exposes
133
+ `OrzMarkdownRuntime.{ init, initTabs, initQrCodes, elementToMarkdown }`.
134
+
135
+ **If you render content dynamically** (after load, or re-render — e.g. an editor
136
+ or a slide engine), the one-time `init` won't see it. Call
137
+ `OrzMarkdownRuntime.init(newRoot)` (or `initTabs`/`initQrCodes`) on the new
138
+ content yourself. `initTabs` guards on `data-js="1"` — if you also have your own
139
+ tab init, use the **same** `data-js="1"` marker so the two don't both build a bar
140
+ (double tab bar bug).
141
+
142
+ ### 2b. The runtime does **not** draw diagrams/charts — you do
143
+
144
+ mermaid, smiles, and chart canvases are placeholders. Load the libraries and draw
145
+ them (the bundled themes/`tests/example.html` show the calls):
146
+
147
+ | Construct | You must load | Then |
148
+ |---|---|---|
149
+ | `.mermaid` | mermaid.js | `mermaid.run({ querySelector: '.mermaid' })` |
150
+ | `canvas[data-smiles]` | smiles-drawer | `SmilesDrawer.parse(...)` → `drawer.draw(tree, canvas, scheme, false)` — pass `'dark'` on dark backgrounds so bonds are light |
151
+ | `canvas.orz-chart` | Chart.js | `new Chart(canvas, JSON.parse(canvas.dataset.chart))` |
152
+ | `$…$` / `$$…$$` | KaTeX **CSS** only | math is pre-rendered by `md.render()`; you just need `katex.min.css` |
153
+
154
+ Draw on first display and re-draw when the container resizes; canvases drawn
155
+ while their container is hidden (`display:none`) size to 0.
156
+
157
+ ---
158
+
159
+ ## 3. Copy-as-Markdown
160
+
161
+ Selecting rendered content and copying yields **Markdown source** when (and only
162
+ when) all of this holds:
163
+
164
+ 1. **The runtime is loaded** (§2a) — it installs the `copy` handler and the
165
+ DOM→Markdown walker (`elementToMarkdown`).
166
+ 2. **Content is inside `.markdown-body`** (or an element with `data-orz-copy`).
167
+ The handler ignores selections elsewhere and inside inputs/textareas.
168
+ 3. **`data-md` breadcrumbs are preserved.** Generated constructs whose source is
169
+ otherwise lost — `mermaid`, `smiles`, `qrcode`, `youtube`, `chart` (and any
170
+ plugin that adds one) — carry `data-md` with their original directive. The
171
+ walker emits it verbatim. **Never strip `data-md`** when you post-process HTML.
172
+ 4. **Plugin/container classes are preserved.** Constructs without a `data-md`
173
+ are recovered *by class/structure*: `<span class="red">` → `{{sp[red] …}}`,
174
+ `<div class="center">` → `::: center … :::`, nested `cols`/`tabs` get the
175
+ right fence length, and a `{{toc}}` is recovered as its rendered list of
176
+ heading links (not the `{{toc}}` directive). If you rewrite/strip these
177
+ classes, copy loses the source.
178
+
179
+ Caveat: a container (`::: center`, `:::: cols`, spoiler, tabs) is recovered only
180
+ when the **selection includes the container element**, not just the text inside
181
+ it — select the whole block/region, not the inner words.
182
+
183
+ Convert a node directly: `OrzMarkdownRuntime.elementToMarkdown(node)`.
184
+
185
+ ---
186
+
187
+ ## Pre-flight checklist
188
+
189
+ - [ ] Output wrapped in `.markdown-body`; rules scoped under it.
190
+ - [ ] Inline semantics restored if a host reset touches `strong/em/sub/sup/…` (§1b).
191
+ - [ ] Every admonition / span / cols / tabs / spoil / align / qr / youtube / math / diagram class is styled (§1c, `css-classes.md`).
192
+ - [ ] Span badges have the inline-flex structural CSS; QR has a white plate; tabs have the no-JS fallback; YouTube is a 16:9 box.
193
+ - [ ] Graphics are width-capped (and size-fitted in fixed layouts) (§1d).
194
+ - [ ] Runtime loaded; `init` re-run on dynamically rendered content; tab `data-js="1"` aligned (§2a).
195
+ - [ ] mermaid / smiles-drawer / Chart.js loaded and drawn; KaTeX CSS loaded (§2b).
196
+ - [ ] `data-md` and plugin/container classes preserved end-to-end for copy (§3).
@@ -310,10 +310,12 @@ Uses `node-emoji` for name lookup. The emoji text alias (e.g., `wave`, `smile`)
310
310
  ### space — Inline horizontal whitespace
311
311
 
312
312
  ```markdown
313
- {{space 4}} → &nbsp;&nbsp;&nbsp;&nbsp; (4 non-breaking spaces)
314
- {{space 1}} → &nbsp;
313
+ {{space 4}} → 4rem-wide inline-block spacer
314
+ {{space 1}} → 1rem-wide inline-block spacer
315
315
  ```
316
316
 
317
+ The numeric argument is a `rem` width: `{{space 4}}` emits `<span style="display:inline-block;width:4rem"></span>`. Non-positive or non-numeric values render nothing.
318
+
317
319
  ---
318
320
 
319
321
  ### qrcode / qr — Inline QR code
@@ -381,7 +383,7 @@ Renders `<div class="smiles-render"><canvas data-smiles="...">`. SmilesDrawer.js
381
383
  {{toc 1,4}}
382
384
  ```
383
385
 
384
- Auto-generates a `<nav class="toc">` from all headings in the document. Optional argument is a comma-separated level range (`min,max`), defaulting to all levels. Must appear after the headings it references in the source (or at the start the parser makes a pass over the full document).
386
+ Auto-generates a `<ul class="toc-list">` of heading links (each `<li>` indented by depth). Optional argument is a comma-separated level range (`min,max`), defaulting to `1,3`. The placement in the source does not matter a core rule scans the whole document, so `{{toc}}` works at the top or bottom.
385
387
 
386
388
  ---
387
389
 
@@ -99,8 +99,8 @@ Follow this section order in every theme file for consistency:
99
99
  12. Semantic containers: `.success`, `.info`, `.warning`, `.danger`
100
100
  13. Layout utilities: `.left`, `.right`, `.center`
101
101
  14. Spoiler, tabs, and columns: `details.spoil`, `.tabs`, `.cols`
102
- 15. TOC and footnotes: `nav.toc`, `.footnotes`
103
- 16. Plugin output: `.youtube-embed`, `.mermaid`, `.smiles-render`, `.qrcode`, span color/badge classes
102
+ 15. TOC and footnotes: `.toc-list`, `.footnotes`
103
+ 16. Plugin output: `.youtube-embed`, `.mermaid`, `.smiles-render`, `canvas.orz-chart`, `.qrcode`, span color/badge classes
104
104
  17. Decorative pseudo-elements and theme-only flourishes
105
105
  18. Responsive rules (`@media`)
106
106
  19. Print rules (`@media print`)
@@ -165,12 +165,13 @@ Every theme must have CSS rules for all of the following. See `references/css-cl
165
165
  - [ ] `.tabs` `.tabs-bar` `.tabs-bar-btn` `.tabs-bar-btn.active` `.tab` `.tab.active`
166
166
  - [ ] `.tabs:not([data-js]) .tab` — no-JS fallback: show all panels
167
167
  - [ ] `.cols` `.col`
168
- - [ ] `nav.toc` `.toc-list` — list reset, link styles
168
+ - [ ] `.toc-list` — list reset, link styles (the parser emits only `<ul class="toc-list">`, no `nav.toc` wrapper)
169
169
  - [ ] `span.red` `span.yellow` `span.green` `span.blue`
170
170
  - [ ] `span.success` `span.info` `span.warning` `span.danger` — inline badge variant
171
171
  - [ ] `div.youtube-embed` `div.youtube-embed iframe` — 16:9 responsive wrapper (structural)
172
172
  - [ ] `div.mermaid` `div.mermaid svg`
173
173
  - [ ] `div.smiles-render` `canvas[data-smiles]`
174
+ - [ ] `canvas.orz-chart` — width-capped (Chart.js draws into it client-side)
174
175
  - [ ] `span.qrcode` — **must have `background: #fff`** (SVG is always black-on-white)
175
176
 
176
177
  ---
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "orz-markdown",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
4
4
  "description": "Customized markdown-it parser with official and custom plugins",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "exports": {
8
8
  ".": "./dist/index.js",
9
9
  "./runtime": "./dist/runtime.js",
10
+ "./preview-frame": "./dist/preview-frame.js",
10
11
  "./themes/*": "./themes/*"
11
12
  },
12
13
  "files": [
@@ -64,7 +65,9 @@
64
65
  "@types/markdown-it-footnote": "^3.0.4",
65
66
  "@types/node": "^25.3.5",
66
67
  "@types/qrcode-svg": "^1.1.5",
68
+ "esbuild": "^0.21.5",
67
69
  "happy-dom": "^15.11.0",
70
+ "path-browserify": "^1.0.1",
68
71
  "tsx": "^4.21.0",
69
72
  "typescript": "^5.9.3",
70
73
  "vitest": "^4.0.18"
@@ -0,0 +1,43 @@
1
+ # orz-markdown themes
2
+
3
+ Twelve ready-to-use CSS themes for the parser's output. All rendered HTML lives
4
+ inside `<article class="markdown-body">`, so every theme is scoped to that class.
5
+ Each theme `@import`s the shared `common.css` (structural rules for tables,
6
+ images, KaTeX blocks, QR overlays, tabs/columns, and print), so you only ever
7
+ import a **single** theme file.
8
+
9
+ ```js
10
+ // With a bundler:
11
+ import 'orz-markdown/themes/light-academic-1.css';
12
+
13
+ // Or over a CDN (jsDelivr):
14
+ // https://cdn.jsdelivr.net/npm/orz-markdown/themes/light-academic-1.css
15
+ ```
16
+
17
+ ## Bundled themes
18
+
19
+ | File | Style | Scheme |
20
+ |---|---|---|
21
+ | `light-neat-1.css` | Figtree · clean modern sans | Light |
22
+ | `light-neat-2.css` | Light neat variant | Light |
23
+ | `light-neat-3.css` | Bricolage · calm green — "Orchard" | Light |
24
+ | `light-academic-1.css` | Alegreya · justified scholarly prose | Light |
25
+ | `light-academic-2.css` | Light academic variant | Light |
26
+ | `beige-decent-1.css` | Warm beige · print-like prose | Light |
27
+ | `beige-decent-2.css` | Beige decent variant | Light |
28
+ | `light-playful-1.css` | Casual · personal blog | Light |
29
+ | `light-playful-2.css` | Light playful variant | Light |
30
+ | `dark-elegant-1.css` | Cinzel headings · scholarly serif | Dark |
31
+ | `dark-elegant-2.css` | Dark elegant variant | Dark |
32
+ | `dark-elegant-3.css` | Lora · VS Code-dark, colourful headings — "Nocturne" | Dark |
33
+
34
+ `common.css` is not a theme — it is imported automatically by each theme above.
35
+
36
+ ## Theming your own output
37
+
38
+ If you supply your **own** stylesheet instead of a bundled theme, the
39
+ [embedding guide](../orz-markdown-skills/references/embedding.md) lists every CSS
40
+ class the parser emits (semantic containers, tabs/columns, spoilers, KaTeX
41
+ blocks, Mermaid/SMILES/chart canvases, clickable QR codes) and the JavaScript the
42
+ browser runtime needs. Start from `common.css` for the structural rules and layer
43
+ your visual design on top.
@@ -27,7 +27,7 @@
27
27
  --line-height: 1.7;
28
28
 
29
29
  --markdown-body-max-width: 800px;
30
- --markdown-body-padding: 4rem 2rem;
30
+ --markdown-body-padding: 2rem 2rem 4rem;
31
31
  --markdown-body-border: none;
32
32
  --markdown-body-radius: 0;
33
33
  --markdown-body-shadow: none;
@@ -56,6 +56,8 @@ body {
56
56
  }
57
57
 
58
58
  /* 2. Headings and body copy */
59
+ .markdown-body > :first-child { margin-top: 0; }
60
+
59
61
  .markdown-body h1,
60
62
  .markdown-body h2,
61
63
  .markdown-body h3,
@@ -556,4 +558,27 @@ body {
556
558
  box-shadow: none;
557
559
  }
558
560
 
559
- }
561
+ }
562
+ /* code block — warm paper panel */
563
+ .markdown-body pre { background: #f5f0e6; border: 1px solid #e7ddc9; border-radius: 8px; }
564
+ .markdown-body pre code, .markdown-body pre code.hljs { background: transparent; padding: 0; }
565
+
566
+
567
+ /* ============ modern decorative refresh ============ */
568
+ /* blockquote — warm card with a soft corner quote glyph (replaces the top/bottom rules) */
569
+ .markdown-body blockquote { position: relative; border: none; background: #f3ece0; border-radius: 12px;
570
+ padding: 1.1rem 1.4rem 1.1rem 1.6rem; margin: 1.9rem 0; font-style: italic; color: #5a4d42;
571
+ box-shadow: 0 2px 10px rgba(120, 90, 50, 0.06); }
572
+ .markdown-body blockquote::before { content: '“'; position: absolute; left: 0.55rem; top: -0.3rem;
573
+ font-family: Georgia, serif; font-size: 2.6em; line-height: 1; color: #c8a98a; opacity: 0.55; }
574
+ .markdown-body blockquote p { margin: 0 0 0.6em; } .markdown-body blockquote > :last-child { margin-bottom: 0; }
575
+ .markdown-body blockquote blockquote { background: rgba(255, 255, 255, 0.4); box-shadow: none; }
576
+ .markdown-body blockquote blockquote::before { display: none; }
577
+ /* callouts — rounder, softer */
578
+ .markdown-body .info, .markdown-body .success, .markdown-body .warning, .markdown-body .danger { border-radius: 10px; }
579
+
580
+ /* spoiler — add a chevron indicator */
581
+ .markdown-body details.spoil > summary { display: flex; align-items: center; gap: 0.55rem; list-style: none; }
582
+ .markdown-body details.spoil > summary::-webkit-details-marker { display: none; }
583
+ .markdown-body details.spoil > summary::before { content: '▸'; color: #b09a78; transition: transform 0.2s; }
584
+ .markdown-body details.spoil[open] > summary::before { transform: rotate(90deg); }
@@ -31,7 +31,7 @@
31
31
  --line-height: 1.8;
32
32
 
33
33
  --markdown-body-max-width: 760px;
34
- --markdown-body-padding: 5rem 2rem;
34
+ --markdown-body-padding: 2.5rem 2rem 5rem;
35
35
  --markdown-body-border: none;
36
36
  --markdown-body-radius: 0;
37
37
  --markdown-body-shadow: none;
@@ -59,6 +59,8 @@ body {
59
59
  }
60
60
 
61
61
  /* 2. Headings and body copy */
62
+ .markdown-body > :first-child { margin-top: 0; }
63
+
62
64
  .markdown-body h1,
63
65
  .markdown-body h2,
64
66
  .markdown-body h3,
@@ -545,7 +547,6 @@ body {
545
547
  @media (max-width: 640px) {
546
548
  .markdown-body {
547
549
  padding: 2.5rem 1.25rem;
548
- font-size: 15px;
549
550
  }
550
551
  .markdown-body .left {
551
552
  float: none;
@@ -575,4 +576,23 @@ body {
575
576
  box-shadow: none;
576
577
  }
577
578
 
578
- }
579
+ }
580
+ /* code block — warm paper panel */
581
+ .markdown-body pre { background: #f6f1e7; border: 1px solid #e6dac3; border-radius: 8px; box-shadow: 0 1px 2px rgba(120,90,50,.06); }
582
+ .markdown-body pre code, .markdown-body pre code.hljs { background: transparent; padding: 0; color: inherit; }
583
+
584
+
585
+ /* ============ modern decorative refresh ============ */
586
+ /* blockquote — warm tint with a teal accent edge (pseudo-bar, not a heavy border) */
587
+ .markdown-body blockquote { position: relative; background: #efe7d8; border: none; border-radius: 0 10px 10px 0;
588
+ padding: 0.9rem 1.3rem; margin: 1.9rem 0; font-style: italic; color: var(--text-muted); overflow: hidden; }
589
+ .markdown-body blockquote::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 4px;
590
+ background: var(--link-color); }
591
+ .markdown-body blockquote p:last-child { margin-bottom: 0; }
592
+ .markdown-body blockquote blockquote { background: rgba(255, 255, 255, 0.35); }
593
+
594
+ /* spoiler — add a chevron indicator */
595
+ .markdown-body details.spoil > summary { display: flex; align-items: center; gap: 0.55rem; list-style: none; }
596
+ .markdown-body details.spoil > summary::-webkit-details-marker { display: none; }
597
+ .markdown-body details.spoil > summary::before { content: '▸'; color: #9c8a6f; transition: transform 0.2s; }
598
+ .markdown-body details.spoil[open] > summary::before { transform: rotate(90deg); }
@@ -727,3 +727,14 @@ details.spoil > summary:focus-visible {
727
727
  page-break-inside: avoid;
728
728
  }
729
729
  }
730
+
731
+ /* code: uniform dark panel (no highlight.js inner box) */
732
+ .markdown-body pre code, .markdown-body pre code.hljs { background: transparent; padding: 0; }
733
+
734
+
735
+ /* ---- mermaid & charts: light panel for legibility on the dark background ---- */
736
+ .markdown-body div.mermaid { background: rgba(255, 255, 255, 0.88); border: 1px solid rgba(255, 255, 255, 0.1);
737
+ border-radius: 10px; padding: 1.1rem; box-shadow: 0 1px 8px rgba(0, 0, 0, 0.22); }
738
+ .markdown-body div.mermaid svg { max-width: 100%; height: auto; }
739
+ .markdown-body canvas.orz-chart { background: rgba(255, 255, 255, 0.88); border-radius: 10px; padding: 0.6rem;
740
+ box-shadow: 0 1px 8px rgba(0, 0, 0, 0.22); max-width: 100%; height: auto; }