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
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, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
|
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.
|