orz-paged 0.1.0
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.
- package/README.md +146 -0
- package/assets/app.js +413 -0
- package/assets/templates/article-page.md +71 -0
- package/assets/templates/article-section.md +83 -0
- package/assets/templates/cover-letter.md +53 -0
- package/assets/templates/cv-elegant.md +93 -0
- package/assets/templates/cv-modern.md +86 -0
- package/assets/templates/cv.md +45 -0
- package/assets/templates/exam-page.md +76 -0
- package/assets/templates/exam-section.md +59 -0
- package/assets/templates/letter.md +47 -0
- package/assets/templates/note.md +28 -0
- package/assets/templates/report-page.md +72 -0
- package/assets/templates/report-section.md +80 -0
- package/assets/themes/base.css +538 -0
- package/assets/themes/beige-decent-1.css +103 -0
- package/assets/themes/beige-decent-2.css +92 -0
- package/assets/themes/light-academic-1.css +114 -0
- package/assets/themes/light-academic-2.css +99 -0
- package/assets/themes/light-neat-1.css +90 -0
- package/assets/themes/light-neat-2.css +101 -0
- package/assets/themes/light-neat-3.css +91 -0
- package/dist/browser-entry.js +302 -0
- package/dist/cli.js +167 -0
- package/dist/doc/dynamic.js +57 -0
- package/dist/doc/elements.js +813 -0
- package/dist/doc/fonts.js +83 -0
- package/dist/doc/nyml.js +140 -0
- package/dist/doc/page-css.js +230 -0
- package/dist/doc/settings.js +390 -0
- package/dist/doc/templates.js +147 -0
- package/dist/doc/theme-fonts.js +10 -0
- package/dist/orz-paged.browser.js +1524 -0
- package/dist/paged.js +31 -0
- package/dist/render-paged.js +123 -0
- package/dist/template.js +242 -0
- package/dist/types.js +11 -0
- package/orz-paged-skills/SKILL.md +547 -0
- package/package.json +51 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* ==========================================================================
|
|
2
|
+
orz-paged · light-neat-3.css — "Orchard"
|
|
3
|
+
Calm brand-green accent on warm off-white paper, characterful headings.
|
|
4
|
+
Tinted leafy blockquote card, accent list markers, light harmonic code,
|
|
5
|
+
generous spacing. Adapted from the Orchard web theme for light / print.
|
|
6
|
+
|
|
7
|
+
Theme contract:
|
|
8
|
+
- :root sets the page-managed tokens (--accent, --page-bg, --font-*) DIRECTLY
|
|
9
|
+
so they apply by default; an explicit font_preset / decoration_color /
|
|
10
|
+
page_background overrides them (page CSS is added last & only emits when set).
|
|
11
|
+
- .orz-doc.markdown-body sets the content-only palette + construct tokens that
|
|
12
|
+
base.css consumes, plus this theme's signature construct styling.
|
|
13
|
+
========================================================================== */
|
|
14
|
+
|
|
15
|
+
:root {
|
|
16
|
+
--accent: #5f9e44;
|
|
17
|
+
--page-bg: #fbfbf8;
|
|
18
|
+
--font-body: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
19
|
+
--font-heading: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.orz-doc.markdown-body {
|
|
23
|
+
--text-main: #1b1d1a;
|
|
24
|
+
--text-muted: #6c7064;
|
|
25
|
+
--heading-color: #18211a;
|
|
26
|
+
--border-soft: #e7e7df;
|
|
27
|
+
--code-bg: #f3f6ee;
|
|
28
|
+
--table-head-bg: #f1f7ec;
|
|
29
|
+
--toc-leader: #c3d3b5;
|
|
30
|
+
--mark-bg: #fdeb9c;
|
|
31
|
+
--surface-2: #f7f8f3;
|
|
32
|
+
--ins-color: #1d7a36;
|
|
33
|
+
|
|
34
|
+
--span-red: #c0392b; --span-yellow: #b07d12; --span-green: #2e8b3d; --span-blue: #2e6db5;
|
|
35
|
+
--success-bg: #eaf6ec; --success-text: #1d7a36; --success-border: #c5e6cb;
|
|
36
|
+
--info-bg: #eef4fb; --info-text: #1f5fa6; --info-border: #cfe0f3;
|
|
37
|
+
--warning-bg: #fdf6e3; --warning-text: #8a6411; --warning-border: #efe2bf;
|
|
38
|
+
--danger-bg: #fbecec; --danger-text: #b03030; --danger-border: #f1cfcf;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/* Headings — characterful display face, firm weight, tight tracking. */
|
|
42
|
+
.orz-doc.markdown-body h1,
|
|
43
|
+
.orz-doc.markdown-body h2,
|
|
44
|
+
.orz-doc.markdown-body h3,
|
|
45
|
+
.orz-doc.markdown-body h4,
|
|
46
|
+
.orz-doc.markdown-body h5,
|
|
47
|
+
.orz-doc.markdown-body h6 { font-weight: 700; letter-spacing: -0.015em; }
|
|
48
|
+
|
|
49
|
+
.orz-doc.markdown-body h1 { font-size: 2.05em; color: var(--accent); padding-bottom: 0.35em; border-bottom: 1px solid var(--border-soft); }
|
|
50
|
+
.orz-doc.markdown-body h2 { font-size: 1.5em; }
|
|
51
|
+
.orz-doc.markdown-body h3 { font-size: 1.22em; color: color-mix(in srgb, var(--accent) 70%, #1b3514); }
|
|
52
|
+
.orz-doc.markdown-body h6 { text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); }
|
|
53
|
+
|
|
54
|
+
.orz-doc.markdown-body a {
|
|
55
|
+
color: color-mix(in srgb, var(--accent) 70%, #1b3514);
|
|
56
|
+
text-decoration-color: color-mix(in srgb, var(--accent) 38%, transparent);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* Blockquote — leafy tinted card with an accent edge. */
|
|
60
|
+
.orz-doc.markdown-body blockquote {
|
|
61
|
+
border-left: 3px solid var(--accent);
|
|
62
|
+
background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 10%, var(--page-bg)), transparent 90%);
|
|
63
|
+
color: var(--text-muted);
|
|
64
|
+
padding: 0.55em 1.1em;
|
|
65
|
+
border-radius: 0 8px 8px 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* Tables — gridded with a washed, accent-underlined header. */
|
|
69
|
+
.orz-doc.markdown-body th {
|
|
70
|
+
background: var(--table-head-bg);
|
|
71
|
+
color: var(--heading-color);
|
|
72
|
+
border-bottom: 2px solid var(--accent);
|
|
73
|
+
}
|
|
74
|
+
.orz-doc.markdown-body tbody tr:nth-child(even) td { background: color-mix(in srgb, var(--accent) 4%, transparent); }
|
|
75
|
+
|
|
76
|
+
.orz-doc.markdown-body ul > li::marker,
|
|
77
|
+
.orz-doc.markdown-body ol > li::marker { color: var(--accent); }
|
|
78
|
+
|
|
79
|
+
/* Code — light & harmonic panel, code sits flat on the tint. */
|
|
80
|
+
.orz-doc.markdown-body pre,
|
|
81
|
+
.orz-doc.markdown-body :not(pre) > code { background: var(--code-bg); border-color: var(--border-soft); }
|
|
82
|
+
.orz-doc.markdown-body :not(pre) > code { color: #2f5226; }
|
|
83
|
+
|
|
84
|
+
.orz-doc.markdown-body hr { height: 1px; background: color-mix(in srgb, var(--accent) 32%, transparent); }
|
|
85
|
+
|
|
86
|
+
.orz-toc { --toc-leader: color-mix(in srgb, var(--accent) 42%, transparent); }
|
|
87
|
+
|
|
88
|
+
/* Title-block elements — accent baseline rule. (cv-header / letterhead draw
|
|
89
|
+
their own rule via the element's `border` field, so they're excluded here.) */
|
|
90
|
+
.orz-element.orz-el-report-title,
|
|
91
|
+
.orz-element.orz-el-article-title { padding-bottom: 0.6em; border-bottom: 2px solid var(--accent); }
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser entry for orz-paged — the in-file paged-document engine (A2).
|
|
3
|
+
*
|
|
4
|
+
* esbuild bundles this (+ orz-markdown, paged.js, the document layer and
|
|
5
|
+
* assembler) into `dist/orz-paged.browser.js`, exposing `window.orzpaged`.
|
|
6
|
+
*
|
|
7
|
+
* Flow (DESIGN §6 — the critical sequencing): read the embedded Markdown →
|
|
8
|
+
* `assemble()` it → render async content (mermaid/smiles/chart) on a hidden
|
|
9
|
+
* stage and **await it + `document.fonts.ready`** (pagination depends on final
|
|
10
|
+
* heights) → paginate with paged.js into `.orz-pages` → fill TOC page numbers.
|
|
11
|
+
* Export = the browser's own print.
|
|
12
|
+
*/
|
|
13
|
+
import { assemble } from './render-paged.js';
|
|
14
|
+
import { paginate, printDocument } from './paged.js';
|
|
15
|
+
import { fontPreset } from './doc/fonts.js';
|
|
16
|
+
import { THEME_FONTS } from './doc/theme-fonts.js';
|
|
17
|
+
import { applyDynamicChoices, collectDynamicChoices } from './doc/dynamic.js';
|
|
18
|
+
const VERSION = typeof __ORZPAGED_VERSION__ !== 'undefined' ? __ORZPAGED_VERSION__ : '0.0.0';
|
|
19
|
+
function cfg() {
|
|
20
|
+
return window.__ORZ_PAGED__ || {};
|
|
21
|
+
}
|
|
22
|
+
/* ---------- helpers ---------- */
|
|
23
|
+
function sourceText() {
|
|
24
|
+
const el = document.getElementById('orz-src');
|
|
25
|
+
return (el ? el.textContent || '' : '').replace(/^\n/, '').replace(/\n\s*$/, '');
|
|
26
|
+
}
|
|
27
|
+
function pagesContainer() {
|
|
28
|
+
let el = document.getElementById('orz-pages');
|
|
29
|
+
if (!el) {
|
|
30
|
+
el = document.createElement('div');
|
|
31
|
+
el.id = 'orz-pages';
|
|
32
|
+
// paged.js reflows the .markdown-body content into page boxes, dropping that
|
|
33
|
+
// wrapper; mark the container so the orz runtime's copy-as-Markdown handler
|
|
34
|
+
// still fires for a selection in the rendered pages.
|
|
35
|
+
el.setAttribute('data-orz-copy', '');
|
|
36
|
+
document.body.appendChild(el);
|
|
37
|
+
}
|
|
38
|
+
return el;
|
|
39
|
+
}
|
|
40
|
+
function loadScript(src) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
if (!src)
|
|
43
|
+
return resolve();
|
|
44
|
+
if (document.querySelector(`script[data-lib="${src}"]`))
|
|
45
|
+
return resolve();
|
|
46
|
+
const s = document.createElement('script');
|
|
47
|
+
s.src = src;
|
|
48
|
+
s.async = true;
|
|
49
|
+
s.setAttribute('data-lib', src);
|
|
50
|
+
s.setAttribute('data-orz-runtime', '1'); // stripped before save (self-reproducing)
|
|
51
|
+
s.onload = () => resolve();
|
|
52
|
+
s.onerror = () => reject(new Error('failed to load ' + src));
|
|
53
|
+
document.head.appendChild(s);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function ensureLink(href, id) {
|
|
57
|
+
if (!href)
|
|
58
|
+
return;
|
|
59
|
+
if (id && document.getElementById(id))
|
|
60
|
+
return;
|
|
61
|
+
if (!id && document.querySelector(`link[href="${href}"]`))
|
|
62
|
+
return;
|
|
63
|
+
const l = document.createElement('link');
|
|
64
|
+
l.rel = 'stylesheet';
|
|
65
|
+
l.href = href;
|
|
66
|
+
if (id)
|
|
67
|
+
l.id = id;
|
|
68
|
+
l.setAttribute('data-orz-runtime', '1'); // stripped before save (self-reproducing)
|
|
69
|
+
document.head.appendChild(l);
|
|
70
|
+
}
|
|
71
|
+
/** Turn a drawn <canvas> into an <img> so it survives HTML serialization for paged.js. */
|
|
72
|
+
function canvasToImg(canvas) {
|
|
73
|
+
try {
|
|
74
|
+
const img = document.createElement('img');
|
|
75
|
+
img.src = canvas.toDataURL('image/png');
|
|
76
|
+
img.className = canvas.className;
|
|
77
|
+
img.style.maxWidth = '100%';
|
|
78
|
+
const dm = canvas.getAttribute('data-md');
|
|
79
|
+
if (dm != null)
|
|
80
|
+
img.setAttribute('data-md', dm); // preserve copy-as-Markdown breadcrumb
|
|
81
|
+
canvas.replaceWith(img);
|
|
82
|
+
}
|
|
83
|
+
catch { /* tainted/empty canvas — leave as-is */ }
|
|
84
|
+
}
|
|
85
|
+
/** Load (on demand) and run mermaid / smiles / chart on `root`, then settle to static HTML. */
|
|
86
|
+
async function renderEnhancers(root) {
|
|
87
|
+
const e = cfg().enhancers || {};
|
|
88
|
+
const w = window;
|
|
89
|
+
const jobs = [];
|
|
90
|
+
// Syntax-highlight code on the stage before pagination, so the highlighted
|
|
91
|
+
// markup flows into the pages (and prints).
|
|
92
|
+
if (e.hljsJs && root.querySelector('pre code')) {
|
|
93
|
+
await loadScript(e.hljsJs);
|
|
94
|
+
if (w.hljs) {
|
|
95
|
+
root.querySelectorAll('pre code:not(.hljs)').forEach((b) => {
|
|
96
|
+
try {
|
|
97
|
+
w.hljs.highlightElement(b);
|
|
98
|
+
}
|
|
99
|
+
catch { /* ignore */ }
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (e.mermaidJs && root.querySelector('.mermaid')) {
|
|
104
|
+
await loadScript(e.mermaidJs);
|
|
105
|
+
if (w.mermaid) {
|
|
106
|
+
w.mermaid.initialize({ startOnLoad: false });
|
|
107
|
+
jobs.push(Promise.resolve(w.mermaid.run({ nodes: root.querySelectorAll('.mermaid') })).catch(() => undefined));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (e.smilesJs && root.querySelector('canvas[data-smiles]')) {
|
|
111
|
+
await loadScript(e.smilesJs);
|
|
112
|
+
if (w.SmilesDrawer) {
|
|
113
|
+
root.querySelectorAll('canvas[data-smiles]').forEach((c) => {
|
|
114
|
+
const drawer = new w.SmilesDrawer.Drawer({ width: c.width, height: c.height });
|
|
115
|
+
jobs.push(new Promise((res) => {
|
|
116
|
+
w.SmilesDrawer.parse(c.getAttribute('data-smiles'), (tree) => {
|
|
117
|
+
try {
|
|
118
|
+
drawer.draw(tree, c, 'light', false);
|
|
119
|
+
}
|
|
120
|
+
catch { /* ignore */ }
|
|
121
|
+
res();
|
|
122
|
+
}, () => res());
|
|
123
|
+
}));
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (e.chartJs && root.querySelector('canvas.orz-chart[data-chart]')) {
|
|
128
|
+
await loadScript(e.chartJs);
|
|
129
|
+
if (w.Chart) {
|
|
130
|
+
root.querySelectorAll('canvas.orz-chart[data-chart]').forEach((c) => {
|
|
131
|
+
try {
|
|
132
|
+
const conf = JSON.parse(c.getAttribute('data-chart') || '{}');
|
|
133
|
+
conf.options = { ...(conf.options || {}), responsive: false, animation: false };
|
|
134
|
+
new w.Chart(c, conf);
|
|
135
|
+
}
|
|
136
|
+
catch { /* ignore */ }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
await Promise.all(jobs);
|
|
141
|
+
// Freeze canvases (smiles/chart) to images so paginating from innerHTML keeps them.
|
|
142
|
+
root.querySelectorAll('canvas').forEach((c) => canvasToImg(c));
|
|
143
|
+
}
|
|
144
|
+
/* ---------- render ---------- */
|
|
145
|
+
let lastAssembly = null;
|
|
146
|
+
let themeOverride = null; // editor theme picker; null = the document's theme
|
|
147
|
+
// Dynamic switch: the document's `dynamic_choices` is the single source of truth
|
|
148
|
+
// (the editor's switcher writes back to it), so there's no separate override here.
|
|
149
|
+
let lastDynChoices = {}; // choices last applied (from the source)
|
|
150
|
+
let lastDynOptions = {}; // key → values seen (for the switcher UI)
|
|
151
|
+
function stylesheetsFor(a) {
|
|
152
|
+
const c = cfg();
|
|
153
|
+
const theme = themeOverride || a.theme;
|
|
154
|
+
const sheets = [];
|
|
155
|
+
if (c.mode === 'cdn') {
|
|
156
|
+
if (c.baseUrl)
|
|
157
|
+
sheets.push(c.baseUrl);
|
|
158
|
+
if (theme !== 'none' && c.themeUrls && c.themeUrls[theme])
|
|
159
|
+
sheets.push(c.themeUrls[theme]);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
if (c.baseCss)
|
|
163
|
+
sheets.push({ 'orz-base.css': c.baseCss });
|
|
164
|
+
if (theme !== 'none' && c.themes && c.themes[theme])
|
|
165
|
+
sheets.push({ 'orz-theme.css': c.themes[theme] });
|
|
166
|
+
}
|
|
167
|
+
// NOTE: do NOT push KaTeX (a URL) here — paged.js' polisher would XHR-fetch it,
|
|
168
|
+
// which file:// blocks (cross-origin from an opaque origin), breaking pagination
|
|
169
|
+
// offline. KaTeX is loaded as a <head> <link> in render(), which styles the
|
|
170
|
+
// .pagedjs_page math globally — no fetch needed. (In --inline mode every entry
|
|
171
|
+
// below is inline CSS, so pagination needs no network at all.)
|
|
172
|
+
sheets.push({ 'orz-page.css': a.css }); // @page rules + element CSS — last so it wins
|
|
173
|
+
return sheets;
|
|
174
|
+
}
|
|
175
|
+
/** Assemble `source`, render async content, and paginate into `.orz-pages`. */
|
|
176
|
+
async function render(source) {
|
|
177
|
+
const a = assemble(source);
|
|
178
|
+
lastAssembly = a;
|
|
179
|
+
// Stylesheets the document needs in <head> for fonts + math (and staging measurement).
|
|
180
|
+
// The active theme owns the document font: load its webfonts (lazily — only the
|
|
181
|
+
// active theme's, so a theme switch loads the new font, then re-paginates after
|
|
182
|
+
// document.fonts.ready). An explicit font_preset in the source still wins (its
|
|
183
|
+
// URL is in a.fontCssUrls and page CSS emits --font-body to override the theme).
|
|
184
|
+
const activeTheme = (themeOverride || a.theme);
|
|
185
|
+
const tf = THEME_FONTS[activeTheme];
|
|
186
|
+
if (tf) {
|
|
187
|
+
ensureLink(fontPreset(tf.body).cssUrl);
|
|
188
|
+
ensureLink(fontPreset(tf.heading).cssUrl);
|
|
189
|
+
}
|
|
190
|
+
a.fontCssUrls.forEach((u) => ensureLink(u));
|
|
191
|
+
if (cfg().katexCss)
|
|
192
|
+
ensureLink(cfg().katexCss, 'orz-katex');
|
|
193
|
+
if (cfg().hljsCss)
|
|
194
|
+
ensureLink(cfg().hljsCss, 'orz-hljs');
|
|
195
|
+
// Stage off-screen so diagrams render (and get real sizes) before pagination.
|
|
196
|
+
const stage = document.createElement('div');
|
|
197
|
+
stage.className = 'orz-stage';
|
|
198
|
+
stage.style.cssText = 'position:absolute;left:-99999px;top:0;width:680px;';
|
|
199
|
+
stage.innerHTML = a.bodyHtml;
|
|
200
|
+
document.body.appendChild(stage);
|
|
201
|
+
// Dynamic switch: collect the available choices (for the editor's live
|
|
202
|
+
// switcher) BEFORE resolving, then remove conditional content that doesn't
|
|
203
|
+
// match the effective choices (source `dynamic_choices` + live overrides).
|
|
204
|
+
lastDynOptions = collectDynamicChoices(stage);
|
|
205
|
+
lastDynChoices = { ...a.settings.dynamicChoices };
|
|
206
|
+
applyDynamicChoices(stage, lastDynChoices);
|
|
207
|
+
// Double-buffer: paginate into an off-screen container, then swap it in. The old
|
|
208
|
+
// pages stay visible until the swap, so a re-render never flashes to the top —
|
|
209
|
+
// and we restore the scroll position across the swap.
|
|
210
|
+
const out = document.createElement('div');
|
|
211
|
+
out.style.cssText = 'position:absolute;left:-99999px;top:0;width:900px;';
|
|
212
|
+
document.body.appendChild(out);
|
|
213
|
+
try {
|
|
214
|
+
await renderEnhancers(stage.querySelector('.orz-doc') || stage);
|
|
215
|
+
if (document.fonts && document.fonts.ready) {
|
|
216
|
+
try {
|
|
217
|
+
await document.fonts.ready;
|
|
218
|
+
}
|
|
219
|
+
catch { /* ignore */ }
|
|
220
|
+
}
|
|
221
|
+
await paginate(stage.innerHTML, stylesheetsFor(a), out);
|
|
222
|
+
const pages = pagesContainer();
|
|
223
|
+
const prevScroll = pages.scrollTop;
|
|
224
|
+
pages.replaceChildren(...Array.prototype.slice.call(out.childNodes));
|
|
225
|
+
pages.scrollTop = prevScroll;
|
|
226
|
+
// Clean front matter: renumber the body in JS (paged.js' page counter is
|
|
227
|
+
// unreliable across later page breaks). Each non-front page gets a sequential
|
|
228
|
+
// --orz-pageno (1, 2, …) that its page-number box reads; --orz-body-pages is
|
|
229
|
+
// the body total. Quote them — content var() interpolation needs a <string>.
|
|
230
|
+
if (a.settings.frontMatterClean) {
|
|
231
|
+
const allPages = Array.prototype.slice.call(pages.querySelectorAll('.pagedjs_page'));
|
|
232
|
+
const bodyPages = allPages.filter((p) => !p.classList.contains('pagedjs_orz-front_page'));
|
|
233
|
+
document.documentElement.style.setProperty('--orz-body-pages', `"${bodyPages.length}"`);
|
|
234
|
+
bodyPages.forEach((p, i) => p.style.setProperty('--orz-pageno', `"${i + 1}"`));
|
|
235
|
+
}
|
|
236
|
+
fillToc();
|
|
237
|
+
// let the editor re-fit the preview zoom to the pane (it owns the layout)
|
|
238
|
+
const hook = window.__orzPagedAfterRender;
|
|
239
|
+
if (hook) {
|
|
240
|
+
try {
|
|
241
|
+
hook();
|
|
242
|
+
}
|
|
243
|
+
catch { /* ignore */ }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
stage.remove();
|
|
248
|
+
out.remove();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** After pagination, fill `.orz-toc` placeholders with heading links + page numbers. */
|
|
252
|
+
function fillToc() {
|
|
253
|
+
const pages = pagesContainer();
|
|
254
|
+
const tocNav = pages.querySelector('.orz-toc');
|
|
255
|
+
if (!tocNav)
|
|
256
|
+
return;
|
|
257
|
+
const maxLevel = Number(tocNav.getAttribute('data-max-level') || '3');
|
|
258
|
+
const items = [];
|
|
259
|
+
pages.querySelectorAll('.pagedjs_page h1, .pagedjs_page h2, .pagedjs_page h3, .pagedjs_page h4').forEach((h) => {
|
|
260
|
+
const level = Number(h.tagName.slice(1));
|
|
261
|
+
if (level > maxLevel)
|
|
262
|
+
return;
|
|
263
|
+
const page = h.closest('.pagedjs_page');
|
|
264
|
+
const pageNum = page ? page.getAttribute('data-page-number') || '' : '';
|
|
265
|
+
items.push(`<li class="orz-toc-item orz-toc-l${level}"><span class="orz-toc-text">${h.textContent || ''}</span>` +
|
|
266
|
+
`<span class="orz-toc-page">${pageNum}</span></li>`);
|
|
267
|
+
});
|
|
268
|
+
if (items.length)
|
|
269
|
+
tocNav.innerHTML = `<ol class="orz-toc-list">${items.join('')}</ol>`;
|
|
270
|
+
}
|
|
271
|
+
/* ---------- public API ---------- */
|
|
272
|
+
const api = {
|
|
273
|
+
version: VERSION,
|
|
274
|
+
assemble,
|
|
275
|
+
/** Re-render from a (possibly edited) source. */
|
|
276
|
+
render: (source) => render(source),
|
|
277
|
+
/** Re-render from the embedded source. */
|
|
278
|
+
refresh: () => render(sourceText()),
|
|
279
|
+
/** Export to PDF via browser print. */
|
|
280
|
+
exportPdf: () => printDocument(),
|
|
281
|
+
/** Force a theme (editor picker); '' / 'none' clears the override. */
|
|
282
|
+
setTheme: (id) => { themeOverride = id && id !== 'none' ? id : null; return render(sourceText()); },
|
|
283
|
+
/** The theme currently in effect (the override, or the document's own theme). */
|
|
284
|
+
getTheme: () => themeOverride || (lastAssembly ? lastAssembly.theme : null),
|
|
285
|
+
/** Dynamic switch: the keys/values available + the effective choices (from the
|
|
286
|
+
* source's `dynamic_choices`). The editor's switcher writes back to the source
|
|
287
|
+
* so the dropdown and the nyml block always agree. */
|
|
288
|
+
getDynamicState: () => ({
|
|
289
|
+
options: lastDynOptions,
|
|
290
|
+
choices: lastDynChoices,
|
|
291
|
+
}),
|
|
292
|
+
/** The last assembly (resolved settings etc.) for the editor's settings panel. */
|
|
293
|
+
get assembly() { return lastAssembly; },
|
|
294
|
+
/** Read the embedded source. */
|
|
295
|
+
source: sourceText,
|
|
296
|
+
};
|
|
297
|
+
window.orzpaged = api;
|
|
298
|
+
function boot() { render(sourceText()).catch((err) => console.error('[orz-paged]', err)); }
|
|
299
|
+
if (document.readyState === 'loading')
|
|
300
|
+
document.addEventListener('DOMContentLoaded', boot);
|
|
301
|
+
else
|
|
302
|
+
boot();
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* orz-paged CLI (A5) — generate a self-contained `.paged.html` from Markdown.
|
|
4
|
+
*
|
|
5
|
+
* orz-paged <input.md> [-o out.paged.html] [--inline|--cdn] [--title t]
|
|
6
|
+
*
|
|
7
|
+
* `--inline` (default) embeds the engine bundle + base/theme CSS; `--cdn`
|
|
8
|
+
* references the published `orz-paged-browser` on jsDelivr. Fonts, images, and the
|
|
9
|
+
* math/diagram libs always load from CDN at view time (DESIGN §4).
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync, writeFileSync, readdirSync, existsSync } from 'node:fs';
|
|
12
|
+
import { dirname, join, basename, resolve } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { getBrowserRuntimeScript } from 'orz-markdown/runtime';
|
|
15
|
+
import { PREVIEW_CDN } from 'orz-markdown/preview-frame';
|
|
16
|
+
import { buildHtml } from './template.js';
|
|
17
|
+
import { templateList } from './doc/templates.js';
|
|
18
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const ROOT = join(HERE, '..');
|
|
20
|
+
const selfVersion = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8')).version || '0.0.0';
|
|
21
|
+
/** Pinned CDN assets used at view time — sourced from orz-markdown's shared
|
|
22
|
+
* preview-frame helper so every host app loads identical, tested versions.
|
|
23
|
+
* Paged is light-only, so highlight.js uses the light (github) stylesheet. */
|
|
24
|
+
const CDN = {
|
|
25
|
+
katexCss: PREVIEW_CDN.katexCss,
|
|
26
|
+
mermaidJs: PREVIEW_CDN.mermaidJs,
|
|
27
|
+
smilesJs: PREVIEW_CDN.smilesJs,
|
|
28
|
+
chartJs: PREVIEW_CDN.chartJs,
|
|
29
|
+
hljsJs: PREVIEW_CDN.hljsJs,
|
|
30
|
+
hljsCss: PREVIEW_CDN.hljsLightCss,
|
|
31
|
+
};
|
|
32
|
+
function findAsset(rel) {
|
|
33
|
+
const p = join(ROOT, rel);
|
|
34
|
+
if (!existsSync(p))
|
|
35
|
+
throw new Error('missing asset: ' + rel + ' (run `npm run build` first)');
|
|
36
|
+
return p;
|
|
37
|
+
}
|
|
38
|
+
/** Drop a theme's `@import url("./base.css")` — base is inlined separately, so the
|
|
39
|
+
* relative import points nowhere (404 on http; a fetch hazard on file://). */
|
|
40
|
+
function stripBaseImport(css) {
|
|
41
|
+
return css.replace(/@import\s+url\(\s*['"]?\.\/base\.css['"]?\s*\)\s*;?/gi, '');
|
|
42
|
+
}
|
|
43
|
+
function loadThemes() {
|
|
44
|
+
const dir = findAsset('assets/themes');
|
|
45
|
+
const baseCss = readFileSync(join(dir, 'base.css'), 'utf8');
|
|
46
|
+
const themes = readdirSync(dir)
|
|
47
|
+
.filter((f) => f.endsWith('.css') && f !== 'base.css')
|
|
48
|
+
.map((f) => ({ id: f.replace(/\.css$/, ''), css: stripBaseImport(readFileSync(join(dir, f), 'utf8')) }));
|
|
49
|
+
return { baseCss, themes };
|
|
50
|
+
}
|
|
51
|
+
/** Load every template's starter skeleton from `assets/templates/<stem>.md`. */
|
|
52
|
+
function loadTemplates() {
|
|
53
|
+
const dir = findAsset('assets/templates');
|
|
54
|
+
return templateList().map((t) => ({
|
|
55
|
+
id: t.id,
|
|
56
|
+
label: t.label,
|
|
57
|
+
description: t.description,
|
|
58
|
+
group: t.group,
|
|
59
|
+
skeleton: readFileSync(join(dir, t.skeleton + '.md'), 'utf8'),
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
function parseArgs(argv) {
|
|
63
|
+
const a = { mode: 'inline' };
|
|
64
|
+
for (let i = 0; i < argv.length; i++) {
|
|
65
|
+
const arg = argv[i];
|
|
66
|
+
if (arg === '-o' || arg === '--out')
|
|
67
|
+
a.out = argv[++i];
|
|
68
|
+
else if (arg === '--inline')
|
|
69
|
+
a.mode = 'inline';
|
|
70
|
+
else if (arg === '--cdn')
|
|
71
|
+
a.mode = 'cdn';
|
|
72
|
+
else if (arg === '--title')
|
|
73
|
+
a.title = argv[++i];
|
|
74
|
+
else if (arg === '--template')
|
|
75
|
+
a.template = argv[++i];
|
|
76
|
+
else if (arg === '--new')
|
|
77
|
+
a.newDoc = argv[++i];
|
|
78
|
+
else if (arg === '--list-templates')
|
|
79
|
+
a.list = true;
|
|
80
|
+
else if (!arg.startsWith('-'))
|
|
81
|
+
a.input = arg;
|
|
82
|
+
}
|
|
83
|
+
return a;
|
|
84
|
+
}
|
|
85
|
+
/** Resolve a (possibly legacy) template name to its loaded skeleton, or exit. */
|
|
86
|
+
function skeletonOrExit(name, templates) {
|
|
87
|
+
const t = templates.find((x) => x.id === name)
|
|
88
|
+
?? templates.find((x) => x.id === name + '-section'); // legacy: article→article-section
|
|
89
|
+
if (!t) {
|
|
90
|
+
console.error(`Unknown template: ${name}\nKnown: ${templates.map((x) => x.id).join(', ')}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
return t;
|
|
94
|
+
}
|
|
95
|
+
function main() {
|
|
96
|
+
const args = parseArgs(process.argv.slice(2));
|
|
97
|
+
const templates = loadTemplates();
|
|
98
|
+
// `--list-templates` — print the catalog and exit.
|
|
99
|
+
if (args.list) {
|
|
100
|
+
console.log('Templates (use --template <name> or --new <name>):\n');
|
|
101
|
+
let group = '';
|
|
102
|
+
for (const t of templates) {
|
|
103
|
+
if (t.group !== group) {
|
|
104
|
+
group = t.group;
|
|
105
|
+
console.log(` ${group}`);
|
|
106
|
+
}
|
|
107
|
+
console.log(` ${t.id.padEnd(16)} ${t.description}`);
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// `--new <name>` — scaffold the starter Markdown to a .md file (then edit it).
|
|
112
|
+
if (args.newDoc) {
|
|
113
|
+
const t = skeletonOrExit(args.newDoc, templates);
|
|
114
|
+
const outMd = args.out ? resolve(args.out) : resolve(t.id + '.md');
|
|
115
|
+
writeFileSync(outMd, t.skeleton, 'utf8');
|
|
116
|
+
console.log(`Wrote ${outMd} (starter for "${t.label}")`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Source = an input file, or a template skeleton via `--template <name>`.
|
|
120
|
+
let source;
|
|
121
|
+
let base;
|
|
122
|
+
if (args.input) {
|
|
123
|
+
source = readFileSync(resolve(args.input), 'utf8');
|
|
124
|
+
base = basename(resolve(args.input)).replace(/\.(md|markdown)$/i, '');
|
|
125
|
+
}
|
|
126
|
+
else if (args.template) {
|
|
127
|
+
const t = skeletonOrExit(args.template, templates);
|
|
128
|
+
source = t.skeleton;
|
|
129
|
+
base = t.id;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
console.error('Usage: orz-paged <input.md> [-o out.paged.html] [--inline|--cdn] [--title t]\n' +
|
|
133
|
+
' orz-paged --template <name> [-o out.paged.html] # scaffold + render a starter\n' +
|
|
134
|
+
' orz-paged --new <name> [-o out.md] # write a starter .md to edit\n' +
|
|
135
|
+
' orz-paged --list-templates');
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
const inDir = args.input ? dirname(resolve(args.input)) : process.cwd();
|
|
139
|
+
const outPath = args.out ? resolve(args.out) : join(inDir, base + '.paged.html');
|
|
140
|
+
const title = args.title || base;
|
|
141
|
+
const { baseCss, themes } = loadThemes();
|
|
142
|
+
const appJs = readFileSync(findAsset('assets/app.js'), 'utf8');
|
|
143
|
+
const runtime = getBrowserRuntimeScript(); // copy-as-Markdown (+ qr) from orz-markdown
|
|
144
|
+
let renderer;
|
|
145
|
+
if (args.mode === 'cdn') {
|
|
146
|
+
renderer = { mode: 'cdn', src: `https://cdn.jsdelivr.net/npm/orz-paged-browser@${selfVersion}/orz-paged.browser.js` };
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
renderer = { mode: 'inline', js: readFileSync(findAsset('dist/orz-paged.browser.js'), 'utf8') };
|
|
150
|
+
}
|
|
151
|
+
const html = buildHtml({
|
|
152
|
+
source,
|
|
153
|
+
title,
|
|
154
|
+
rendererVersion: selfVersion,
|
|
155
|
+
renderer,
|
|
156
|
+
baseCss,
|
|
157
|
+
themes,
|
|
158
|
+
defaultTheme: themes.some((t) => t.id === 'light-neat-1') ? 'light-neat-1' : (themes[0]?.id ?? 'none'),
|
|
159
|
+
templates,
|
|
160
|
+
cdn: CDN,
|
|
161
|
+
appJs,
|
|
162
|
+
runtime,
|
|
163
|
+
});
|
|
164
|
+
writeFileSync(outPath, html, 'utf8');
|
|
165
|
+
console.log(`Wrote ${outPath} (${args.mode}, ${themes.length} themes)`);
|
|
166
|
+
}
|
|
167
|
+
main();
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dynamic switch — conditional content driven by the `dynamic_choices` document
|
|
3
|
+
* setting. Elements carry `data-show-when="key=value"` / `data-hide-when="key=value"`;
|
|
4
|
+
* at render time non-matching ones are removed and the attribute is stripped from
|
|
5
|
+
* those that stay. One source can then print several versions (e.g. an exam with
|
|
6
|
+
* `answer-key: hide` for students and `show` for the instructor key).
|
|
7
|
+
*
|
|
8
|
+
* Operates on a live DOM (browser render) so nested removals are correct.
|
|
9
|
+
*/
|
|
10
|
+
/** Normalize a choice key: lowercase, hyphens → underscores (matches settings). */
|
|
11
|
+
export function normalizeChoiceKey(key) {
|
|
12
|
+
return key.trim().toLowerCase().replace(/-/g, '_');
|
|
13
|
+
}
|
|
14
|
+
function parseCond(raw) {
|
|
15
|
+
const eq = raw.indexOf('=');
|
|
16
|
+
if (eq < 0)
|
|
17
|
+
return { key: normalizeChoiceKey(raw), value: '' };
|
|
18
|
+
return { key: normalizeChoiceKey(raw.slice(0, eq)), value: raw.slice(eq + 1).trim() };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolve every `data-show-when` / `data-hide-when` under `root` against
|
|
22
|
+
* `choices`, mutating the DOM in place: drop non-matching elements, strip the
|
|
23
|
+
* attribute from the rest.
|
|
24
|
+
*/
|
|
25
|
+
export function applyDynamicChoices(root, choices) {
|
|
26
|
+
Array.prototype.forEach.call(root.querySelectorAll('[data-show-when]'), (el) => {
|
|
27
|
+
const { key, value } = parseCond(el.getAttribute('data-show-when') || '');
|
|
28
|
+
if (choices[key] === value)
|
|
29
|
+
el.removeAttribute('data-show-when');
|
|
30
|
+
else
|
|
31
|
+
el.remove();
|
|
32
|
+
});
|
|
33
|
+
Array.prototype.forEach.call(root.querySelectorAll('[data-hide-when]'), (el) => {
|
|
34
|
+
const { key, value } = parseCond(el.getAttribute('data-hide-when') || '');
|
|
35
|
+
if (choices[key] === value)
|
|
36
|
+
el.remove();
|
|
37
|
+
else
|
|
38
|
+
el.removeAttribute('data-hide-when');
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Collect the dynamic keys present under `root` and the set of values each is
|
|
43
|
+
* used with — for building a live switcher UI. Returns key → sorted values.
|
|
44
|
+
*/
|
|
45
|
+
export function collectDynamicChoices(root) {
|
|
46
|
+
const sets = {};
|
|
47
|
+
for (const attr of ['data-show-when', 'data-hide-when']) {
|
|
48
|
+
Array.prototype.forEach.call(root.querySelectorAll('[' + attr + ']'), (el) => {
|
|
49
|
+
const { key, value } = parseCond(el.getAttribute(attr) || '');
|
|
50
|
+
(sets[key] = sets[key] || new Set()).add(value);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const out = {};
|
|
54
|
+
for (const k of Object.keys(sets))
|
|
55
|
+
out[k] = Array.from(sets[k]).sort();
|
|
56
|
+
return out;
|
|
57
|
+
}
|