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.
Files changed (39) hide show
  1. package/README.md +146 -0
  2. package/assets/app.js +413 -0
  3. package/assets/templates/article-page.md +71 -0
  4. package/assets/templates/article-section.md +83 -0
  5. package/assets/templates/cover-letter.md +53 -0
  6. package/assets/templates/cv-elegant.md +93 -0
  7. package/assets/templates/cv-modern.md +86 -0
  8. package/assets/templates/cv.md +45 -0
  9. package/assets/templates/exam-page.md +76 -0
  10. package/assets/templates/exam-section.md +59 -0
  11. package/assets/templates/letter.md +47 -0
  12. package/assets/templates/note.md +28 -0
  13. package/assets/templates/report-page.md +72 -0
  14. package/assets/templates/report-section.md +80 -0
  15. package/assets/themes/base.css +538 -0
  16. package/assets/themes/beige-decent-1.css +103 -0
  17. package/assets/themes/beige-decent-2.css +92 -0
  18. package/assets/themes/light-academic-1.css +114 -0
  19. package/assets/themes/light-academic-2.css +99 -0
  20. package/assets/themes/light-neat-1.css +90 -0
  21. package/assets/themes/light-neat-2.css +101 -0
  22. package/assets/themes/light-neat-3.css +91 -0
  23. package/dist/browser-entry.js +302 -0
  24. package/dist/cli.js +167 -0
  25. package/dist/doc/dynamic.js +57 -0
  26. package/dist/doc/elements.js +813 -0
  27. package/dist/doc/fonts.js +83 -0
  28. package/dist/doc/nyml.js +140 -0
  29. package/dist/doc/page-css.js +230 -0
  30. package/dist/doc/settings.js +390 -0
  31. package/dist/doc/templates.js +147 -0
  32. package/dist/doc/theme-fonts.js +10 -0
  33. package/dist/orz-paged.browser.js +1524 -0
  34. package/dist/paged.js +31 -0
  35. package/dist/render-paged.js +123 -0
  36. package/dist/template.js +242 -0
  37. package/dist/types.js +11 -0
  38. package/orz-paged-skills/SKILL.md +547 -0
  39. package/package.json +51 -0
