orz-paged 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -0
- package/assets/app.js +413 -0
- package/assets/templates/article-page.md +71 -0
- package/assets/templates/article-section.md +83 -0
- package/assets/templates/cover-letter.md +53 -0
- package/assets/templates/cv-elegant.md +93 -0
- package/assets/templates/cv-modern.md +86 -0
- package/assets/templates/cv.md +45 -0
- package/assets/templates/exam-page.md +76 -0
- package/assets/templates/exam-section.md +59 -0
- package/assets/templates/letter.md +47 -0
- package/assets/templates/note.md +28 -0
- package/assets/templates/report-page.md +72 -0
- package/assets/templates/report-section.md +80 -0
- package/assets/themes/base.css +538 -0
- package/assets/themes/beige-decent-1.css +103 -0
- package/assets/themes/beige-decent-2.css +92 -0
- package/assets/themes/light-academic-1.css +114 -0
- package/assets/themes/light-academic-2.css +99 -0
- package/assets/themes/light-neat-1.css +90 -0
- package/assets/themes/light-neat-2.css +101 -0
- package/assets/themes/light-neat-3.css +91 -0
- package/dist/browser-entry.js +302 -0
- package/dist/cli.js +167 -0
- package/dist/doc/dynamic.js +57 -0
- package/dist/doc/elements.js +813 -0
- package/dist/doc/fonts.js +83 -0
- package/dist/doc/nyml.js +140 -0
- package/dist/doc/page-css.js +230 -0
- package/dist/doc/settings.js +390 -0
- package/dist/doc/templates.js +147 -0
- package/dist/doc/theme-fonts.js +10 -0
- package/dist/orz-paged.browser.js +1524 -0
- package/dist/paged.js +31 -0
- package/dist/render-paged.js +123 -0
- package/dist/template.js +242 -0
- package/dist/types.js +11 -0
- package/orz-paged-skills/SKILL.md +547 -0
- package/package.json +51 -0
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
/** jsDelivr `@fontsource` stylesheet for a given package id (major v5). */
|
|
16
|
+
function fontsourceUrl(id) {
|
|
17
|
+
return `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/index.css`;
|
|
18
|
+
}
|
|
19
|
+
/** Times-style fallback stack for the webfont-less `system-serif` preset. */
|
|
20
|
+
const SYSTEM_SERIF_FAMILY = '"Times New Roman", Times, Georgia, "Liberation Serif", serif';
|
|
21
|
+
/**
|
|
22
|
+
* Every curated preset → its CDN stylesheet + CSS family stack. `system-serif`
|
|
23
|
+
* carries an empty `cssUrl` (no network font).
|
|
24
|
+
*/
|
|
25
|
+
export const FONT_PRESETS = {
|
|
26
|
+
'system-serif': {
|
|
27
|
+
cssUrl: '',
|
|
28
|
+
family: SYSTEM_SERIF_FAMILY,
|
|
29
|
+
},
|
|
30
|
+
'source-serif-4': {
|
|
31
|
+
cssUrl: fontsourceUrl('source-serif-4'),
|
|
32
|
+
family: '"Source Serif 4", Georgia, "Times New Roman", serif',
|
|
33
|
+
},
|
|
34
|
+
lora: {
|
|
35
|
+
cssUrl: fontsourceUrl('lora'),
|
|
36
|
+
family: '"Lora", Georgia, "Times New Roman", serif',
|
|
37
|
+
},
|
|
38
|
+
'crimson-pro': {
|
|
39
|
+
cssUrl: fontsourceUrl('crimson-pro'),
|
|
40
|
+
family: '"Crimson Pro", Georgia, "Times New Roman", serif',
|
|
41
|
+
},
|
|
42
|
+
'noto-serif': {
|
|
43
|
+
cssUrl: fontsourceUrl('noto-serif'),
|
|
44
|
+
family: '"Noto Serif", Georgia, "Times New Roman", serif',
|
|
45
|
+
},
|
|
46
|
+
inter: {
|
|
47
|
+
cssUrl: fontsourceUrl('inter'),
|
|
48
|
+
family: '"Inter", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
49
|
+
},
|
|
50
|
+
'ibm-plex-sans': {
|
|
51
|
+
cssUrl: fontsourceUrl('ibm-plex-sans'),
|
|
52
|
+
family: '"IBM Plex Sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
53
|
+
},
|
|
54
|
+
roboto: {
|
|
55
|
+
cssUrl: fontsourceUrl('roboto'),
|
|
56
|
+
family: '"Roboto", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
57
|
+
},
|
|
58
|
+
raleway: {
|
|
59
|
+
cssUrl: fontsourceUrl('raleway'),
|
|
60
|
+
family: '"Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
61
|
+
},
|
|
62
|
+
'noto-sans': {
|
|
63
|
+
cssUrl: fontsourceUrl('noto-sans'),
|
|
64
|
+
family: '"Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif',
|
|
65
|
+
},
|
|
66
|
+
'courier-prime': {
|
|
67
|
+
cssUrl: fontsourceUrl('courier-prime'),
|
|
68
|
+
family: '"Courier Prime", "Courier New", Courier, monospace',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
/** Sensible fallback for an unknown preset name: the webfont-less system serif. */
|
|
72
|
+
const SYSTEM_FALLBACK = {
|
|
73
|
+
cssUrl: '',
|
|
74
|
+
family: SYSTEM_SERIF_FAMILY,
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Resolve a preset by name. Unknown names fall back to a webfont-less system
|
|
78
|
+
* serif (empty `cssUrl`) rather than throwing.
|
|
79
|
+
*/
|
|
80
|
+
export function fontPreset(name) {
|
|
81
|
+
const preset = FONT_PRESETS[name];
|
|
82
|
+
return preset ?? SYSTEM_FALLBACK;
|
|
83
|
+
}
|
package/dist/doc/nyml.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
const OPEN = '{{nyml';
|
|
17
|
+
/** Find the index of the `}}` that closes a `{{nyml` opened at/after `from`. */
|
|
18
|
+
function findClosing(source, from) {
|
|
19
|
+
for (let i = from; i < source.length - 1; i++) {
|
|
20
|
+
if (source[i] === '}' && source[i + 1] === '}')
|
|
21
|
+
return i;
|
|
22
|
+
}
|
|
23
|
+
return -1;
|
|
24
|
+
}
|
|
25
|
+
/** Strip a single pair of matching surrounding quotes, if present. */
|
|
26
|
+
function stripQuotes(s) {
|
|
27
|
+
if (s.length >= 2) {
|
|
28
|
+
const first = s[0];
|
|
29
|
+
const last = s[s.length - 1];
|
|
30
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
31
|
+
return s.slice(1, -1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return s;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parse a nyml body into a flat `key: value` map plus its `kind`. Supports
|
|
38
|
+
* `key: |` multiline blocks: subsequent lines indented more than the key are
|
|
39
|
+
* dedented (block style) and joined with `\n`. The `kind:` line populates
|
|
40
|
+
* `kind` rather than `fields`.
|
|
41
|
+
*/
|
|
42
|
+
function parseBody(inner) {
|
|
43
|
+
const fields = {};
|
|
44
|
+
let kind = '';
|
|
45
|
+
const lines = inner.split('\n');
|
|
46
|
+
let i = 0;
|
|
47
|
+
while (i < lines.length) {
|
|
48
|
+
const line = lines[i];
|
|
49
|
+
const m = /^(\s*)([A-Za-z0-9_]+)\s*:\s?(.*)$/.exec(line);
|
|
50
|
+
if (!m) {
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const indent = m[1].length;
|
|
55
|
+
const key = m[2];
|
|
56
|
+
const value = m[3];
|
|
57
|
+
if (value.trim() === '|') {
|
|
58
|
+
// Multiline block: gather following lines indented more than this key.
|
|
59
|
+
const collected = [];
|
|
60
|
+
let blockIndent = -1;
|
|
61
|
+
i++;
|
|
62
|
+
while (i < lines.length) {
|
|
63
|
+
const next = lines[i];
|
|
64
|
+
if (next.trim() === '') {
|
|
65
|
+
collected.push('');
|
|
66
|
+
i++;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const nextIndent = next.match(/^[ \t]*/)?.[0].length ?? 0;
|
|
70
|
+
if (nextIndent <= indent)
|
|
71
|
+
break;
|
|
72
|
+
if (blockIndent === -1)
|
|
73
|
+
blockIndent = nextIndent;
|
|
74
|
+
collected.push(next.slice(Math.min(blockIndent, nextIndent)));
|
|
75
|
+
i++;
|
|
76
|
+
}
|
|
77
|
+
while (collected.length && collected[collected.length - 1] === '') {
|
|
78
|
+
collected.pop();
|
|
79
|
+
}
|
|
80
|
+
const joined = collected.join('\n');
|
|
81
|
+
if (key === 'kind')
|
|
82
|
+
kind = joined.trim();
|
|
83
|
+
else
|
|
84
|
+
fields[key] = joined;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const v = stripQuotes(value.trim());
|
|
88
|
+
if (key === 'kind')
|
|
89
|
+
kind = v;
|
|
90
|
+
else
|
|
91
|
+
fields[key] = v;
|
|
92
|
+
i++;
|
|
93
|
+
}
|
|
94
|
+
return { kind, fields };
|
|
95
|
+
}
|
|
96
|
+
/** Char ranges `[start, end)` covered by HTML comments `<!-- … -->`. */
|
|
97
|
+
function commentRanges(source) {
|
|
98
|
+
const ranges = [];
|
|
99
|
+
let i = 0;
|
|
100
|
+
while (true) {
|
|
101
|
+
const open = source.indexOf('<!--', i);
|
|
102
|
+
if (open === -1)
|
|
103
|
+
break;
|
|
104
|
+
const close = source.indexOf('-->', open + 4);
|
|
105
|
+
if (close === -1) {
|
|
106
|
+
ranges.push([open, source.length]);
|
|
107
|
+
break;
|
|
108
|
+
} // unclosed → to end
|
|
109
|
+
ranges.push([open, close + 3]);
|
|
110
|
+
i = close + 3;
|
|
111
|
+
}
|
|
112
|
+
return ranges;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Scan a source for every `{{nyml … }}` block, in document order. Each result
|
|
116
|
+
* carries its parsed `kind`, `fields`, and `[start, end)` char offsets. Blocks
|
|
117
|
+
* inside an HTML comment (`<!-- … -->`) are skipped — so a commented-out element
|
|
118
|
+
* (e.g. content the editor's template picker preserves) does not render.
|
|
119
|
+
*/
|
|
120
|
+
export function scanNymlBlocks(source) {
|
|
121
|
+
const out = [];
|
|
122
|
+
const comments = commentRanges(source);
|
|
123
|
+
const inComment = (pos) => comments.some(([a, b]) => pos >= a && pos < b);
|
|
124
|
+
let searchFrom = 0;
|
|
125
|
+
while (searchFrom < source.length) {
|
|
126
|
+
const open = source.indexOf(OPEN, searchFrom);
|
|
127
|
+
if (open === -1)
|
|
128
|
+
break;
|
|
129
|
+
const close = findClosing(source, open + OPEN.length);
|
|
130
|
+
if (close === -1)
|
|
131
|
+
break;
|
|
132
|
+
if (!inComment(open)) {
|
|
133
|
+
const inner = source.slice(open + OPEN.length, close);
|
|
134
|
+
const { kind, fields } = parseBody(inner);
|
|
135
|
+
out.push({ kind, fields, start: open, end: close + 2 });
|
|
136
|
+
}
|
|
137
|
+
searchFrom = close + 2;
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
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 { fontPreset } from './fonts.js';
|
|
12
|
+
/* ─────────────────────────── helpers ─────────────────────────── */
|
|
13
|
+
/** Escape a user string for safe use inside a CSS `content: "..."` value. */
|
|
14
|
+
function cssString(raw) {
|
|
15
|
+
return '"' + raw.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
16
|
+
}
|
|
17
|
+
/** Named page sizes pass straight through; a custom string passes through too. */
|
|
18
|
+
function pageSizeValue(size) {
|
|
19
|
+
return size.trim();
|
|
20
|
+
}
|
|
21
|
+
/** The margin-box at-rule name for a page-number / header / footer position. */
|
|
22
|
+
function marginBoxName(pos) {
|
|
23
|
+
switch (pos) {
|
|
24
|
+
case 'header-left':
|
|
25
|
+
return '@top-left';
|
|
26
|
+
case 'header-center':
|
|
27
|
+
return '@top-center';
|
|
28
|
+
case 'header-right':
|
|
29
|
+
return '@top-right';
|
|
30
|
+
case 'footer-left':
|
|
31
|
+
return '@bottom-left';
|
|
32
|
+
case 'footer-center':
|
|
33
|
+
return '@bottom-center';
|
|
34
|
+
case 'footer-right':
|
|
35
|
+
return '@bottom-right';
|
|
36
|
+
default:
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** The `content` expression for a page-number style. */
|
|
41
|
+
function pageNumberExpr(style) {
|
|
42
|
+
switch (style) {
|
|
43
|
+
case 'simple':
|
|
44
|
+
return 'counter(page)';
|
|
45
|
+
case 'page-n':
|
|
46
|
+
return '"Page " counter(page)';
|
|
47
|
+
case 'page-n-of-N':
|
|
48
|
+
return '"Page " counter(page) " of " counter(pages)';
|
|
49
|
+
case 'n-of-N':
|
|
50
|
+
return 'counter(page) " of " counter(pages)';
|
|
51
|
+
case 'n-slash-N':
|
|
52
|
+
return 'counter(page) " / " counter(pages)';
|
|
53
|
+
case 'dash-n-dash':
|
|
54
|
+
return '"— " counter(page) " —"';
|
|
55
|
+
case 'brackets':
|
|
56
|
+
return '"[" counter(page) "]"';
|
|
57
|
+
case 'parentheses':
|
|
58
|
+
return '"(" counter(page) ")"';
|
|
59
|
+
default:
|
|
60
|
+
return 'counter(page)';
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/* ─────────────────────────── builder ─────────────────────────── */
|
|
64
|
+
/**
|
|
65
|
+
* Build the full page CSS string for a document. Pure: depends only on `s`.
|
|
66
|
+
*/
|
|
67
|
+
export function buildPageCss(s) {
|
|
68
|
+
// Font family is explicit only when the user/template set fontFamily or
|
|
69
|
+
// fontPreset; otherwise leave it to the theme (or base.css fallback).
|
|
70
|
+
const bodyFamily = s.fontFamily ?? (s.fontPreset ? fontPreset(s.fontPreset).family : undefined);
|
|
71
|
+
const headingFamily = s.fontHeadingPreset ? fontPreset(s.fontHeadingPreset).family : bodyFamily;
|
|
72
|
+
const marginBoxFamily = s.fontMarginBoxPreset
|
|
73
|
+
? fontPreset(s.fontMarginBoxPreset).family
|
|
74
|
+
: (bodyFamily ?? 'system-ui, "Segoe UI", Helvetica, Arial, sans-serif');
|
|
75
|
+
const headerRuleColor = s.headerRuleColor || s.decorationColor;
|
|
76
|
+
const footerRuleColor = s.footerRuleColor || s.decorationColor;
|
|
77
|
+
// Collect the margin-box declarations for the @page rule. Each value is the
|
|
78
|
+
// inner CSS (content + any per-box styling) keyed by margin-box at-rule.
|
|
79
|
+
const boxes = new Map();
|
|
80
|
+
const addBox = (name, decl) => {
|
|
81
|
+
const list = boxes.get(name);
|
|
82
|
+
if (list)
|
|
83
|
+
list.push(decl);
|
|
84
|
+
else
|
|
85
|
+
boxes.set(name, [decl]);
|
|
86
|
+
};
|
|
87
|
+
// Static running headers/footers.
|
|
88
|
+
const headerFs = `font-size: ${s.headerFontSize}pt;`;
|
|
89
|
+
const footerFs = `font-size: ${s.footerFontSize}pt;`;
|
|
90
|
+
const addStatic = (name, text, fontSize) => {
|
|
91
|
+
if (text && text.length > 0) {
|
|
92
|
+
addBox(name, `content: ${cssString(text)}; ${fontSize}`);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
addStatic('@top-left', s.headerLeft, headerFs);
|
|
96
|
+
addStatic('@top-center', s.headerCenter, headerFs);
|
|
97
|
+
addStatic('@top-right', s.headerRight, headerFs);
|
|
98
|
+
addStatic('@bottom-left', s.footerLeft, footerFs);
|
|
99
|
+
addStatic('@bottom-center', s.footerCenter, footerFs);
|
|
100
|
+
addStatic('@bottom-right', s.footerRight, footerFs);
|
|
101
|
+
// Page number → its margin box. A page number wins the `content` of its box
|
|
102
|
+
// (so it isn't double-declared with a static header/footer in the same box).
|
|
103
|
+
const pnBox = marginBoxName(s.pageNumberPosition);
|
|
104
|
+
if (pnBox && s.pageNumberPosition !== 'none') {
|
|
105
|
+
let expr = pageNumberExpr(s.pageNumberStyle);
|
|
106
|
+
// With clean front matter the body is renumbered from 1. paged.js' own page
|
|
107
|
+
// counter / counter-reset is unreliable across later page breaks, so the
|
|
108
|
+
// engine numbers body pages in JS after layout and feeds the values through
|
|
109
|
+
// CSS vars: --orz-pageno (this page's body number, set per page) and
|
|
110
|
+
// --orz-body-pages (body total = physical − front-matter pages). Both fall
|
|
111
|
+
// back to the native counters when unset (no front matter / pre-measure).
|
|
112
|
+
if (s.frontMatterClean) {
|
|
113
|
+
expr = expr
|
|
114
|
+
.replace(/counter\(pages\)/g, 'var(--orz-body-pages, counter(pages))')
|
|
115
|
+
.replace(/counter\(page\)/g, 'var(--orz-pageno, counter(page))');
|
|
116
|
+
}
|
|
117
|
+
const isHeader = s.pageNumberPosition.startsWith('header-');
|
|
118
|
+
const fontSize = isHeader ? headerFs : footerFs;
|
|
119
|
+
boxes.set(pnBox, [`content: ${expr}; ${fontSize}`]);
|
|
120
|
+
}
|
|
121
|
+
// Header/footer rule lines: drawn via a margin-box border. A rule applies to
|
|
122
|
+
// the whole edge, so attach it to the center box (always realized by paged.js).
|
|
123
|
+
if (s.headerRule) {
|
|
124
|
+
const name = '@top-center';
|
|
125
|
+
const list = boxes.get(name) ?? [];
|
|
126
|
+
list.push(`border-bottom: 0.5pt solid ${headerRuleColor};`);
|
|
127
|
+
boxes.set(name, list);
|
|
128
|
+
}
|
|
129
|
+
if (s.footerRule) {
|
|
130
|
+
const name = '@bottom-center';
|
|
131
|
+
const list = boxes.get(name) ?? [];
|
|
132
|
+
list.push(`border-top: 0.5pt solid ${footerRuleColor};`);
|
|
133
|
+
boxes.set(name, list);
|
|
134
|
+
}
|
|
135
|
+
// Assemble @page margin-box rules.
|
|
136
|
+
const marginBoxRules = [];
|
|
137
|
+
for (const [name, decls] of boxes) {
|
|
138
|
+
marginBoxRules.push(` ${name} {\n ${decls.join('\n ')}\n }`);
|
|
139
|
+
}
|
|
140
|
+
const margin = `${s.marginTop}mm ${s.marginRight}mm ${s.marginBottom}mm ${s.marginLeft}mm`;
|
|
141
|
+
const pageRule = `@page {\n` +
|
|
142
|
+
` size: ${pageSizeValue(s.pageSize)};\n` +
|
|
143
|
+
` margin: ${margin};\n` +
|
|
144
|
+
(marginBoxRules.length ? marginBoxRules.join('\n') + '\n' : '') +
|
|
145
|
+
`}`;
|
|
146
|
+
// First-page header/footer suppression.
|
|
147
|
+
const firstPageDecls = [];
|
|
148
|
+
if (s.firstPageHideHeader) {
|
|
149
|
+
firstPageDecls.push(' @top-left { content: none }', ' @top-center { content: none }', ' @top-right { content: none }');
|
|
150
|
+
}
|
|
151
|
+
if (s.firstPageHideFooter || s.firstPageSkipNumber) {
|
|
152
|
+
firstPageDecls.push(' @bottom-left { content: none }', ' @bottom-center { content: none }', ' @bottom-right { content: none }');
|
|
153
|
+
}
|
|
154
|
+
const firstPageRule = firstPageDecls.length
|
|
155
|
+
? `@page:first {\n${firstPageDecls.join('\n')}\n}`
|
|
156
|
+
: '';
|
|
157
|
+
// Front-matter "clean" mode: every `placement: page` page (title / abstract /
|
|
158
|
+
// toc) is rendered on the named page `orz-front` (paged.js tags it with the
|
|
159
|
+
// class `.pagedjs_orz-front_page`). Hide its margin rows to strip the running
|
|
160
|
+
// header, footer, and page number — done via the page class, NOT `@page
|
|
161
|
+
// orz-front { @top-* { content: none } }`, because paged.js leaks that
|
|
162
|
+
// `content: none` onto the base @page and blanks every page's chrome. Body
|
|
163
|
+
// renumbering is done in JS (per-page --orz-pageno), so no counter-reset here.
|
|
164
|
+
const frontMatterRule = s.frontMatterClean
|
|
165
|
+
? `.pagedjs_orz-front_page .pagedjs_margin-top,\n` +
|
|
166
|
+
`.pagedjs_orz-front_page .pagedjs_margin-bottom { display: none; }`
|
|
167
|
+
: '';
|
|
168
|
+
// :root design tokens (dom-contract).
|
|
169
|
+
const rootVars = [
|
|
170
|
+
` --page-size: ${pageSizeValue(s.pageSize)};`,
|
|
171
|
+
` --margin-t: ${s.marginTop}mm;`,
|
|
172
|
+
` --margin-b: ${s.marginBottom}mm;`,
|
|
173
|
+
` --margin-l: ${s.marginLeft}mm;`,
|
|
174
|
+
` --margin-r: ${s.marginRight}mm;`,
|
|
175
|
+
` --font-margin-box: ${marginBoxFamily};`,
|
|
176
|
+
` --font-size: ${s.fontSize}pt;`,
|
|
177
|
+
` --line-height: ${s.lineHeight};`,
|
|
178
|
+
];
|
|
179
|
+
// Look tokens: emit only when explicitly set, so a theme's own font / accent /
|
|
180
|
+
// page background apply by default and an explicit setting still wins (this
|
|
181
|
+
// sheet is added last). header/footer rules follow the accent when unset.
|
|
182
|
+
if (bodyFamily)
|
|
183
|
+
rootVars.push(` --font-body: ${bodyFamily};`);
|
|
184
|
+
if (headingFamily)
|
|
185
|
+
rootVars.push(` --font-heading: ${headingFamily};`);
|
|
186
|
+
if (s.decorationColor)
|
|
187
|
+
rootVars.push(` --accent: ${s.decorationColor};`);
|
|
188
|
+
if (s.pageBackground)
|
|
189
|
+
rootVars.push(` --page-bg: ${s.pageBackground};`);
|
|
190
|
+
rootVars.push(` --header-rule: ${headerRuleColor || 'var(--accent, #cccccc)'};`);
|
|
191
|
+
rootVars.push(` --footer-rule: ${footerRuleColor || 'var(--accent, #cccccc)'};`);
|
|
192
|
+
const rootRule = `:root {\n${rootVars.join('\n')}\n}`;
|
|
193
|
+
// Body / element base styling that consumes the tokens.
|
|
194
|
+
const baseRule = `.orz-doc {\n` +
|
|
195
|
+
` font-family: var(--font-body);\n` +
|
|
196
|
+
` font-size: var(--font-size);\n` +
|
|
197
|
+
` line-height: var(--line-height);\n` +
|
|
198
|
+
`}\n` +
|
|
199
|
+
`.pagedjs_margin-top, .pagedjs_margin-bottom,\n` +
|
|
200
|
+
`.pagedjs_margin-top-left, .pagedjs_margin-top-center, .pagedjs_margin-top-right,\n` +
|
|
201
|
+
`.pagedjs_margin-bottom-left, .pagedjs_margin-bottom-center, .pagedjs_margin-bottom-right {\n` +
|
|
202
|
+
` font-family: var(--font-margin-box);\n` +
|
|
203
|
+
`}`;
|
|
204
|
+
// Layout-behavior CSS.
|
|
205
|
+
const layout = [];
|
|
206
|
+
if (s.limitImageToPage) {
|
|
207
|
+
layout.push('img { max-width: 100%; max-height: 100%; }');
|
|
208
|
+
}
|
|
209
|
+
if (s.keepImageTogether) {
|
|
210
|
+
layout.push('img, figure { break-inside: avoid; }');
|
|
211
|
+
}
|
|
212
|
+
if (s.repeatTableHeader) {
|
|
213
|
+
layout.push('thead { display: table-header-group; }');
|
|
214
|
+
}
|
|
215
|
+
if (s.avoidTableRowBreaks) {
|
|
216
|
+
layout.push('tr { break-inside: avoid; }');
|
|
217
|
+
}
|
|
218
|
+
const layoutRule = layout.join('\n');
|
|
219
|
+
return [
|
|
220
|
+
pageRule,
|
|
221
|
+
firstPageRule,
|
|
222
|
+
frontMatterRule,
|
|
223
|
+
rootRule,
|
|
224
|
+
baseRule,
|
|
225
|
+
layoutRule,
|
|
226
|
+
s.customCss ?? '',
|
|
227
|
+
]
|
|
228
|
+
.filter((part) => part.length > 0)
|
|
229
|
+
.join('\n\n');
|
|
230
|
+
}
|