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,390 @@
|
|
|
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
|
+
/* ─────────────────────────── defaults ─────────────────────────── */
|
|
13
|
+
/** Every field at its documented default (docs/document-model.md). */
|
|
14
|
+
export const DEFAULTS = {
|
|
15
|
+
// page
|
|
16
|
+
pageSize: 'A4',
|
|
17
|
+
marginTop: 20,
|
|
18
|
+
marginBottom: 20,
|
|
19
|
+
marginLeft: 20,
|
|
20
|
+
marginRight: 20,
|
|
21
|
+
// typography — fontPreset / decorationColor intentionally unset so a theme's
|
|
22
|
+
// own font + accent apply by default; an explicit setting (or a template)
|
|
23
|
+
// overrides them (page CSS is added last and only emits a token when set).
|
|
24
|
+
fontSize: 12,
|
|
25
|
+
lineHeight: 1.5,
|
|
26
|
+
// page numbers
|
|
27
|
+
pageNumberPosition: 'footer-center',
|
|
28
|
+
pageNumberStyle: 'simple',
|
|
29
|
+
pageNumberStartPage: 1,
|
|
30
|
+
firstPageSkipNumber: false,
|
|
31
|
+
// running headers / footers
|
|
32
|
+
headerLeft: '',
|
|
33
|
+
headerCenter: '',
|
|
34
|
+
headerRight: '',
|
|
35
|
+
headerRule: true,
|
|
36
|
+
headerFontSize: 9,
|
|
37
|
+
footerLeft: '',
|
|
38
|
+
footerCenter: '',
|
|
39
|
+
footerRight: '',
|
|
40
|
+
footerRule: true,
|
|
41
|
+
footerFontSize: 9,
|
|
42
|
+
firstPageHideHeader: false,
|
|
43
|
+
firstPageHideFooter: false,
|
|
44
|
+
frontMatterClean: false,
|
|
45
|
+
// layout behavior
|
|
46
|
+
limitImageToPage: true,
|
|
47
|
+
keepImageTogether: true,
|
|
48
|
+
repeatTableHeader: true,
|
|
49
|
+
avoidTableRowBreaks: true,
|
|
50
|
+
// theme — the default look (font + accent + element style) when a document
|
|
51
|
+
// doesn't pick one. Templates set layout only, never a theme.
|
|
52
|
+
theme: 'light-academic-1',
|
|
53
|
+
// dynamic switch — empty by default (no conditional content removed).
|
|
54
|
+
dynamicChoices: {},
|
|
55
|
+
};
|
|
56
|
+
/* ─────────────────────────── parsing ─────────────────────────── */
|
|
57
|
+
/**
|
|
58
|
+
* Locate the first `{{nyml ... }}` block whose body declares `kind: document`,
|
|
59
|
+
* parse its `key: value` lines (including `key: |` multiline blocks) into a
|
|
60
|
+
* {@link RawDocSettings}, and return the source with that block removed so it
|
|
61
|
+
* is not rendered as content. No document block → `{ raw: {}, body: source }`.
|
|
62
|
+
*/
|
|
63
|
+
export function parseDocSettings(source) {
|
|
64
|
+
let searchFrom = 0;
|
|
65
|
+
while (searchFrom < source.length) {
|
|
66
|
+
const open = source.indexOf('{{nyml', searchFrom);
|
|
67
|
+
if (open === -1)
|
|
68
|
+
break;
|
|
69
|
+
// Find the matching closing `}}` for this block, allowing `}` inside the
|
|
70
|
+
// body as long as it is not the `}}` terminator.
|
|
71
|
+
const close = findClosing(source, open + '{{nyml'.length);
|
|
72
|
+
if (close === -1)
|
|
73
|
+
break;
|
|
74
|
+
const inner = source.slice(open + '{{nyml'.length, close);
|
|
75
|
+
if (isDocumentBlock(inner)) {
|
|
76
|
+
const raw = parseNymlBody(inner);
|
|
77
|
+
const body = stripLeadingBlank(source, open, close);
|
|
78
|
+
return { raw, body };
|
|
79
|
+
}
|
|
80
|
+
searchFrom = close + 2;
|
|
81
|
+
}
|
|
82
|
+
return { raw: {}, body: source };
|
|
83
|
+
}
|
|
84
|
+
/** Find the index of the `}}` that closes a `{{nyml` opened at/after `from`. */
|
|
85
|
+
function findClosing(source, from) {
|
|
86
|
+
for (let i = from; i < source.length - 1; i++) {
|
|
87
|
+
if (source[i] === '}' && source[i + 1] === '}')
|
|
88
|
+
return i;
|
|
89
|
+
}
|
|
90
|
+
return -1;
|
|
91
|
+
}
|
|
92
|
+
/** Remove the block `[open, close+2)` and collapse a resulting blank gap. */
|
|
93
|
+
function stripLeadingBlank(source, open, close) {
|
|
94
|
+
const before = source.slice(0, open);
|
|
95
|
+
let after = source.slice(close + 2);
|
|
96
|
+
// If the block sat on its own line(s), drop one trailing newline pair so we
|
|
97
|
+
// don't leave a large blank hole where it was.
|
|
98
|
+
if (/\n[ \t]*$/.test(before) || before === '') {
|
|
99
|
+
after = after.replace(/^[ \t]*\n/, '');
|
|
100
|
+
}
|
|
101
|
+
return before + after;
|
|
102
|
+
}
|
|
103
|
+
/** True if the nyml body contains a `kind: document` declaration. */
|
|
104
|
+
function isDocumentBlock(inner) {
|
|
105
|
+
return /(^|\n)\s*kind\s*:\s*document\s*(\n|$)/.test(inner);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Parse a nyml body into a flat `key: value` map. Supports `key: |` multiline
|
|
109
|
+
* blocks: subsequent lines indented more than the key are joined (newline
|
|
110
|
+
* separated) as that key's value. The `kind:` line itself is skipped.
|
|
111
|
+
*/
|
|
112
|
+
function parseNymlBody(inner) {
|
|
113
|
+
const raw = {};
|
|
114
|
+
const lines = inner.split('\n');
|
|
115
|
+
let i = 0;
|
|
116
|
+
while (i < lines.length) {
|
|
117
|
+
const line = lines[i];
|
|
118
|
+
const m = /^(\s*)([A-Za-z0-9_]+)\s*:\s?(.*)$/.exec(line);
|
|
119
|
+
if (!m) {
|
|
120
|
+
i++;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const indent = m[1].length;
|
|
124
|
+
const key = m[2];
|
|
125
|
+
let value = m[3];
|
|
126
|
+
if (key === 'kind') {
|
|
127
|
+
i++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (value.trim() === '|') {
|
|
131
|
+
// Multiline block: gather following lines indented more than this key.
|
|
132
|
+
// Dedent by the indentation of the first content line (block style).
|
|
133
|
+
const collected = [];
|
|
134
|
+
let blockIndent = -1;
|
|
135
|
+
i++;
|
|
136
|
+
while (i < lines.length) {
|
|
137
|
+
const next = lines[i];
|
|
138
|
+
if (next.trim() === '') {
|
|
139
|
+
collected.push('');
|
|
140
|
+
i++;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const nextIndent = next.match(/^[ \t]*/)?.[0].length ?? 0;
|
|
144
|
+
if (nextIndent <= indent)
|
|
145
|
+
break;
|
|
146
|
+
if (blockIndent === -1)
|
|
147
|
+
blockIndent = nextIndent;
|
|
148
|
+
collected.push(next.slice(Math.min(blockIndent, nextIndent)));
|
|
149
|
+
i++;
|
|
150
|
+
}
|
|
151
|
+
// Drop trailing blank lines from the collected block.
|
|
152
|
+
while (collected.length && collected[collected.length - 1] === '') {
|
|
153
|
+
collected.pop();
|
|
154
|
+
}
|
|
155
|
+
raw[key] = collected.join('\n');
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
raw[key] = stripQuotes(value.trim());
|
|
159
|
+
i++;
|
|
160
|
+
}
|
|
161
|
+
return raw;
|
|
162
|
+
}
|
|
163
|
+
/** Strip a single pair of matching surrounding quotes, if present. */
|
|
164
|
+
function stripQuotes(s) {
|
|
165
|
+
if (s.length >= 2) {
|
|
166
|
+
const first = s[0];
|
|
167
|
+
const last = s[s.length - 1];
|
|
168
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
169
|
+
return s.slice(1, -1);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return s;
|
|
173
|
+
}
|
|
174
|
+
/* ─────────────────────────── normalization ─────────────────────────── */
|
|
175
|
+
const PAGE_SIZE_ALIASES = {
|
|
176
|
+
a3: 'A3',
|
|
177
|
+
a4: 'A4',
|
|
178
|
+
a5: 'A5',
|
|
179
|
+
letter: 'Letter',
|
|
180
|
+
legal: 'Legal',
|
|
181
|
+
};
|
|
182
|
+
function normPageSize(v) {
|
|
183
|
+
const key = v.trim().toLowerCase();
|
|
184
|
+
return PAGE_SIZE_ALIASES[key] ?? v.trim();
|
|
185
|
+
}
|
|
186
|
+
function toNumber(v) {
|
|
187
|
+
if (typeof v === 'number')
|
|
188
|
+
return v;
|
|
189
|
+
if (typeof v === 'boolean')
|
|
190
|
+
return v ? 1 : 0;
|
|
191
|
+
const n = parseFloat(String(v).trim());
|
|
192
|
+
return Number.isFinite(n) ? n : undefined;
|
|
193
|
+
}
|
|
194
|
+
function toBool(v) {
|
|
195
|
+
if (typeof v === 'boolean')
|
|
196
|
+
return v;
|
|
197
|
+
if (typeof v === 'number')
|
|
198
|
+
return v !== 0;
|
|
199
|
+
const s = String(v).trim().toLowerCase();
|
|
200
|
+
return s === 'true' || s === 'yes' || s === '1' || s === 'on';
|
|
201
|
+
}
|
|
202
|
+
function toStr(v) {
|
|
203
|
+
return String(v);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Map snake_case raw keys → camelCase {@link DocSettings} fields, coercing
|
|
207
|
+
* types and normalizing aliases. Only keys present in `raw` are included;
|
|
208
|
+
* unknown keys are ignored.
|
|
209
|
+
*/
|
|
210
|
+
export function normalizeRawToLayer(raw) {
|
|
211
|
+
const out = {};
|
|
212
|
+
const setNum = (key, v) => {
|
|
213
|
+
const n = toNumber(v);
|
|
214
|
+
if (n !== undefined)
|
|
215
|
+
out[key] = n;
|
|
216
|
+
};
|
|
217
|
+
const setBool = (key, v) => {
|
|
218
|
+
out[key] = toBool(v);
|
|
219
|
+
};
|
|
220
|
+
const setStr = (key, v) => {
|
|
221
|
+
out[key] = toStr(v);
|
|
222
|
+
};
|
|
223
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
224
|
+
if (v === undefined)
|
|
225
|
+
continue;
|
|
226
|
+
switch (k) {
|
|
227
|
+
// page
|
|
228
|
+
case 'page_size':
|
|
229
|
+
out.pageSize = normPageSize(toStr(v));
|
|
230
|
+
break;
|
|
231
|
+
case 'margin_top':
|
|
232
|
+
setNum('marginTop', v);
|
|
233
|
+
break;
|
|
234
|
+
case 'margin_bottom':
|
|
235
|
+
setNum('marginBottom', v);
|
|
236
|
+
break;
|
|
237
|
+
case 'margin_left':
|
|
238
|
+
setNum('marginLeft', v);
|
|
239
|
+
break;
|
|
240
|
+
case 'margin_right':
|
|
241
|
+
setNum('marginRight', v);
|
|
242
|
+
break;
|
|
243
|
+
case 'page_background':
|
|
244
|
+
setStr('pageBackground', v);
|
|
245
|
+
break;
|
|
246
|
+
// typography
|
|
247
|
+
case 'font_preset':
|
|
248
|
+
out.fontPreset = toStr(v).trim();
|
|
249
|
+
break;
|
|
250
|
+
case 'font_family':
|
|
251
|
+
setStr('fontFamily', v);
|
|
252
|
+
break;
|
|
253
|
+
case 'font_heading_preset':
|
|
254
|
+
out.fontHeadingPreset = toStr(v).trim();
|
|
255
|
+
break;
|
|
256
|
+
case 'font_margin_box_preset':
|
|
257
|
+
out.fontMarginBoxPreset = toStr(v).trim();
|
|
258
|
+
break;
|
|
259
|
+
case 'font_size':
|
|
260
|
+
setNum('fontSize', v);
|
|
261
|
+
break;
|
|
262
|
+
case 'line_height':
|
|
263
|
+
setNum('lineHeight', v);
|
|
264
|
+
break;
|
|
265
|
+
case 'decoration_color':
|
|
266
|
+
setStr('decorationColor', v);
|
|
267
|
+
break;
|
|
268
|
+
// page numbers
|
|
269
|
+
case 'page_number_position':
|
|
270
|
+
out.pageNumberPosition = toStr(v).trim();
|
|
271
|
+
break;
|
|
272
|
+
case 'page_number_style':
|
|
273
|
+
out.pageNumberStyle = toStr(v).trim();
|
|
274
|
+
break;
|
|
275
|
+
case 'page_number_start_page':
|
|
276
|
+
setNum('pageNumberStartPage', v);
|
|
277
|
+
break;
|
|
278
|
+
case 'first_page_skip_number':
|
|
279
|
+
setBool('firstPageSkipNumber', v);
|
|
280
|
+
break;
|
|
281
|
+
// headers / footers
|
|
282
|
+
case 'header_left':
|
|
283
|
+
setStr('headerLeft', v);
|
|
284
|
+
break;
|
|
285
|
+
case 'header_center':
|
|
286
|
+
setStr('headerCenter', v);
|
|
287
|
+
break;
|
|
288
|
+
case 'header_right':
|
|
289
|
+
setStr('headerRight', v);
|
|
290
|
+
break;
|
|
291
|
+
case 'header_rule':
|
|
292
|
+
setBool('headerRule', v);
|
|
293
|
+
break;
|
|
294
|
+
case 'header_rule_color':
|
|
295
|
+
setStr('headerRuleColor', v);
|
|
296
|
+
break;
|
|
297
|
+
case 'header_font_size':
|
|
298
|
+
setNum('headerFontSize', v);
|
|
299
|
+
break;
|
|
300
|
+
case 'footer_left':
|
|
301
|
+
setStr('footerLeft', v);
|
|
302
|
+
break;
|
|
303
|
+
case 'footer_center':
|
|
304
|
+
setStr('footerCenter', v);
|
|
305
|
+
break;
|
|
306
|
+
case 'footer_right':
|
|
307
|
+
setStr('footerRight', v);
|
|
308
|
+
break;
|
|
309
|
+
case 'footer_rule':
|
|
310
|
+
setBool('footerRule', v);
|
|
311
|
+
break;
|
|
312
|
+
case 'footer_rule_color':
|
|
313
|
+
setStr('footerRuleColor', v);
|
|
314
|
+
break;
|
|
315
|
+
case 'footer_font_size':
|
|
316
|
+
setNum('footerFontSize', v);
|
|
317
|
+
break;
|
|
318
|
+
case 'first_page_hide_header':
|
|
319
|
+
setBool('firstPageHideHeader', v);
|
|
320
|
+
break;
|
|
321
|
+
case 'first_page_hide_footer':
|
|
322
|
+
setBool('firstPageHideFooter', v);
|
|
323
|
+
break;
|
|
324
|
+
case 'front_matter':
|
|
325
|
+
case 'front_matter_clean': {
|
|
326
|
+
const fm = toStr(v).toLowerCase();
|
|
327
|
+
out.frontMatterClean =
|
|
328
|
+
fm === 'clean' || fm === 'plain' || toBool(v);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
// layout behavior
|
|
332
|
+
case 'limit_image_to_page':
|
|
333
|
+
setBool('limitImageToPage', v);
|
|
334
|
+
break;
|
|
335
|
+
case 'keep_image_together':
|
|
336
|
+
setBool('keepImageTogether', v);
|
|
337
|
+
break;
|
|
338
|
+
case 'repeat_table_header':
|
|
339
|
+
setBool('repeatTableHeader', v);
|
|
340
|
+
break;
|
|
341
|
+
case 'avoid_table_row_breaks':
|
|
342
|
+
setBool('avoidTableRowBreaks', v);
|
|
343
|
+
break;
|
|
344
|
+
// theme / template / escape hatch
|
|
345
|
+
case 'theme':
|
|
346
|
+
out.theme = toStr(v).trim();
|
|
347
|
+
break;
|
|
348
|
+
case 'template':
|
|
349
|
+
out.template = toStr(v).trim();
|
|
350
|
+
break;
|
|
351
|
+
case 'custom_css':
|
|
352
|
+
setStr('customCss', v);
|
|
353
|
+
break;
|
|
354
|
+
case 'dynamic_choices': {
|
|
355
|
+
// multiline `key: value` map → object with normalized keys
|
|
356
|
+
// (lowercase, hyphens → underscores), matching data-*-when resolution.
|
|
357
|
+
const map = {};
|
|
358
|
+
for (const line of toStr(v).split(/\r?\n/)) {
|
|
359
|
+
const idx = line.indexOf(':');
|
|
360
|
+
if (idx < 0)
|
|
361
|
+
continue;
|
|
362
|
+
const key = line.slice(0, idx).trim().toLowerCase().replace(/-/g, '_');
|
|
363
|
+
if (key)
|
|
364
|
+
map[key] = line.slice(idx + 1).trim();
|
|
365
|
+
}
|
|
366
|
+
out.dynamicChoices = map;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
default:
|
|
370
|
+
// unknown key — ignore
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
/* ─────────────────────────── merging ─────────────────────────── */
|
|
377
|
+
/**
|
|
378
|
+
* Shallow-merge layers left → right (later wins). Intended use:
|
|
379
|
+
* `mergeSettings(DEFAULTS, templateLayer, userLayer)`.
|
|
380
|
+
*/
|
|
381
|
+
export function mergeSettings(...layers) {
|
|
382
|
+
const out = { ...DEFAULTS };
|
|
383
|
+
for (const layer of layers) {
|
|
384
|
+
for (const [k, v] of Object.entries(layer)) {
|
|
385
|
+
if (v !== undefined)
|
|
386
|
+
out[k] = v;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return out;
|
|
390
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
// Templates set LAYOUT only (page size, margins, furniture). Font, accent, and
|
|
22
|
+
// element style come from the theme — so the same theme looks the same across
|
|
23
|
+
// every template. A document still overrides any of this in its settings block.
|
|
24
|
+
const ARTICLE_SETTINGS = { pageSize: 'Letter' };
|
|
25
|
+
const REPORT_SETTINGS = { pageSize: 'Letter' };
|
|
26
|
+
const EXAM_SETTINGS = { pageSize: 'Letter' };
|
|
27
|
+
/** The 10 curated templates. */
|
|
28
|
+
export const TEMPLATES = {
|
|
29
|
+
'article-page': {
|
|
30
|
+
label: 'Article — title page',
|
|
31
|
+
description: 'Academic paper with a dedicated title page, then abstract + body.',
|
|
32
|
+
group: 'Article',
|
|
33
|
+
skeleton: 'article-page',
|
|
34
|
+
settings: { ...ARTICLE_SETTINGS, template: 'article-page' },
|
|
35
|
+
},
|
|
36
|
+
'article-section': {
|
|
37
|
+
label: 'Article — title section',
|
|
38
|
+
description: 'Academic paper with an inline title block above the body.',
|
|
39
|
+
group: 'Article',
|
|
40
|
+
skeleton: 'article-section',
|
|
41
|
+
settings: { ...ARTICLE_SETTINGS, template: 'article-section' },
|
|
42
|
+
},
|
|
43
|
+
'report-page': {
|
|
44
|
+
label: 'Report — title page',
|
|
45
|
+
description: 'Business/technical report with a cover page, TOC, then body.',
|
|
46
|
+
group: 'Report',
|
|
47
|
+
skeleton: 'report-page',
|
|
48
|
+
settings: { ...REPORT_SETTINGS, template: 'report-page' },
|
|
49
|
+
},
|
|
50
|
+
'report-section': {
|
|
51
|
+
label: 'Report — title section',
|
|
52
|
+
description: 'Business/technical report with an inline title block.',
|
|
53
|
+
group: 'Report',
|
|
54
|
+
skeleton: 'report-section',
|
|
55
|
+
settings: { ...REPORT_SETTINGS, template: 'report-section' },
|
|
56
|
+
},
|
|
57
|
+
'exam-page': {
|
|
58
|
+
label: 'Exam — cover page',
|
|
59
|
+
description: 'Exam with a cover/instructions page, then numbered questions.',
|
|
60
|
+
group: 'Exam',
|
|
61
|
+
skeleton: 'exam-page',
|
|
62
|
+
settings: { ...EXAM_SETTINGS, template: 'exam-page' },
|
|
63
|
+
},
|
|
64
|
+
'exam-section': {
|
|
65
|
+
label: 'Exam — title section',
|
|
66
|
+
description: 'Exam with an inline header block, then numbered questions.',
|
|
67
|
+
group: 'Exam',
|
|
68
|
+
skeleton: 'exam-section',
|
|
69
|
+
settings: { ...EXAM_SETTINGS, template: 'exam-section' },
|
|
70
|
+
},
|
|
71
|
+
letter: {
|
|
72
|
+
label: 'Letter',
|
|
73
|
+
description: 'Formal letter: letterhead, inside address, body, signature.',
|
|
74
|
+
group: 'Letter',
|
|
75
|
+
skeleton: 'letter',
|
|
76
|
+
settings: { template: 'letter', pageSize: 'Letter' },
|
|
77
|
+
},
|
|
78
|
+
'cover-letter': {
|
|
79
|
+
label: 'Cover letter',
|
|
80
|
+
description: 'Job cover letter: sender header, recipient, body, sign-off.',
|
|
81
|
+
group: 'Letter',
|
|
82
|
+
skeleton: 'cover-letter',
|
|
83
|
+
settings: { template: 'cover-letter', pageSize: 'Letter' },
|
|
84
|
+
},
|
|
85
|
+
cv: {
|
|
86
|
+
label: 'CV — classic',
|
|
87
|
+
description: 'Résumé: name/contact header and clean sectioned body.',
|
|
88
|
+
group: 'CV',
|
|
89
|
+
skeleton: 'cv',
|
|
90
|
+
settings: { template: 'cv', pageSize: 'Letter' },
|
|
91
|
+
},
|
|
92
|
+
'cv-modern': {
|
|
93
|
+
label: 'CV — modern',
|
|
94
|
+
description: 'Accent name + rule, uppercase section labels, right-aligned dates, skill chips.',
|
|
95
|
+
group: 'CV',
|
|
96
|
+
skeleton: 'cv-modern',
|
|
97
|
+
settings: { template: 'cv-modern', pageSize: 'Letter' },
|
|
98
|
+
},
|
|
99
|
+
'cv-elegant': {
|
|
100
|
+
label: 'CV — elegant',
|
|
101
|
+
description: 'Centered, letter-spaced name, hairline rules, two-column "ledger" entries.',
|
|
102
|
+
group: 'CV',
|
|
103
|
+
skeleton: 'cv-elegant',
|
|
104
|
+
settings: { template: 'cv-elegant', pageSize: 'Letter' },
|
|
105
|
+
},
|
|
106
|
+
note: {
|
|
107
|
+
label: 'Note',
|
|
108
|
+
description: 'Clean readable note: minimal furniture on A4.',
|
|
109
|
+
group: 'Note',
|
|
110
|
+
skeleton: 'note',
|
|
111
|
+
settings: { template: 'note', pageSize: 'A4' },
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
/** Legacy single names → their `-section` variant. */
|
|
115
|
+
const LEGACY_ALIASES = {
|
|
116
|
+
article: 'article-section',
|
|
117
|
+
report: 'report-section',
|
|
118
|
+
exam: 'exam-section',
|
|
119
|
+
};
|
|
120
|
+
/** Canonicalize a (possibly legacy) template name, or `undefined` if unknown. */
|
|
121
|
+
export function canonicalTemplate(name) {
|
|
122
|
+
if (name in TEMPLATES)
|
|
123
|
+
return name;
|
|
124
|
+
if (name in LEGACY_ALIASES)
|
|
125
|
+
return LEGACY_ALIASES[name];
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
/** Return a template's *settings* defaults layer, or `{}` for an unknown name. */
|
|
129
|
+
export function resolveTemplate(name) {
|
|
130
|
+
const key = canonicalTemplate(String(name));
|
|
131
|
+
return key ? TEMPLATES[key].settings : {};
|
|
132
|
+
}
|
|
133
|
+
/** The starter-markdown file stem for a template, or `undefined` if unknown. */
|
|
134
|
+
export function templateSkeletonName(name) {
|
|
135
|
+
const key = canonicalTemplate(String(name));
|
|
136
|
+
return key ? TEMPLATES[key].skeleton : undefined;
|
|
137
|
+
}
|
|
138
|
+
/** Picker catalog: id + metadata for every template (no settings/skeleton text). */
|
|
139
|
+
export function templateList() {
|
|
140
|
+
return Object.keys(TEMPLATES).map((id) => ({
|
|
141
|
+
id,
|
|
142
|
+
label: TEMPLATES[id].label,
|
|
143
|
+
description: TEMPLATES[id].description,
|
|
144
|
+
group: TEMPLATES[id].group,
|
|
145
|
+
skeleton: TEMPLATES[id].skeleton,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const THEME_FONTS = {
|
|
2
|
+
none: { body: 'system-serif', heading: 'system-serif' },
|
|
3
|
+
'light-neat-1': { body: 'inter', heading: 'inter' },
|
|
4
|
+
'light-neat-2': { body: 'ibm-plex-sans', heading: 'ibm-plex-sans' },
|
|
5
|
+
'light-neat-3': { body: 'raleway', heading: 'raleway' },
|
|
6
|
+
'light-academic-1': { body: 'source-serif-4', heading: 'source-serif-4' },
|
|
7
|
+
'light-academic-2': { body: 'crimson-pro', heading: 'inter' },
|
|
8
|
+
'beige-decent-1': { body: 'lora', heading: 'lora' },
|
|
9
|
+
'beige-decent-2': { body: 'noto-serif', heading: 'noto-serif' },
|
|
10
|
+
};
|