orz-paged 0.1.0 → 0.3.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 +22 -14
- package/assets/app.js +61 -1
- package/dist/browser-entry.d.ts +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +31 -68
- package/dist/doc/dynamic.d.ts +31 -0
- package/dist/doc/elements.d.ts +16 -0
- package/dist/doc/fonts.d.ts +25 -0
- package/dist/doc/nyml.d.ts +33 -0
- package/dist/doc/page-css.d.ts +15 -0
- package/dist/doc/settings.d.ts +35 -0
- package/dist/doc/templates.d.ts +49 -0
- package/dist/doc/theme-fonts.d.ts +12 -0
- package/dist/lib.d.ts +59 -0
- package/dist/lib.js +136 -0
- package/dist/orz-paged.browser.js +1 -1
- package/dist/paged.d.ts +9 -0
- package/dist/render-paged.d.ts +21 -0
- package/dist/template.d.ts +60 -0
- package/dist/types.d.ts +144 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# orz-paged
|
|
2
2
|
|
|
3
|
-
> **Status:
|
|
4
|
-
> the full document-element set, an in-file editor with a
|
|
5
|
-
> dynamic switch (e.g. exam answer keys).
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
> [DESIGN.md](./DESIGN.md) for the design.
|
|
3
|
+
> **Status: published (v0.1.0) and browser-verified** — 12 starter templates,
|
|
4
|
+
> 7 light themes, the full document-element set, an in-file editor with a
|
|
5
|
+
> template picker, and a dynamic switch (e.g. exam answer keys). Two packages
|
|
6
|
+
> publish in lockstep: [`orz-paged`](https://www.npmjs.com/package/orz-paged)
|
|
7
|
+
> and [`orz-paged-browser`](https://www.npmjs.com/package/orz-paged-browser).
|
|
8
|
+
> See [DESIGN.md](./DESIGN.md) for the design.
|
|
9
9
|
|
|
10
10
|
Generate a single, self-contained, **editable** `.paged.html` from Markdown — a
|
|
11
11
|
document shown as **print pages** in any browser (A4/Letter, real margins,
|
|
@@ -56,8 +56,7 @@ orz-paged --list-templates # list the templates
|
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
`--inline` (default) embeds the engine + paged.js + every theme. `--cdn` instead
|
|
59
|
-
references jsDelivr for the
|
|
60
|
-
published, so **while orz-paged is unpublished, use the default `--inline`**.
|
|
59
|
+
references jsDelivr for the published `orz-paged-browser` engine + theme.
|
|
61
60
|
**Fonts, web images, and math/diagram libraries always load from CDN** (internet
|
|
62
61
|
assumed) — so files stay small. The document's `{{nyml kind: document}}` block
|
|
63
62
|
(`template:`, `theme:`, …) is the source of truth.
|
|
@@ -109,13 +108,22 @@ The package ships an **agent skill** that teaches an AI agent the document forma
|
|
|
109
108
|
page templates, headers/footers, the curated elements (CVs, letters, exams), and
|
|
110
109
|
the dynamic switch. The quickest way to produce a document is to let an agent do it:
|
|
111
110
|
|
|
112
|
-
- **
|
|
113
|
-
|
|
114
|
-
what you want.
|
|
111
|
+
- **Any agent** — point it at
|
|
112
|
+
`https://cdn.jsdelivr.net/npm/orz-paged/orz-paged-skills/SKILL.md`, then
|
|
113
|
+
describe what you want.
|
|
114
|
+
- **Claude Code** — copy `orz-paged-skills/` into `~/.claude/skills/orz-paged/`
|
|
115
|
+
from this repo or from the installed npm package.
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
More install routes: <https://markdown.orz.how/agents.html>
|
|
118
|
+
|
|
119
|
+
## Host embedding
|
|
120
|
+
|
|
121
|
+
`.paged.html` files conform to **`orz-host-save@1`**: a platform can embed a
|
|
122
|
+
document in an iframe, announce itself with a `postMessage` handshake, and
|
|
123
|
+
receive saves (`{ source, html }`) instead of the file-system path —
|
|
124
|
+
standalone behavior, Export to PDF, and downloads are unchanged, and nothing
|
|
125
|
+
activates without the handshake. The canonical spec lives in the orz-mdhtml
|
|
126
|
+
repo: [PROTOCOL.md](https://github.com/wangyu16/orz-mdhtml/blob/main/PROTOCOL.md).
|
|
119
127
|
|
|
120
128
|
## Security — treat these as programs, not documents
|
|
121
129
|
|
package/assets/app.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
var CM_BASE = 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16';
|
|
11
11
|
var cm = null;
|
|
12
12
|
var rerenderTimer = null;
|
|
13
|
+
var dirty = false; // edit-state, surfaced to an embedding host (orz-host-dirty)
|
|
13
14
|
|
|
14
15
|
function $(id) { return document.getElementById(id); }
|
|
15
16
|
function api() { return window.orzpaged; }
|
|
@@ -123,6 +124,7 @@
|
|
|
123
124
|
function applyDynChoiceToSource(key, value) {
|
|
124
125
|
var next = setSourceDynamicChoice(currentSource(), key, value);
|
|
125
126
|
if (next == null) return;
|
|
127
|
+
setDirty(true);
|
|
126
128
|
if (cm) cm.setValue(next);
|
|
127
129
|
else { var ta = $('orz-ta'); if (ta) ta.value = next; }
|
|
128
130
|
syncSource();
|
|
@@ -207,6 +209,7 @@
|
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
function scheduleRerender() {
|
|
212
|
+
setDirty(true);
|
|
210
213
|
if (rerenderTimer) clearTimeout(rerenderTimer);
|
|
211
214
|
rerenderTimer = setTimeout(function () {
|
|
212
215
|
syncSource();
|
|
@@ -262,7 +265,10 @@
|
|
|
262
265
|
return Promise.resolve(false);
|
|
263
266
|
}
|
|
264
267
|
function save() {
|
|
265
|
-
|
|
268
|
+
// A hosting platform (verified handshake) receives the save instead of the
|
|
269
|
+
// file system; the host acknowledges with orz-host-saved (see PROTOCOL.md).
|
|
270
|
+
if (isHosted()) { hostSave(currentSource(), serialize()); return; }
|
|
271
|
+
persist(serialize()).then(function (inPlace) { if (inPlace) { setDirty(false); toast('Saved'); } })
|
|
266
272
|
.catch(function (e) { if (e && e.name !== 'AbortError') downloadFile(serialize()); });
|
|
267
273
|
}
|
|
268
274
|
function downloadFile(html) {
|
|
@@ -274,6 +280,59 @@
|
|
|
274
280
|
toast('Downloaded a copy');
|
|
275
281
|
}
|
|
276
282
|
|
|
283
|
+
/* ---- host embedding (orz-host-save@1) ----
|
|
284
|
+
* When a platform embeds this file in an iframe and announces the
|
|
285
|
+
* orz-host-save protocol (spec: PROTOCOL.md in the orz-mdhtml repo), Save
|
|
286
|
+
* posts the document to the host instead of touching the file system. Never
|
|
287
|
+
* enabled without the host's hello; protocol messages are accepted only from
|
|
288
|
+
* window.parent, and after the handshake only from the recorded host origin.
|
|
289
|
+
* Message content is read as data, never evaluated. Export/PDF keeps working. */
|
|
290
|
+
var HOST_PROTOCOL = 'orz-host-save';
|
|
291
|
+
var HOST_VERSION = 1;
|
|
292
|
+
var hostOrigin = null; // recorded at handshake; null = unhosted
|
|
293
|
+
var hostSaveTimer = null; // watchdog for a save awaiting acknowledgement
|
|
294
|
+
|
|
295
|
+
function isHosted() { return hostOrigin !== null; }
|
|
296
|
+
// An opaque embedder (sandboxed/srcdoc host) serializes as the string 'null',
|
|
297
|
+
// which postMessage rejects as a targetOrigin — fall back to '*' (the payload
|
|
298
|
+
// contains nothing the host doesn't already have).
|
|
299
|
+
function hostTarget() { return hostOrigin && hostOrigin !== 'null' ? hostOrigin : '*'; }
|
|
300
|
+
function hostPost(msg) { try { window.parent.postMessage(msg, hostTarget()); } catch (e) {} }
|
|
301
|
+
function setDirty(d) {
|
|
302
|
+
d = !!d;
|
|
303
|
+
if (dirty === d) return;
|
|
304
|
+
dirty = d;
|
|
305
|
+
if (isHosted()) hostPost({ type: 'orz-host-dirty', protocol: HOST_PROTOCOL, version: HOST_VERSION, dirty: d });
|
|
306
|
+
}
|
|
307
|
+
function hostSave(src, html) {
|
|
308
|
+
if (hostSaveTimer) return; // one save in flight at a time
|
|
309
|
+
hostSaveTimer = setTimeout(function () {
|
|
310
|
+
hostSaveTimer = null;
|
|
311
|
+
toast('Save failed — no response from the host'); // document intact, still dirty
|
|
312
|
+
}, 10000);
|
|
313
|
+
hostPost({ type: 'orz-host-save', protocol: HOST_PROTOCOL, version: HOST_VERSION, source: src, html: html });
|
|
314
|
+
}
|
|
315
|
+
function onHostMessage(event) {
|
|
316
|
+
// only the embedding parent may speak the protocol
|
|
317
|
+
if (window.parent === window || event.source !== window.parent) return;
|
|
318
|
+
var d = event.data;
|
|
319
|
+
if (!d || typeof d !== 'object') return;
|
|
320
|
+
// after the handshake, hold the parent to the origin it introduced itself with
|
|
321
|
+
if (isHosted() && hostOrigin !== 'null' && event.origin !== hostOrigin) return;
|
|
322
|
+
if (d.type === 'orz-host-hello' && d.protocol === HOST_PROTOCOL && typeof d.version === 'number' && d.version >= 1) {
|
|
323
|
+
hostOrigin = event.origin;
|
|
324
|
+
// reply with the highest version we support ≤ the host's (we speak only 1)
|
|
325
|
+
hostPost({ type: 'orz-host-ready', protocol: HOST_PROTOCOL, version: HOST_VERSION, kind: 'paged' });
|
|
326
|
+
if (dirty) hostPost({ type: 'orz-host-dirty', protocol: HOST_PROTOCOL, version: HOST_VERSION, dirty: true });
|
|
327
|
+
} else if (d.type === 'orz-host-saved' && hostSaveTimer) {
|
|
328
|
+
clearTimeout(hostSaveTimer); hostSaveTimer = null;
|
|
329
|
+
if (d.ok) { setDirty(false); toast('Saved'); }
|
|
330
|
+
else { toast('Save failed' + (d.error ? ' — ' + String(d.error) : '')); }
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// listen from script load so an early hello isn't missed
|
|
334
|
+
window.addEventListener('message', onHostMessage);
|
|
335
|
+
|
|
277
336
|
/* ---- framework self-update (checked only in edit view) ---- */
|
|
278
337
|
// SECURITY: the update source is HARDCODED here, never read from the file's
|
|
279
338
|
// config — so a tampered/forged file cannot redirect "Update" to fetch
|
|
@@ -377,6 +436,7 @@
|
|
|
377
436
|
// first one would close the wrapper early and leak the rest back in.
|
|
378
437
|
if (cur) next += '\n\n<!-- Previous content (kept below — edit or delete):\n\n'
|
|
379
438
|
+ cur.replace(/--+>/g, '-->') + '\n-->\n';
|
|
439
|
+
setDirty(true);
|
|
380
440
|
if (cm) { cm.setValue(next); cm.focus(); }
|
|
381
441
|
else { var ta = $('orz-ta'); if (ta) ta.value = next; syncSource(); if (api() && api().refresh) api().refresh(); }
|
|
382
442
|
toast('Started from "' + t.label + '"');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
CHANGED
|
@@ -8,57 +8,11 @@
|
|
|
8
8
|
* references the published `orz-paged-browser` on jsDelivr. Fonts, images, and the
|
|
9
9
|
* math/diagram libs always load from CDN at view time (DESIGN §4).
|
|
10
10
|
*/
|
|
11
|
-
import { readFileSync, writeFileSync
|
|
11
|
+
import { readFileSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { dirname, join, basename, resolve } from 'node:path';
|
|
13
|
-
import { fileURLToPath } from 'node:url';
|
|
14
13
|
import { getBrowserRuntimeScript } from 'orz-markdown/runtime';
|
|
15
|
-
import { PREVIEW_CDN } from 'orz-markdown/preview-frame';
|
|
16
14
|
import { buildHtml } from './template.js';
|
|
17
|
-
import {
|
|
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
|
-
}
|
|
15
|
+
import { selfVersion, CDN, findAsset, loadThemes, loadTemplates, pickDefaultTheme, composeInlineHtml, } from './lib.js';
|
|
62
16
|
function parseArgs(argv) {
|
|
63
17
|
const a = { mode: 'inline' };
|
|
64
18
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -138,30 +92,39 @@ function main() {
|
|
|
138
92
|
const inDir = args.input ? dirname(resolve(args.input)) : process.cwd();
|
|
139
93
|
const outPath = args.out ? resolve(args.out) : join(inDir, base + '.paged.html');
|
|
140
94
|
const title = args.title || base;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const runtime = getBrowserRuntimeScript(); // copy-as-Markdown (+ qr) from orz-markdown
|
|
144
|
-
let renderer;
|
|
95
|
+
let html;
|
|
96
|
+
let themeCount;
|
|
145
97
|
if (args.mode === 'cdn') {
|
|
146
|
-
|
|
98
|
+
// CDN mode references the published engine on jsDelivr (not inlined), so it
|
|
99
|
+
// has its own composition; the default inline path shares composeInlineHtml.
|
|
100
|
+
const { baseCss, themes } = loadThemes();
|
|
101
|
+
const appJs = readFileSync(findAsset('assets/app.js'), 'utf8');
|
|
102
|
+
const runtime = getBrowserRuntimeScript(); // copy-as-Markdown (+ qr) from orz-markdown
|
|
103
|
+
const renderer = {
|
|
104
|
+
mode: 'cdn',
|
|
105
|
+
src: `https://cdn.jsdelivr.net/npm/orz-paged-browser@${selfVersion}/orz-paged.browser.js`,
|
|
106
|
+
};
|
|
107
|
+
themeCount = themes.length;
|
|
108
|
+
html = buildHtml({
|
|
109
|
+
source,
|
|
110
|
+
title,
|
|
111
|
+
rendererVersion: selfVersion,
|
|
112
|
+
renderer,
|
|
113
|
+
baseCss,
|
|
114
|
+
themes,
|
|
115
|
+
defaultTheme: pickDefaultTheme(themes),
|
|
116
|
+
templates,
|
|
117
|
+
cdn: CDN,
|
|
118
|
+
appJs,
|
|
119
|
+
runtime,
|
|
120
|
+
});
|
|
147
121
|
}
|
|
148
122
|
else {
|
|
149
|
-
|
|
123
|
+
// Default: fully-inline, via the shared composition used by the library.
|
|
124
|
+
themeCount = loadThemes().themes.length;
|
|
125
|
+
html = composeInlineHtml(source, title);
|
|
150
126
|
}
|
|
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
127
|
writeFileSync(outPath, html, 'utf8');
|
|
165
|
-
console.log(`Wrote ${outPath} (${args.mode}, ${
|
|
128
|
+
console.log(`Wrote ${outPath} (${args.mode}, ${themeCount} themes)`);
|
|
166
129
|
}
|
|
167
130
|
main();
|
|
@@ -0,0 +1,31 @@
|
|
|
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 declare function normalizeChoiceKey(key: string): string;
|
|
12
|
+
interface CondEl {
|
|
13
|
+
getAttribute(name: string): string | null;
|
|
14
|
+
removeAttribute(name: string): void;
|
|
15
|
+
remove(): void;
|
|
16
|
+
}
|
|
17
|
+
interface CondRoot {
|
|
18
|
+
querySelectorAll(sel: string): ArrayLike<CondEl>;
|
|
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 declare function applyDynamicChoices(root: CondRoot, choices: Record<string, string>): void;
|
|
26
|
+
/**
|
|
27
|
+
* Collect the dynamic keys present under `root` and the set of values each is
|
|
28
|
+
* used with — for building a live switcher UI. Returns key → sorted values.
|
|
29
|
+
*/
|
|
30
|
+
export declare function collectDynamicChoices(root: CondRoot): Record<string, string[]>;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* orz-paged — built-in document elements.
|
|
3
|
+
*
|
|
4
|
+
* `renderElement(spec, ctx)` turns one `{{nyml kind: element}}` block into
|
|
5
|
+
* semantic HTML + a small scoped stylesheet. Every element is wrapped in
|
|
6
|
+
* `<section class="orz-element orz-el-<kind>">…</section>`. Markdown-typed
|
|
7
|
+
* fields go through the injected `ctx.renderInline` / `ctx.renderBlock` so this
|
|
8
|
+
* module never depends on orz-markdown directly.
|
|
9
|
+
*
|
|
10
|
+
* The `css` returned per kind is identical on every call (the assembler dedupes
|
|
11
|
+
* by kind), uses `var(--accent)` for the accent color, and stays light-only.
|
|
12
|
+
*
|
|
13
|
+
* Spec: docs/document-model.md (Built-in Elements) — reimplemented cleanly.
|
|
14
|
+
*/
|
|
15
|
+
import type { ElementSpec, ElementCtx, ElementResult } from '../types.js';
|
|
16
|
+
export declare function renderElement(spec: ElementSpec, ctx: ElementCtx): ElementResult;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Font presets — M3.
|
|
3
|
+
*
|
|
4
|
+
* Maps each curated {@link FontPresetName} to a {@link FontPreset}: a CDN
|
|
5
|
+
* stylesheet URL (`@fontsource/*` served via jsDelivr) plus the matching CSS
|
|
6
|
+
* `font-family` stack. orz-paged assumes internet access, so fonts load from
|
|
7
|
+
* the CDN rather than being bundled.
|
|
8
|
+
*
|
|
9
|
+
* `system-serif` is the one preset with **no** webfont — it relies on the
|
|
10
|
+
* platform's Times-style serif and ships an empty `cssUrl`.
|
|
11
|
+
*
|
|
12
|
+
* Source of truth for the curated list: `docs/document-model.md` (Tier A) and
|
|
13
|
+
* `src/types.ts` (`FontPresetName`).
|
|
14
|
+
*/
|
|
15
|
+
import type { FontPreset, FontPresetName } from '../types.js';
|
|
16
|
+
/**
|
|
17
|
+
* Every curated preset → its CDN stylesheet + CSS family stack. `system-serif`
|
|
18
|
+
* carries an empty `cssUrl` (no network font).
|
|
19
|
+
*/
|
|
20
|
+
export declare const FONT_PRESETS: Record<FontPresetName, FontPreset>;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a preset by name. Unknown names fall back to a webfont-less system
|
|
23
|
+
* serif (empty `cssUrl`) rather than throwing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function fontPreset(name: string): FontPreset;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic nyml-block scanner — shared by the settings reader (M1) and the
|
|
3
|
+
* assembler (A1).
|
|
4
|
+
*
|
|
5
|
+
* `settings.ts` parses the single `kind: document` block inline; this module is
|
|
6
|
+
* the reusable version: it finds **every** `{{nyml … }}` block in a source,
|
|
7
|
+
* parses its YAML-ish `key: value` body (including `key: |` multiline blocks),
|
|
8
|
+
* reads the `kind:` discriminator, and reports the block's char offsets so a
|
|
9
|
+
* caller can splice it out / replace it.
|
|
10
|
+
*
|
|
11
|
+
* Values are always strings: surrounding quotes are stripped, multiline `|`
|
|
12
|
+
* blocks are dedented and joined with `\n`. The `{{nyml … }}` grammar matches
|
|
13
|
+
* what `parseDocSettings` already accepts (`}` is allowed inside the body; only
|
|
14
|
+
* the first `}}` terminates the block).
|
|
15
|
+
*/
|
|
16
|
+
/** One scanned `{{nyml … }}` block: its kind, parsed fields, and char range. */
|
|
17
|
+
export interface NymlBlock {
|
|
18
|
+
/** The `kind:` value (e.g. `'document'`, `'element'`), or `''` if absent. */
|
|
19
|
+
kind: string;
|
|
20
|
+
/** snake_case field → raw string value (multiline values preserved). */
|
|
21
|
+
fields: Record<string, string>;
|
|
22
|
+
/** Offset of the opening `{` of `{{nyml` in `source`. */
|
|
23
|
+
start: number;
|
|
24
|
+
/** Offset just past the closing `}}` (so `source.slice(start, end)` is the block). */
|
|
25
|
+
end: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Scan a source for every `{{nyml … }}` block, in document order. Each result
|
|
29
|
+
* carries its parsed `kind`, `fields`, and `[start, end)` char offsets. Blocks
|
|
30
|
+
* inside an HTML comment (`<!-- … -->`) are skipped — so a commented-out element
|
|
31
|
+
* (e.g. content the editor's template picker preserves) does not render.
|
|
32
|
+
*/
|
|
33
|
+
export declare function scanNymlBlocks(source: string): NymlBlock[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page CSS — M5.
|
|
3
|
+
*
|
|
4
|
+
* Turns a fully-resolved {@link DocSettings} into the `@page` rules, margin-box
|
|
5
|
+
* running headers/footers, page-number counters, `:root` design tokens, and
|
|
6
|
+
* layout-behavior CSS that paged.js realizes into physical page boxes.
|
|
7
|
+
*
|
|
8
|
+
* Behavior SPEC: `docs/document-model.md`; token set + page model:
|
|
9
|
+
* `docs/dom-contract.md`. Reimplemented cleanly in TS (light-only output).
|
|
10
|
+
*/
|
|
11
|
+
import type { DocSettings } from '../types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Build the full page CSS string for a document. Pure: depends only on `s`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildPageCss(s: DocSettings): string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Document settings — M1.
|
|
3
|
+
*
|
|
4
|
+
* Reads the single `{{nyml kind: document}}` block from a Markdown source,
|
|
5
|
+
* normalizes its snake_case keys into a {@link DocSettingsLayer}, and merges
|
|
6
|
+
* template + user layers over {@link DEFAULTS} into a fully-resolved
|
|
7
|
+
* {@link DocSettings}.
|
|
8
|
+
*
|
|
9
|
+
* Source of truth for defaults and aliases: `docs/document-model.md`
|
|
10
|
+
* (the curated subset; see DESIGN.md §8).
|
|
11
|
+
*/
|
|
12
|
+
import type { DocSettings, RawDocSettings, DocSettingsLayer } from '../types.js';
|
|
13
|
+
/** Every field at its documented default (docs/document-model.md). */
|
|
14
|
+
export declare const DEFAULTS: DocSettings;
|
|
15
|
+
/**
|
|
16
|
+
* Locate the first `{{nyml ... }}` block whose body declares `kind: document`,
|
|
17
|
+
* parse its `key: value` lines (including `key: |` multiline blocks) into a
|
|
18
|
+
* {@link RawDocSettings}, and return the source with that block removed so it
|
|
19
|
+
* is not rendered as content. No document block → `{ raw: {}, body: source }`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseDocSettings(source: string): {
|
|
22
|
+
raw: RawDocSettings;
|
|
23
|
+
body: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Map snake_case raw keys → camelCase {@link DocSettings} fields, coercing
|
|
27
|
+
* types and normalizing aliases. Only keys present in `raw` are included;
|
|
28
|
+
* unknown keys are ignored.
|
|
29
|
+
*/
|
|
30
|
+
export declare function normalizeRawToLayer(raw: RawDocSettings): DocSettingsLayer;
|
|
31
|
+
/**
|
|
32
|
+
* Shallow-merge layers left → right (later wins). Intended use:
|
|
33
|
+
* `mergeSettings(DEFAULTS, templateLayer, userLayer)`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function mergeSettings(...layers: DocSettingsLayer[]): DocSettings;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated document templates — LAYOUT defaults + a starter skeleton.
|
|
3
|
+
*
|
|
4
|
+
* Responsibility split: a **template** owns layout only (page size, margins,
|
|
5
|
+
* furniture, and which elements appear / where). A **theme** owns the look —
|
|
6
|
+
* font, accent/decoration color, and element styling — so the same theme renders
|
|
7
|
+
* the same across every template (see ../doc/theme-fonts.ts + assets/themes/*).
|
|
8
|
+
* The default theme is `light-academic-1` (settings.ts DEFAULTS.theme).
|
|
9
|
+
*
|
|
10
|
+
* Each template carries:
|
|
11
|
+
* - a {@link DocSettingsLayer} of *layout defaults* (page size; no font/theme),
|
|
12
|
+
* merged under any `kind: document` field the user sets,
|
|
13
|
+
* - picker metadata (`label`, `description`, `group`),
|
|
14
|
+
* - a `skeleton` stem naming the starter Markdown in `assets/templates/<stem>.md`.
|
|
15
|
+
*
|
|
16
|
+
* `article` / `report` / `exam` ship in two variants — `-page` (a dedicated
|
|
17
|
+
* title/cover page) and `-section` (title-on-content) — that differ only in their
|
|
18
|
+
* skeleton (the title element's `placement: page | section`). Legacy names
|
|
19
|
+
* (`article`/`report`/`exam`) resolve to the `-section` variant.
|
|
20
|
+
*/
|
|
21
|
+
import type { TemplateName, DocSettingsLayer } from '../types.js';
|
|
22
|
+
export interface TemplateDef {
|
|
23
|
+
/** Friendly name for the picker, e.g. "Article — title page". */
|
|
24
|
+
label: string;
|
|
25
|
+
/** One-line description (picker tooltip). */
|
|
26
|
+
description: string;
|
|
27
|
+
/** Grouping for the picker, e.g. "Article". */
|
|
28
|
+
group: string;
|
|
29
|
+
/** Starter-markdown file stem: `assets/templates/<skeleton>.md`. */
|
|
30
|
+
skeleton: string;
|
|
31
|
+
/** Settings defaults this template applies. */
|
|
32
|
+
settings: DocSettingsLayer;
|
|
33
|
+
}
|
|
34
|
+
/** The 10 curated templates. */
|
|
35
|
+
export declare const TEMPLATES: Record<TemplateName, TemplateDef>;
|
|
36
|
+
/** Canonicalize a (possibly legacy) template name, or `undefined` if unknown. */
|
|
37
|
+
export declare function canonicalTemplate(name: string): TemplateName | undefined;
|
|
38
|
+
/** Return a template's *settings* defaults layer, or `{}` for an unknown name. */
|
|
39
|
+
export declare function resolveTemplate(name: TemplateName | string): DocSettingsLayer;
|
|
40
|
+
/** The starter-markdown file stem for a template, or `undefined` if unknown. */
|
|
41
|
+
export declare function templateSkeletonName(name: TemplateName | string): string | undefined;
|
|
42
|
+
/** Picker catalog: id + metadata for every template (no settings/skeleton text). */
|
|
43
|
+
export declare function templateList(): Array<{
|
|
44
|
+
id: TemplateName;
|
|
45
|
+
label: string;
|
|
46
|
+
description: string;
|
|
47
|
+
group: string;
|
|
48
|
+
skeleton: string;
|
|
49
|
+
}>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Theme typography — each theme OWNS its font (alongside its colors + element
|
|
3
|
+
* style in the theme CSS). The engine loads the active theme's webfonts at view
|
|
4
|
+
* time and the theme CSS sets the matching `--font-body` / `--font-heading`, so
|
|
5
|
+
* the same theme yields the same font across every template. An explicit
|
|
6
|
+
* `font_preset` in the document settings still overrides this (page CSS wins).
|
|
7
|
+
*/
|
|
8
|
+
import type { ThemeName, FontPresetName } from '../types.js';
|
|
9
|
+
export declare const THEME_FONTS: Record<ThemeName, {
|
|
10
|
+
body: FontPresetName;
|
|
11
|
+
heading: FontPresetName;
|
|
12
|
+
}>;
|
package/dist/lib.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type ThemeAsset, type TemplateAsset } from './template.js';
|
|
2
|
+
export declare const selfVersion: string;
|
|
3
|
+
/** Pinned CDN assets used at view time — sourced from orz-markdown's shared
|
|
4
|
+
* preview-frame helper so every host app loads identical, tested versions.
|
|
5
|
+
* Paged is light-only, so highlight.js uses the light (github) stylesheet. */
|
|
6
|
+
export declare const CDN: {
|
|
7
|
+
katexCss: "https://cdn.jsdelivr.net/npm/katex@0.16.35/dist/katex.min.css";
|
|
8
|
+
mermaidJs: "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js";
|
|
9
|
+
smilesJs: "https://unpkg.com/smiles-drawer@1.0.10/dist/smiles-drawer.min.js";
|
|
10
|
+
chartJs: "https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js";
|
|
11
|
+
hljsJs: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js";
|
|
12
|
+
hljsCss: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css";
|
|
13
|
+
};
|
|
14
|
+
export declare function findAsset(rel: string): string;
|
|
15
|
+
/** Drop a theme's `@import url("./base.css")` — base is inlined separately, so the
|
|
16
|
+
* relative import points nowhere (404 on http; a fetch hazard on file://). */
|
|
17
|
+
export declare function stripBaseImport(css: string): string;
|
|
18
|
+
export declare function loadThemes(): {
|
|
19
|
+
baseCss: string;
|
|
20
|
+
themes: ThemeAsset[];
|
|
21
|
+
};
|
|
22
|
+
/** Load every template's starter skeleton from `assets/templates/<stem>.md`. */
|
|
23
|
+
export declare function loadTemplates(): TemplateAsset[];
|
|
24
|
+
/** The default theme id, matching the CLI: prefer `light-neat-1`, else the first
|
|
25
|
+
* loaded theme, else `'none'`. */
|
|
26
|
+
export declare function pickDefaultTheme(themes: ThemeAsset[]): string;
|
|
27
|
+
/**
|
|
28
|
+
* The shared inline composition — called by both the CLI (`--inline`, the
|
|
29
|
+
* default) and {@link buildPagedHtml}. Loads all assets from the package, builds
|
|
30
|
+
* an inline renderer, and returns the full document string. Deterministic: paged
|
|
31
|
+
* has no per-document random id, so the same `(source, title, theme?)` always
|
|
32
|
+
* yields byte-identical output.
|
|
33
|
+
*
|
|
34
|
+
* @param theme optional theme id; when omitted (or unknown) the default theme
|
|
35
|
+
* ({@link pickDefaultTheme}) drives the picker's initial selection.
|
|
36
|
+
*/
|
|
37
|
+
export declare function composeInlineHtml(source: string, title: string, theme?: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Generate a self-contained `.paged.html` document, fully inline.
|
|
40
|
+
*
|
|
41
|
+
* Mirrors the CLI's default `--inline` mode; the returned string is
|
|
42
|
+
* byte-identical to CLI inline output for the same source/title/theme.
|
|
43
|
+
*
|
|
44
|
+
* @param opts.markdown the document source (the host passes content).
|
|
45
|
+
* @param opts.title `<title>` + generator title; defaults to `'Untitled'`.
|
|
46
|
+
* @param opts.theme initial theme id; unknown/omitted → the default theme.
|
|
47
|
+
* @param opts.template a starter-skeleton selector, mirroring the CLI's
|
|
48
|
+
* `--template <name>` with no input file: used as the source ONLY when
|
|
49
|
+
* `opts.markdown` is empty/whitespace. When `markdown` has content, it wins.
|
|
50
|
+
* The full template catalog is always embedded regardless (it powers the
|
|
51
|
+
* in-editor picker), so this never changes the catalog — only the initial
|
|
52
|
+
* source. An unknown template name is ignored (falls back to `markdown`).
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildPagedHtml(opts: {
|
|
55
|
+
markdown: string;
|
|
56
|
+
title?: string;
|
|
57
|
+
theme?: string;
|
|
58
|
+
template?: string;
|
|
59
|
+
}): string;
|