package/README.md ADDED
@@ -0,0 +1,146 @@
1
+ # orz-paged
2
+
3
+ > **Status: built and browser-verified** — 12 starter templates, 7 light themes,
4
+ > the full document-element set, an in-file editor with a template picker, and a
5
+ > dynamic switch (e.g. exam answer keys). **Not yet published to npm**, so use it
6
+ > from a local checkout: `npm install`, `npm run build`, then
7
+ > `npm run gen -- tests/sample.md` produces a working `.paged.html`. See
8
+ > [DESIGN.md](./DESIGN.md) for the design.
9
+
10
+ Generate a single, self-contained, **editable** `.paged.html` from Markdown — a
11
+ document shown as **print pages** in any browser (A4/Letter, real margins,
12
+ running headers/footers, page numbers), edited and saved **in the browser**, and
13
+ exported to PDF by **printing it** (no PDF binary, no Puppeteer).
14
+
15
+ It is the paginated-document sibling of:
16
+
17
+ - **[orz-mdhtml](../orz-mdhtml)** — a continuous document (`.md.html`)
18
+ - **[orz-slides](../orz-slides)** — a reveal.js deck (`.slides.html`)
19
+
20
+ and is authored in **[orz-markdown](../orz-markdown)**, the family's parser.
21
+
22
+ ## How it differs from `.md.pdf`
23
+
24
+ The **orz-md-pdf** VS Code extension produces a `.md.pdf`: a real PDF binary with
25
+ the Markdown embedded inside, rendered by Puppeteer. **orz-paged keeps the file as
26
+ editable HTML** in a paged view; a PDF is produced only on demand, by the
27
+ browser's own `Print → Save as PDF`. It borrows orz-md-pdf's *document model* (as
28
+ a spec, reimplemented in TypeScript) and its paged.js pagination — not its PDF
29
+ binary, Puppeteer, or untested code.
30
+
31
+ ## Principles
32
+
33
+ - **Print on light paper.** White or light-colored backgrounds only — **no dark
34
+ mode**, no screen-only constructs (avoid tabs, spoilers, embedded video).
35
+ - **Assume internet.** Fonts, web images, and math/diagram libraries load from
36
+ CDN at view/print time; files stay small.
37
+ - **Curated, not sprawling.** A dozen common templates + seven light themes (shared
38
+ with the orz family) + a clear agent skill, rather than every option — but every
39
+ element/theme/setting stays independently composable for power users and agents.
40
+
41
+ ## Pipeline (all in the browser)
42
+
43
+ ```
44
+ Markdown ──orz-markdown──► content HTML
45
+ ──pipeline-model─► paged HTML (page config, headers/footers, elements, fonts, theme)
46
+ ──paged.js───────► .pagedjs_page boxes (the visible pages — and what prints)
47
+ ```
48
+
49
+ ## Commands
50
+
51
+ ```
52
+ orz-paged <input.md> [-o out.paged.html] [--inline | --cdn] [--title t]
53
+ orz-paged --template <name> [-o out.paged.html] # scaffold + render a starter document
54
+ orz-paged --new <name> [-o out.md] # write a starter .md you then edit
55
+ orz-paged --list-templates # list the templates
56
+ ```
57
+
58
+ `--inline` (default) embeds the engine + paged.js + every theme. `--cdn` instead
59
+ references jsDelivr for the engine + theme — but that needs `orz-paged-browser`
60
+ published, so **while orz-paged is unpublished, use the default `--inline`**.
61
+ **Fonts, web images, and math/diagram libraries always load from CDN** (internet
62
+ assumed) — so files stay small. The document's `{{nyml kind: document}}` block
63
+ (`template:`, `theme:`, …) is the source of truth.
64
+
65
+ ## Templates & themes
66
+
67
+ **Clear split:** a **template owns layout** (page size, furniture, which elements
68
+ appear and where) and a **theme owns the look** (font, decoration color, element
69
+ styling) — so the same theme renders the same across every template.
70
+
71
+ Twelve starter **templates** scaffold a real document, not just defaults —
72
+ `article`, `report`, and `exam` each in a **title-page** and a **title-section**
73
+ variant, three CV styles (`cv` classic, `cv-modern`, `cv-elegant`), plus
74
+ `letter`, `cover-letter`, and `note`. Pick one with `--template <name>`, or in the
75
+ in-file editor from the **template picker** (the 📄 toolbar button); the picker
76
+ drops the starter into the editor (keeping any existing content in a comment).
77
+
78
+ Seven light **themes** are vendored from orz-markdown and print-adapted —
79
+ `light-neat-1/2/3`, `light-academic-1/2`, `beige-decent-1/2` — selectable per
80
+ document (`theme:`) or live in the editor (switching swaps font + color + style
81
+ together). The **default is `light-academic-1`**. They are **light only by design**
82
+ (ink on paper); an explicit `font_preset` / `decoration_color` / `page_background`
83
+ still overrides a theme's font / accent / page tint.
84
+
85
+ ## Dynamic switch — one source, several versions
86
+
87
+ A `dynamic_choices` document setting (a `key: value` map) drives conditional
88
+ content: elements tagged `data-show-when="key=value"` / `data-hide-when="key=value"`
89
+ are kept or dropped at render time. The headline use is **exam answer keys** — the
90
+ question answers are tagged `answer-key=show`, so one source prints both the
91
+ student copy (`answer-key: hide`) and the instructor key (`show`). Flip it in the
92
+ source, or live from the editor's **answer key** dropdown. Define your own keys for
93
+ any tailored-variant document.
94
+
95
+ ## Authoring
96
+
97
+ A `{{nyml kind: document}}` block sets page size/margins, font, headers/footers,
98
+ page numbers, and a template/theme; `{{nyml kind: element}}` blocks insert
99
+ constructs like title sections, abstracts, letterheads, TOCs, CV headers, and exam
100
+ questions. orz-paged ships a **curated** subset of this model (the most common
101
+ templates/elements) with a clear agent skill — see
102
+ [DESIGN.md §8](./DESIGN.md). The full model orz-paged draws from is in
103
+ [docs/document-model.md](docs/document-model.md) (a spec reference from
104
+ orz-md-pdf).
105
+
106
+ ## Use with an AI agent
107
+
108
+ The package ships an **agent skill** that teaches an AI agent the document format —
109
+ page templates, headers/footers, the curated elements (CVs, letters, exams), and
110
+ the dynamic switch. The quickest way to produce a document is to let an agent do it:
111
+
112
+ - **Claude Code** — copy `orz-paged-skills/` into `~/.claude/skills/orz-paged/`.
113
+ - **Any agent** — point it at the local `orz-paged-skills/SKILL.md`, then describe
114
+ what you want.
115
+
116
+ Once orz-paged is published to npm, the skill will also be loadable from jsDelivr
117
+ (`https://cdn.jsdelivr.net/npm/orz-paged/orz-paged-skills/SKILL.md`), like the other
118
+ orz tools. More install routes: <https://markdown.orz.how/agents.html>
119
+
120
+ ## Security — treat these as programs, not documents
121
+
122
+ A `.paged.html` is **self-contained executable HTML**: opening one runs the
123
+ JavaScript embedded in it (the engine, the editor, and — because the parser allows
124
+ raw HTML in the source — potentially anything in the content). The trust model is
125
+ the same as **running a downloaded program**, not opening a PDF.
126
+
127
+ - **Only open or edit files from sources you trust.** Anyone can craft a file that
128
+ looks authentic (same chrome, logo, layout) but contains hostile code. The
129
+ format has no built-in authenticity — appearance proves nothing.
130
+ - **What the browser limits.** A page can't run native code or read your disk
131
+ silently (Save uses the File System Access prompt and is scoped to the file you
132
+ pick). Realistic harm from a hostile file is web-context — phishing, exfiltrating
133
+ what you type or paste, beaconing — within the browser sandbox.
134
+ - **The one-click update is opt-in and fixed-source.** It only checks for and
135
+ fetches a new framework after you enter edit mode and click **Update**, always
136
+ from the canonical jsDelivr packages over HTTPS (the source is hardcoded in the
137
+ engine — a tampered file cannot redirect it), and it shows the exact URLs for
138
+ confirmation first. Clicking Update places trust in npm + jsDelivr for those
139
+ packages.
140
+ - **Integrity can't be self-verified.** A file cannot prove its own integrity (a
141
+ forgery would just lie). If you need authenticity, verify it out-of-band — a
142
+ checksum or signature from the publisher over a trusted channel.
143
+
144
+ ## License
145
+
146
+ TBD.
package/assets/app.js ADDED
@@ -0,0 +1,413 @@
1
+ /* orz-paged in-file editor (A4). Plain browser JS, inlined into every
2
+ * .paged.html. The rendered pages are the live preview: editing re-assembles +
3
+ * re-paginates them (debounced). Save is self-reproducing — it serializes the
4
+ * whole document with the edited source baked back into #orz-src, stripping
5
+ * runtime-injected nodes ([data-orz-runtime]) and the rendered pages so the saved
6
+ * file re-renders cleanly on next open. Ported in spirit from orz-mdhtml/slides.
7
+ */
8
+ (function () {
9
+ 'use strict';
10
+ var CM_BASE = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16';
11
+ var cm = null;
12
+ var rerenderTimer = null;
13
+
14
+ function $(id) { return document.getElementById(id); }
15
+ function api() { return window.orzpaged; }
16
+ function srcEl() { return $('orz-src'); }
17
+ function currentSource() { return cm ? cm.getValue() : (srcEl() ? srcEl().textContent.replace(/^\n/, '').replace(/\n\s*$/, '') : ''); }
18
+
19
+ function toast(msg) {
20
+ var t = $('orz-toast'); if (!t) return;
21
+ t.textContent = msg; t.classList.add('show');
22
+ setTimeout(function () { t.classList.remove('show'); }, 1800);
23
+ }
24
+
25
+ /* ---- load CodeMirror on demand ---- */
26
+ function loadCss(href) {
27
+ if (document.querySelector('link[href="' + href + '"]')) return;
28
+ var l = document.createElement('link'); l.rel = 'stylesheet'; l.href = href;
29
+ l.setAttribute('data-orz-runtime', '1'); document.head.appendChild(l);
30
+ }
31
+ function loadJs(src) {
32
+ return new Promise(function (resolve, reject) {
33
+ if (document.querySelector('script[data-cm="' + src + '"]')) return resolve();
34
+ var s = document.createElement('script'); s.src = src; s.async = false;
35
+ s.setAttribute('data-cm', src); s.setAttribute('data-orz-runtime', '1');
36
+ s.onload = resolve; s.onerror = reject; document.head.appendChild(s);
37
+ });
38
+ }
39
+ function ensureCodeMirror() {
40
+ if (window.CodeMirror) return Promise.resolve();
41
+ loadCss(CM_BASE + '/codemirror.min.css');
42
+ return loadJs(CM_BASE + '/codemirror.min.js')
43
+ .then(function () { return loadJs(CM_BASE + '/mode/markdown/markdown.min.js'); });
44
+ }
45
+
46
+ function pagesEl() { return $('orz-pages'); }
47
+ function isEdit() { return document.documentElement.getAttribute('data-mode') === 'edit'; }
48
+
49
+ /* ---- preview zoom-to-fit (side-by-side pane is narrower than an A4 page) ---- */
50
+ function fitPreview() {
51
+ var pages = pagesEl(); if (!pages) return;
52
+ var wrap = pages.querySelector('.pagedjs_pages'); if (!wrap) return;
53
+ if (!isEdit()) { wrap.style.zoom = ''; return; }
54
+ wrap.style.zoom = ''; // measure natural page width
55
+ var page = pages.querySelector('.pagedjs_page');
56
+ var pageW = page ? page.getBoundingClientRect().width : 0;
57
+ if (!pageW) return;
58
+ wrap.style.zoom = Math.min(1, (pages.clientWidth - 28) / pageW);
59
+ }
60
+ window.__orzPagedAfterRender = function () { fitPreview(); buildDynControls(); }; // engine calls this after every render
61
+
62
+ /* ---- dynamic switch (live) — a control per key the document actually uses ---- */
63
+ function humanizeKey(k) { return String(k).replace(/_/g, ' '); }
64
+ function normKey(k) { return String(k).trim().toLowerCase().replace(/-/g, '_'); }
65
+
66
+ function buildDynControls() {
67
+ var host = $('orz-dyn'); if (!host) return;
68
+ if (!api() || !api().getDynamicState) { host.innerHTML = ''; host.removeAttribute('data-keys'); return; }
69
+ var st = api().getDynamicState();
70
+ var opts = st.options || {}, choices = st.choices || {};
71
+ // Only keys the CURRENT document actually uses (data-show-when / -hide-when),
72
+ // so the control disappears when you switch to a template that has none.
73
+ var keys = Object.keys(opts).sort();
74
+ if (!keys.length) { host.innerHTML = ''; host.removeAttribute('data-keys'); return; }
75
+ var sig = keys.join(',');
76
+ if (host.getAttribute('data-keys') === sig) { // same keys → just sync values
77
+ keys.forEach(function (k) {
78
+ var s = host.querySelector('select[data-key="' + k + '"]');
79
+ if (s) s.value = choices[k] || '';
80
+ });
81
+ return;
82
+ }
83
+ host.setAttribute('data-keys', sig);
84
+ host.innerHTML = '';
85
+ keys.forEach(function (k) {
86
+ var vals = (opts[k] || []).slice();
87
+ var cur = choices[k] || '';
88
+ if (cur && vals.indexOf(cur) < 0) vals.unshift(cur); // keep the current value selectable
89
+ var ctl = document.createElement('span'); ctl.className = 'orz-dyn-ctl';
90
+ var lab = document.createElement('label'); lab.textContent = humanizeKey(k); lab.htmlFor = 'orz-dyn-' + k;
91
+ var sel = document.createElement('select'); sel.id = 'orz-dyn-' + k; sel.setAttribute('data-key', k); sel.title = humanizeKey(k);
92
+ vals.forEach(function (v) { var o = document.createElement('option'); o.value = v; o.textContent = v; sel.appendChild(o); });
93
+ sel.value = cur;
94
+ // Write the choice back into the source's `dynamic_choices` block so the
95
+ // dropdown and the nyml stay in sync (and editing the nyml drives the dropdown).
96
+ sel.addEventListener('change', function () { applyDynChoiceToSource(k, sel.value); });
97
+ ctl.appendChild(lab); ctl.appendChild(sel); host.appendChild(ctl);
98
+ });
99
+ }
100
+
101
+ /** Set `key: value` inside the document block's `dynamic_choices:` map, matching
102
+ * the key by normalized form (so `answer-key` and `answer_key` are the same). */
103
+ function setSourceDynamicChoice(src, key, value) {
104
+ key = normKey(key);
105
+ var lines = src.split('\n');
106
+ var dcIdx = -1, dcIndent = 0;
107
+ for (var i = 0; i < lines.length; i++) {
108
+ var m = lines[i].match(/^(\s*)dynamic_choices\s*:/);
109
+ if (m) { dcIdx = i; dcIndent = m[1].length; break; }
110
+ }
111
+ if (dcIdx < 0) return null; // no block — nothing to sync
112
+ for (var j = dcIdx + 1; j < lines.length; j++) {
113
+ if (lines[j].trim() === '') continue;
114
+ var lm = lines[j].match(/^(\s*)([A-Za-z][\w-]*)\s*:\s*(.*)$/);
115
+ if (!lm || lm[1].length <= dcIndent) break; // dedented / not a key → end of block
116
+ if (normKey(lm[2]) === key) { lines[j] = lm[1] + lm[2] + ': ' + value; return lines.join('\n'); }
117
+ }
118
+ var indent = ''; for (var s = 0; s < dcIndent + 2; s++) indent += ' ';
119
+ lines.splice(dcIdx + 1, 0, indent + key.replace(/_/g, '-') + ': ' + value);
120
+ return lines.join('\n');
121
+ }
122
+
123
+ function applyDynChoiceToSource(key, value) {
124
+ var next = setSourceDynamicChoice(currentSource(), key, value);
125
+ if (next == null) return;
126
+ if (cm) cm.setValue(next);
127
+ else { var ta = $('orz-ta'); if (ta) ta.value = next; }
128
+ syncSource();
129
+ if (api() && api().refresh) api().refresh();
130
+ }
131
+
132
+ /* ---- draggable divider (relative width) ---- */
133
+ function wireDivider() {
134
+ var d = $('orz-divider'); if (!d || d.__wired) return; d.__wired = true;
135
+ var dragging = false;
136
+ d.addEventListener('mousedown', function (e) {
137
+ dragging = true; d.classList.add('dragging'); e.preventDefault();
138
+ document.body.style.userSelect = 'none';
139
+ });
140
+ document.addEventListener('mousemove', function (e) {
141
+ if (!dragging) return;
142
+ var pct = Math.max(20, Math.min(78, (e.clientX / window.innerWidth) * 100));
143
+ document.documentElement.style.setProperty('--orz-split', pct + '%');
144
+ });
145
+ document.addEventListener('mouseup', function () {
146
+ if (!dragging) return;
147
+ dragging = false; d.classList.remove('dragging'); document.body.style.userSelect = '';
148
+ if (cm) cm.refresh(); fitPreview();
149
+ });
150
+ }
151
+
152
+ /* ---- proportional editor <-> preview scroll sync (toggleable) ---- */
153
+ var syncLock = false;
154
+ var syncEnabled = true;
155
+ try { syncEnabled = localStorage.getItem('orz-paged-sync') !== '0'; } catch (e) {}
156
+ function setSync(on) {
157
+ syncEnabled = !!on;
158
+ try { localStorage.setItem('orz-paged-sync', syncEnabled ? '1' : '0'); } catch (e) {}
159
+ var b = $('orz-sync'); if (b) b.setAttribute('aria-pressed', syncEnabled ? 'true' : 'false');
160
+ }
161
+ function wireSync() {
162
+ if (!cm || cm.__sync) return; cm.__sync = true;
163
+ cm.on('scroll', function () {
164
+ if (!syncEnabled || syncLock || !isEdit()) return;
165
+ var info = cm.getScrollInfo(); var max = info.height - info.clientHeight;
166
+ if (max <= 0) return;
167
+ var pages = pagesEl(); var pmax = pages.scrollHeight - pages.clientHeight;
168
+ syncLock = true; pages.scrollTop = (info.top / max) * pmax;
169
+ setTimeout(function () { syncLock = false; }, 24);
170
+ });
171
+ pagesEl().addEventListener('scroll', function () {
172
+ if (!syncEnabled || syncLock || !isEdit()) return;
173
+ var pages = pagesEl(); var pmax = pages.scrollHeight - pages.clientHeight;
174
+ if (pmax <= 0) return;
175
+ var info = cm.getScrollInfo(); var max = info.height - info.clientHeight;
176
+ syncLock = true; cm.scrollTo(null, (pages.scrollTop / pmax) * max);
177
+ setTimeout(function () { syncLock = false; }, 24);
178
+ });
179
+ }
180
+
181
+ /* ---- edit mode ---- */
182
+ function enterEdit() {
183
+ document.documentElement.setAttribute('data-mode', 'edit');
184
+ checkVersion(); // edit view only — broad viewers never see the update banner
185
+ wireDivider();
186
+ // reflect the theme actually in effect (document theme or a prior override)
187
+ var th = $('orz-theme');
188
+ if (th && api() && api().getTheme) { var cur = api().getTheme(); if (cur) th.value = cur; }
189
+ ensureCodeMirror().then(function () {
190
+ if (!cm) {
191
+ var ta = $('orz-ta');
192
+ ta.value = currentSource();
193
+ cm = window.CodeMirror.fromTextArea(ta, {
194
+ mode: 'markdown', lineNumbers: true, lineWrapping: true, theme: 'default',
195
+ });
196
+ cm.on('change', scheduleRerender);
197
+ }
198
+ wireSync();
199
+ setTimeout(function () { if (cm) { cm.refresh(); cm.focus(); } fitPreview(); }, 30);
200
+ setTimeout(fitPreview, 260); // re-fit once the slide-in transition settles
201
+ }).catch(function () { toast('Could not load the editor (offline?)'); });
202
+ }
203
+ function exitEdit() {
204
+ document.documentElement.removeAttribute('data-mode');
205
+ fitPreview(); // clear zoom immediately
206
+ setTimeout(fitPreview, 260); // and again after the slide-out settles
207
+ }
208
+
209
+ function scheduleRerender() {
210
+ if (rerenderTimer) clearTimeout(rerenderTimer);
211
+ rerenderTimer = setTimeout(function () {
212
+ syncSource();
213
+ if (api() && api().refresh) api().refresh();
214
+ }, 500);
215
+ }
216
+
217
+ /** Keep #orz-src in sync with the editor (so refresh()/setTheme()/save use current text). */
218
+ function syncSource() {
219
+ var el = srcEl(); if (!el) return;
220
+ el.textContent = '\n' + currentSource() + '\n';
221
+ }
222
+
223
+ /* ---- self-reproducing save ---- */
224
+ function serialize() {
225
+ syncSource();
226
+ var clone = document.documentElement.cloneNode(true);
227
+ clone.removeAttribute('data-mode');
228
+ // strip runtime-injected nodes (fonts, libs, katex, CodeMirror assets)
229
+ var rt = clone.querySelectorAll('[data-orz-runtime]');
230
+ for (var i = 0; i < rt.length; i++) rt[i].parentNode.removeChild(rt[i]);
231
+ // clear the rendered pages (the engine re-renders on load)
232
+ var pages = clone.querySelector('#orz-pages'); if (pages) pages.innerHTML = '';
233
+ // never bake in the (edit-only) update banner so a viewer can't see it
234
+ var ub = clone.querySelector('#orz-update'); if (ub) { ub.classList.remove('show'); ub.removeAttribute('data-latest'); }
235
+ // reset the editor host to a bare textarea (drop CodeMirror DOM)
236
+ var host = clone.querySelector('#orz-editor-host');
237
+ if (host) host.innerHTML = '<textarea id="orz-ta" spellcheck="false"></textarea>';
238
+ // guard against a closing script tag inside the source breaking the file
239
+ var src = clone.querySelector('#orz-src');
240
+ if (src) src.textContent = src.textContent.replace(/<\/(script)/gi, '<\\/$1');
241
+ return '<!DOCTYPE html>\n' + clone.outerHTML;
242
+ }
243
+
244
+ function suggestedName() {
245
+ var t = (document.title || 'document').replace(/[^\w.-]+/g, '-');
246
+ return t.replace(/\.paged$/, '') + '.paged.html';
247
+ }
248
+
249
+ /** Write `html` to disk; resolves true if saved in place (FS-Access), false if downloaded. */
250
+ function persist(html) {
251
+ if (window.showSaveFilePicker) {
252
+ return window.showSaveFilePicker({
253
+ suggestedName: suggestedName(),
254
+ types: [{ description: 'paged HTML', accept: { 'text/html': ['.paged.html', '.html'] } }],
255
+ }).then(function (handle) {
256
+ return handle.createWritable().then(function (w) {
257
+ return w.write(html).then(function () { return w.close(); });
258
+ });
259
+ }).then(function () { return true; });
260
+ }
261
+ downloadFile(html);
262
+ return Promise.resolve(false);
263
+ }
264
+ function save() {
265
+ persist(serialize()).then(function (inPlace) { if (inPlace) toast('Saved'); })
266
+ .catch(function (e) { if (e && e.name !== 'AbortError') downloadFile(serialize()); });
267
+ }
268
+ function downloadFile(html) {
269
+ var blob = new Blob([html], { type: 'text/html' });
270
+ var a = document.createElement('a');
271
+ a.href = URL.createObjectURL(blob); a.download = suggestedName();
272
+ document.body.appendChild(a); a.click();
273
+ setTimeout(function () { URL.revokeObjectURL(a.href); a.remove(); }, 1000);
274
+ toast('Downloaded a copy');
275
+ }
276
+
277
+ /* ---- framework self-update (checked only in edit view) ---- */
278
+ // SECURITY: the update source is HARDCODED here, never read from the file's
279
+ // config — so a tampered/forged file cannot redirect "Update" to fetch
280
+ // attacker-controlled code. The host is fixed to jsDelivr over HTTPS, and the
281
+ // exact URLs are shown to the user for confirmation before anything is fetched.
282
+ // (This protects genuine files; a wholly-malicious file controls this code too —
283
+ // see the security note in the README. Clicking Update trusts npm + jsDelivr.)
284
+ var UPD = {
285
+ host: 'https://cdn.jsdelivr.net/npm/',
286
+ manifest: 'https://data.jsdelivr.com/v1/packages/npm/orz-paged-browser/resolved',
287
+ enginePkg: 'orz-paged-browser',
288
+ engineFile: 'orz-paged.browser.js',
289
+ appPkg: 'orz-paged'
290
+ };
291
+ function CFG() { return window.__ORZ_PAGED__ || {}; }
292
+ function isNewer(a, b) {
293
+ var pa = String(a).split('.'), pb = String(b).split('.');
294
+ for (var i = 0; i < 3; i++) { var x = parseInt(pa[i], 10) || 0, y = parseInt(pb[i], 10) || 0; if (x > y) return true; if (x < y) return false; }
295
+ return false;
296
+ }
297
+ var versionChecked = false;
298
+ function checkVersion() {
299
+ if (versionChecked) return; versionChecked = true;
300
+ var c = CFG(); if (!c.version) return;
301
+ fetch(UPD.manifest).then(function (r) { return r.json(); }).then(function (j) {
302
+ var latest = j && j.version;
303
+ if (latest && isNewer(latest, c.version)) showUpdate(latest);
304
+ }).catch(function () {});
305
+ }
306
+ function showUpdate(latest) {
307
+ var bar = $('orz-update'); if (!bar) return;
308
+ bar.querySelector('.upd-text').textContent =
309
+ 'Framework ' + latest + ' available (this file uses ' + CFG().version + ').';
310
+ bar.setAttribute('data-latest', latest);
311
+ bar.classList.add('show');
312
+ }
313
+ /** One-click update: re-fetch the engine bundle + app.js from the lockstep CDN
314
+ * at the latest version, re-inline them, bump the version, save, and reload. */
315
+ function applyUpdate() {
316
+ var bar = $('orz-update'); var latest = bar && bar.getAttribute('data-latest'); if (!latest) return;
317
+ var engineUrl = UPD.host + UPD.enginePkg + '@' + latest + '/' + UPD.engineFile;
318
+ var appUrl = UPD.host + UPD.appPkg + '@' + latest + '/assets/app.js';
319
+ if (!window.confirm('Update the framework to ' + latest + '?\n\nThis downloads and runs code from:\n ' + engineUrl + '\n ' + appUrl + '\n\nOnly proceed if you trust this document and its publisher.')) return;
320
+ toast('Downloading framework ' + latest + '…');
321
+ Promise.all([
322
+ fetch(engineUrl).then(function (r) { if (!r.ok) throw new Error('engine'); return r.text(); }),
323
+ fetch(appUrl).then(function (r) { if (!r.ok) throw new Error('app'); return r.text(); }),
324
+ ]).then(function (res) {
325
+ var es = document.querySelector('script[data-orz-asset="engine"]');
326
+ if (es) { if (es.getAttribute('src')) es.setAttribute('src', engineUrl); else es.textContent = res[0]; }
327
+ var as = document.querySelector('script[data-orz-asset="app"]');
328
+ if (as) as.textContent = res[1];
329
+ var cs = document.querySelector('script[data-orz-asset="config"]');
330
+ if (cs) { c.version = latest; cs.textContent = 'window.__ORZ_PAGED__ = ' + JSON.stringify(c) + ';'; }
331
+ bar.classList.remove('show');
332
+ return persist(serialize());
333
+ }).then(function (inPlace) {
334
+ if (inPlace === undefined) return;
335
+ if (inPlace) { toast('Updated to ' + latest + ' — reloading…'); setTimeout(function () { location.reload(); }, 700); }
336
+ else { toast('Updated copy downloaded — reopen it to use the new framework.'); }
337
+ }).catch(function () { toast('Update failed — check your connection.'); });
338
+ }
339
+
340
+ /* ---- template picker ---- */
341
+ function templates() { return (window.__ORZ_PAGED__ && window.__ORZ_PAGED__.templates) || []; }
342
+ function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
343
+ var TPL_ACCENT = { Article: '#2962a4', Report: '#2f6fe0', Exam: '#9a2820', Letter: '#2e7d32', CV: '#7a3f9a', Note: '#b7791f' };
344
+ function buildTemplateMenu() {
345
+ var menu = $('orz-template-menu'); if (!menu || menu.getAttribute('data-built')) return;
346
+ var list = templates(); if (!list.length) return;
347
+ var html = '', lastGroup = '';
348
+ for (var i = 0; i < list.length; i++) {
349
+ var t = list[i];
350
+ if (t.group !== lastGroup) { lastGroup = t.group; html += '<div class="tpl-group">' + esc(t.group) + '</div>'; }
351
+ html += '<button type="button" class="tpl" data-i="' + i + '" style="--tpl-accent:' + (TPL_ACCENT[t.group] || '#2f6fe0') + '">'
352
+ + '<span class="tpl-ic"></span>'
353
+ + '<span class="tpl-meta"><span class="tpl-label">' + esc(t.label) + '</span>'
354
+ + '<span class="tpl-desc">' + esc(t.description) + '</span></span></button>';
355
+ }
356
+ menu.innerHTML = html;
357
+ menu.addEventListener('click', function (e) {
358
+ var b = e.target.closest ? e.target.closest('.tpl') : null;
359
+ if (!b) return;
360
+ applyTemplate(list[+b.getAttribute('data-i')]);
361
+ menu.hidden = true;
362
+ });
363
+ menu.setAttribute('data-built', '1');
364
+ }
365
+ function toggleTemplateMenu(show) {
366
+ var menu = $('orz-template-menu'); if (!menu) return;
367
+ if (show === undefined) show = menu.hidden;
368
+ if (show) buildTemplateMenu();
369
+ menu.hidden = !show;
370
+ }
371
+ function applyTemplate(t) {
372
+ if (!t || !t.skeleton) return;
373
+ var cur = currentSource().trim();
374
+ var next = t.skeleton;
375
+ // Preserve the old content in a comment so it's kept but inert (nyml blocks
376
+ // inside a comment don't render). Neutralize any `-->` in it first, else the
377
+ // first one would close the wrapper early and leak the rest back in.
378
+ if (cur) next += '\n\n<!-- Previous content (kept below — edit or delete):\n\n'
379
+ + cur.replace(/--+>/g, '--​>') + '\n-->\n';
380
+ if (cm) { cm.setValue(next); cm.focus(); }
381
+ else { var ta = $('orz-ta'); if (ta) ta.value = next; syncSource(); if (api() && api().refresh) api().refresh(); }
382
+ toast('Started from "' + t.label + '"');
383
+ }
384
+
385
+ /* ---- wire up ---- */
386
+ function init() {
387
+ var fab = $('orz-edit-fab'); if (fab) fab.addEventListener('click', enterEdit);
388
+ var close = $('orz-close'); if (close) close.addEventListener('click', exitEdit);
389
+ var exp = $('orz-export'); if (exp) exp.addEventListener('click', function () { if (api()) api().exportPdf(); });
390
+ var sav = $('orz-save'); if (sav) sav.addEventListener('click', save);
391
+ var tpl = $('orz-template'); if (tpl) tpl.addEventListener('click', function (e) { e.stopPropagation(); toggleTemplateMenu(); });
392
+ document.addEventListener('click', function (e) {
393
+ var menu = $('orz-template-menu'), btn = $('orz-template');
394
+ if (menu && !menu.hidden && !menu.contains(e.target) && (!btn || !btn.contains(e.target))) menu.hidden = true;
395
+ });
396
+ var theme = $('orz-theme');
397
+ if (theme) theme.addEventListener('change', function () { if (api() && api().setTheme) api().setTheme(theme.value); });
398
+ var sync = $('orz-sync');
399
+ if (sync) { setSync(syncEnabled); sync.addEventListener('click', function () { setSync(!syncEnabled); }); }
400
+ var updApply = $('orz-upd-apply'); if (updApply) updApply.addEventListener('click', applyUpdate);
401
+ var updDismiss = $('orz-upd-dismiss');
402
+ if (updDismiss) updDismiss.addEventListener('click', function () { var u = $('orz-update'); if (u) u.classList.remove('show'); });
403
+ var rz; window.addEventListener('resize', function () {
404
+ if (!isEdit()) return; clearTimeout(rz);
405
+ rz = setTimeout(function () { if (cm) cm.refresh(); fitPreview(); }, 120);
406
+ });
407
+ document.addEventListener('keydown', function (e) {
408
+ if ((e.metaKey || e.ctrlKey) && (e.key === 's' || e.key === 'S')) { e.preventDefault(); save(); }
409
+ });
410
+ }
411
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
412
+ else init();
413
+ })();
@@ -0,0 +1,71 @@
1
+ {{nyml
2
+ kind: document
3
+ template: article-page
4
+ front_matter: clean
5
+ }}
6
+
7
+ {{nyml
8
+ kind: article-title
9
+ title: Your Article Title
10
+ subtitle: A Concise, Descriptive Subtitle
11
+ authors: |
12
+ First Author | 1,* | first.author@example.edu | 0000-0002-1825-0097
13
+ Second Author | 2 | second.author@example.org
14
+ Third Author | 1,2
15
+ affiliations: |
16
+ 1: Department of Example, University of Example
17
+ 2: Institute of Example, Example City
18
+ notes: |
19
+ *: Corresponding author
20
+ date: Month Year
21
+ placement: page
22
+ }}
23
+
24
+ {{nyml
25
+ kind: abstract
26
+ text: Briefly state the problem, what you did, and what you found. Summarize your main contribution in one or two sentences. Close with the significance of the result for the field.
27
+ keywords: keyword one, keyword two, keyword three
28
+ placement: page
29
+ }}
30
+
31
+ ## Introduction
32
+
33
+ Open with the context and motivation for your work. State the gap your study
34
+ addresses and the question you set out to answer. End the section with a short
35
+ statement of your contribution and an outline of what follows.
36
+
37
+ ## Methods
38
+
39
+ Describe your approach so that a reader could reproduce it. Summarize the key
40
+ parameters of your setup in a compact table:
41
+
42
+ | Parameter | Symbol | Value |
43
+ |---|---|---|
44
+ | Sample size | $n$ | 120 |
45
+ | Trials per group | $k$ | 5 |
46
+ | Significance level | $\alpha$ | 0.05 |
47
+
48
+ Quantities are reported with their uncertainties, and the mass–energy relation
49
+ $E = mc^2$ is used where relevant. The core model is given by
50
+
51
+ $$
52
+ \hat{y} = \beta_0 + \sum_{i=1}^{n} \beta_i x_i + \varepsilon
53
+ $$
54
+
55
+ where the residual term $\varepsilon$ is assumed normally distributed.
56
+
57
+ ## Results
58
+
59
+ - The primary measure improved by 18% over the baseline.
60
+ - The effect held across all three experimental groups.
61
+ - Results were statistically significant ($p < 0.05$).
62
+
63
+ ## Conclusion
64
+
65
+ Restate the main finding, note its limitations, and point to one direction for
66
+ future work.
67
+
68
+ ## References
69
+
70
+ 1. Author Name. *Title of the First Reference.* Journal Name, Year, pp. 1–10.
71
+ 2. Author Name. *Title of the Second Reference.* Publisher, Year.