webtex-cn 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.
@@ -0,0 +1,1788 @@
1
+ // src/parser/tokenizer.js
2
+ var TokenType = {
3
+ COMMAND: "COMMAND",
4
+ OPEN_BRACE: "OPEN_BRACE",
5
+ CLOSE_BRACE: "CLOSE_BRACE",
6
+ OPEN_BRACKET: "OPEN_BRACKET",
7
+ CLOSE_BRACKET: "CLOSE_BRACKET",
8
+ TEXT: "TEXT",
9
+ NEWLINE: "NEWLINE",
10
+ // \\
11
+ COMMENT: "COMMENT",
12
+ BEGIN: "BEGIN",
13
+ END: "END",
14
+ MATH: "MATH",
15
+ PARAGRAPH_BREAK: "PARAGRAPH_BREAK",
16
+ EOF: "EOF"
17
+ };
18
+ function isLetter(ch) {
19
+ return /[a-zA-Z]/.test(ch);
20
+ }
21
+ function isCJK(ch) {
22
+ const code = ch.codePointAt(0);
23
+ if (code >= 19968 && code <= 40959) return true;
24
+ if (code >= 13312 && code <= 19903) return true;
25
+ if (code >= 63744 && code <= 64255) return true;
26
+ return false;
27
+ }
28
+ function isCommandChar(ch) {
29
+ return isLetter(ch) || isCJK(ch) || ch === "@" || ch === "*";
30
+ }
31
+ var Tokenizer = class {
32
+ constructor(source) {
33
+ this.source = source;
34
+ this.pos = 0;
35
+ this.tokens = [];
36
+ }
37
+ peek() {
38
+ if (this.pos >= this.source.length) return null;
39
+ return this.source[this.pos];
40
+ }
41
+ advance() {
42
+ const ch = this.source[this.pos];
43
+ this.pos++;
44
+ return ch;
45
+ }
46
+ tokenize() {
47
+ while (this.pos < this.source.length) {
48
+ const ch = this.peek();
49
+ if (ch === "%") {
50
+ this.skipComment();
51
+ continue;
52
+ }
53
+ if (ch === "$") {
54
+ this.readMath();
55
+ continue;
56
+ }
57
+ if (ch === "\\") {
58
+ this.readCommand();
59
+ continue;
60
+ }
61
+ if (ch === "{") {
62
+ this.tokens.push({ type: TokenType.OPEN_BRACE, value: "{" });
63
+ this.advance();
64
+ continue;
65
+ }
66
+ if (ch === "}") {
67
+ this.tokens.push({ type: TokenType.CLOSE_BRACE, value: "}" });
68
+ this.advance();
69
+ continue;
70
+ }
71
+ if (ch === "[") {
72
+ this.tokens.push({ type: TokenType.OPEN_BRACKET, value: "[" });
73
+ this.advance();
74
+ continue;
75
+ }
76
+ if (ch === "]") {
77
+ this.tokens.push({ type: TokenType.CLOSE_BRACKET, value: "]" });
78
+ this.advance();
79
+ continue;
80
+ }
81
+ this.readText();
82
+ }
83
+ this.tokens.push({ type: TokenType.EOF, value: "" });
84
+ return this.tokens;
85
+ }
86
+ skipComment() {
87
+ while (this.pos < this.source.length && this.source[this.pos] !== "\n") {
88
+ this.pos++;
89
+ }
90
+ if (this.pos < this.source.length) this.pos++;
91
+ }
92
+ readCommand() {
93
+ this.advance();
94
+ if (this.pos >= this.source.length) {
95
+ this.tokens.push({ type: TokenType.TEXT, value: "\\" });
96
+ return;
97
+ }
98
+ const nextCh = this.peek();
99
+ if (nextCh === "\\") {
100
+ this.advance();
101
+ this.tokens.push({ type: TokenType.NEWLINE, value: "\\\\" });
102
+ return;
103
+ }
104
+ if ("{}[]%$&#_~^".includes(nextCh)) {
105
+ this.advance();
106
+ this.tokens.push({ type: TokenType.TEXT, value: nextCh });
107
+ return;
108
+ }
109
+ if (nextCh === " " || nextCh === "\n") {
110
+ this.advance();
111
+ this.tokens.push({ type: TokenType.TEXT, value: " " });
112
+ return;
113
+ }
114
+ if (!isCommandChar(nextCh)) {
115
+ this.advance();
116
+ this.tokens.push({ type: TokenType.COMMAND, value: nextCh });
117
+ return;
118
+ }
119
+ const isAsciiStart = isLetter(nextCh) || nextCh === "@" || nextCh === "*";
120
+ const isCJKStart = isCJK(nextCh);
121
+ let name = "";
122
+ if (isAsciiStart) {
123
+ while (this.pos < this.source.length && (isLetter(this.peek()) || this.peek() === "@" || this.peek() === "*")) {
124
+ name += this.advance();
125
+ }
126
+ while (this.pos < this.source.length && this.source[this.pos] === " ") {
127
+ this.pos++;
128
+ }
129
+ } else if (isCJKStart) {
130
+ while (this.pos < this.source.length && isCJK(this.peek())) {
131
+ name += this.advance();
132
+ }
133
+ }
134
+ if (name === "begin") {
135
+ this.tokens.push({ type: TokenType.BEGIN, value: "begin" });
136
+ } else if (name === "end") {
137
+ this.tokens.push({ type: TokenType.END, value: "end" });
138
+ } else {
139
+ this.tokens.push({ type: TokenType.COMMAND, value: name });
140
+ }
141
+ }
142
+ readMath() {
143
+ this.advance();
144
+ let content = "";
145
+ while (this.pos < this.source.length) {
146
+ const ch = this.peek();
147
+ if (ch === "$") {
148
+ this.advance();
149
+ this.tokens.push({ type: TokenType.MATH, value: content });
150
+ return;
151
+ }
152
+ content += this.advance();
153
+ }
154
+ this.tokens.push({ type: TokenType.TEXT, value: "$" + content });
155
+ }
156
+ readText() {
157
+ let text = "";
158
+ while (this.pos < this.source.length) {
159
+ const ch = this.peek();
160
+ if (ch === "\\" || ch === "{" || ch === "}" || ch === "[" || ch === "]" || ch === "%" || ch === "$") {
161
+ break;
162
+ }
163
+ text += this.advance();
164
+ }
165
+ if (text) {
166
+ const parts = text.split(/\n[ \t]*\n/);
167
+ for (let i = 0; i < parts.length; i++) {
168
+ if (i > 0) {
169
+ this.tokens.push({ type: TokenType.PARAGRAPH_BREAK, value: "" });
170
+ }
171
+ const collapsed = parts[i].replace(/[ \t]+/g, " ");
172
+ if (collapsed.trim() || collapsed === " ") {
173
+ this.tokens.push({ type: TokenType.TEXT, value: collapsed });
174
+ }
175
+ }
176
+ }
177
+ }
178
+ };
179
+
180
+ // src/parser/commands.js
181
+ var commandRegistry = {
182
+ // Document structure
183
+ "documentclass": { args: ["optional", "required"] },
184
+ "title": { args: ["required"] },
185
+ "chapter": { args: ["required"] },
186
+ // Jiazhu (夹注)
187
+ "\u5939\u6CE8": { args: ["optional", "required"], node: "jiazhu" },
188
+ "JiaZhu": { alias: "\u5939\u6CE8" },
189
+ "\u5355\u884C\u5939\u6CE8": { args: ["optional", "required"], node: "jiazhu", single: true },
190
+ "DanHangJiaZhu": { alias: "\u5355\u884C\u5939\u6CE8" },
191
+ // SideNote (侧批)
192
+ "\u4FA7\u6279": { args: ["optional", "required"], node: "sidenote" },
193
+ "SideNode": { alias: "\u4FA7\u6279" },
194
+ "CePi": { alias: "\u4FA7\u6279" },
195
+ // MeiPi (眉批)
196
+ "\u7709\u6279": { args: ["optional", "required"], node: "meipi" },
197
+ "MeiPi": { alias: "\u7709\u6279" },
198
+ // PiZhu (批注)
199
+ "\u6279\u6CE8": { args: ["optional", "required"], node: "pizhu" },
200
+ "PiZhu": { alias: "\u6279\u6CE8" },
201
+ // TextBox
202
+ "\u6587\u672C\u6846": { args: ["optional", "required"], node: "textbox" },
203
+ "TextBox": { alias: "\u6587\u672C\u6846" },
204
+ "\u586B\u5145\u6587\u672C\u6846": { args: ["optional", "required"], node: "fillTextbox" },
205
+ "FillTextBox": { alias: "\u586B\u5145\u6587\u672C\u6846" },
206
+ // Decoration
207
+ "\u5708\u70B9": { args: ["optional", "required"], node: "emphasis" },
208
+ "EmphasisMark": { alias: "\u5708\u70B9" },
209
+ "\u88C5\u9970": { args: ["optional", "required"], node: "decorate" },
210
+ "decorate": { alias: "\u88C5\u9970" },
211
+ "\u4E13\u540D\u53F7": { args: ["optional", "required"], node: "properName" },
212
+ "ProperNameMark": { alias: "\u4E13\u540D\u53F7" },
213
+ "\u4E66\u540D\u53F7": { args: ["optional", "required"], node: "bookTitle" },
214
+ "BookTitleMark": { alias: "\u4E66\u540D\u53F7" },
215
+ "\u4E0B\u5212\u7EBF": { alias: "\u4E13\u540D\u53F7" },
216
+ "Underline": { alias: "\u4E13\u540D\u53F7" },
217
+ "\u6CE2\u6D6A\u7EBF": { alias: "\u4E66\u540D\u53F7" },
218
+ "WavyUnderline": { alias: "\u4E66\u540D\u53F7" },
219
+ "\u53CD\u767D": { args: ["required"], node: "inverted" },
220
+ "inverted": { alias: "\u53CD\u767D" },
221
+ "\u516B\u89D2\u6846": { args: ["required"], node: "octagon" },
222
+ "octagon": { alias: "\u516B\u89D2\u6846" },
223
+ "\u5E26\u5708": { args: ["required"], node: "circled" },
224
+ "circled": { alias: "\u5E26\u5708" },
225
+ "\u53CD\u767D\u516B\u89D2\u6846": { args: ["required"], node: "invertedOctagon" },
226
+ "invertedOctagon": { alias: "\u53CD\u767D\u516B\u89D2\u6846" },
227
+ "\u6539": { args: ["required"], node: "fix" },
228
+ "fix": { alias: "\u6539" },
229
+ // Layout control
230
+ "\u7A7A\u683C": { args: ["optional"], node: "space" },
231
+ "Space": { alias: "\u7A7A\u683C" },
232
+ "\u8BBE\u7F6E\u7F29\u8FDB": { args: ["required"], node: "setIndent" },
233
+ "SetIndent": { alias: "\u8BBE\u7F6E\u7F29\u8FDB" },
234
+ "\u6362\u884C": { args: [], node: "columnBreak" },
235
+ // Taitou (抬头)
236
+ "\u62AC\u5934": { args: ["optional"], node: "taitou" },
237
+ "\u5E73\u62AC": { args: [], node: "taitou", defaultOpt: "0" },
238
+ "\u5355\u62AC": { args: [], node: "taitou", defaultOpt: "1" },
239
+ "\u53CC\u62AC": { args: [], node: "taitou", defaultOpt: "2" },
240
+ "\u4E09\u62AC": { args: [], node: "taitou", defaultOpt: "3" },
241
+ "\u632A\u62AC": { args: ["optional"], node: "nuotai" },
242
+ "\u7A7A\u62AC": { args: [], node: "nuotai", defaultOpt: "1" },
243
+ "\u76F8\u5BF9\u62AC\u5934": { args: ["optional"], node: "relativeTaitou" },
244
+ // Setup commands
245
+ "contentSetup": { args: ["required"], node: "setupCmd", setupType: "content" },
246
+ "pageSetup": { args: ["required"], node: "setupCmd", setupType: "page" },
247
+ "banxinSetup": { args: ["required"], node: "setupCmd", setupType: "banxin" },
248
+ "sidenodeSetup": { args: ["required"], node: "setupCmd", setupType: "sidenode" },
249
+ "jiazhuSetup": { args: ["required"], node: "setupCmd", setupType: "jiazhu" },
250
+ "pizhuSetup": { args: ["required"], node: "setupCmd", setupType: "pizhu" },
251
+ "meipiSetup": { args: ["required"], node: "setupCmd", setupType: "meipi" },
252
+ "gujiSetup": { args: ["required"], node: "setupCmd", setupType: "guji" },
253
+ "judouSetup": { args: ["required"], node: "setupCmd", setupType: "judou" },
254
+ // Judou
255
+ "\u53E5\u8BFB\u6A21\u5F0F": { args: ["optional"], node: "setupCmd", setupType: "judou-on" },
256
+ "JudouOn": { alias: "\u53E5\u8BFB\u6A21\u5F0F" },
257
+ "\u6B63\u5E38\u6807\u70B9\u6A21\u5F0F": { args: ["optional"], node: "setupCmd", setupType: "judou-off" },
258
+ "JudouOff": { alias: "\u6B63\u5E38\u6807\u70B9\u6A21\u5F0F" },
259
+ "\u65E0\u6807\u70B9\u6A21\u5F0F": { args: ["optional"], node: "setupCmd", setupType: "judou-none" },
260
+ "NonePunctuationMode": { alias: "\u65E0\u6807\u70B9\u6A21\u5F0F" },
261
+ // Ignored commands
262
+ "usepackage": { args: ["optional", "required"], ignore: true },
263
+ "RequirePackage": { alias: "usepackage" },
264
+ "setmainfont": { args: ["optional", "required"], ignore: true },
265
+ "pagestyle": { args: ["required"], ignore: true },
266
+ "noindent": { args: [], ignore: true },
267
+ "par": { args: [], ignore: true },
268
+ "relax": { args: [], ignore: true },
269
+ "ignorespaces": { args: [], ignore: true },
270
+ "definecolor": { args: ["required", "required", "required"], ignore: true },
271
+ "AddToHook": { args: ["required", "required"], ignore: true },
272
+ "\u7981\u7528\u5206\u9875\u88C1\u526A": { args: [], ignore: true },
273
+ "\u663E\u793A\u5750\u6807": { args: [], ignore: true },
274
+ "LtcDebugOn": { args: [], ignore: true },
275
+ "LtcDebugOff": { args: [], ignore: true },
276
+ // Seal stamp (simplified)
277
+ "\u5370\u7AE0": { args: ["optional", "required"], node: "stamp" }
278
+ };
279
+ var environmentRegistry = {
280
+ "document": { node: "body" },
281
+ "\u6B63\u6587": { node: "contentBlock" },
282
+ "BodyText": { alias: "\u6B63\u6587" },
283
+ "\u6BB5\u843D": { node: "paragraph", hasOptions: true },
284
+ "Paragraph": { alias: "\u6BB5\u843D" },
285
+ "\u5217\u8868": { node: "list" },
286
+ "\u5939\u6CE8\u73AF\u5883": { node: "jiazhu" },
287
+ "JiaZhuEnv": { alias: "\u5939\u6CE8\u73AF\u5883" }
288
+ };
289
+ function resolveCommand(name) {
290
+ let def = commandRegistry[name];
291
+ const visited = /* @__PURE__ */ new Set();
292
+ while (def && def.alias && !visited.has(def.alias)) {
293
+ visited.add(name);
294
+ name = def.alias;
295
+ def = commandRegistry[name];
296
+ }
297
+ return def || null;
298
+ }
299
+ function resolveEnvironment(name) {
300
+ let def = environmentRegistry[name];
301
+ const visited = /* @__PURE__ */ new Set();
302
+ while (def && def.alias && !visited.has(def.alias)) {
303
+ visited.add(name);
304
+ name = def.alias;
305
+ def = environmentRegistry[name];
306
+ }
307
+ return def || null;
308
+ }
309
+
310
+ // src/model/nodes.js
311
+ var NodeType = {
312
+ DOCUMENT: "document",
313
+ CONTENT_BLOCK: "contentBlock",
314
+ PARAGRAPH: "paragraph",
315
+ TEXT: "text",
316
+ NEWLINE: "newline",
317
+ JIAZHU: "jiazhu",
318
+ SIDENOTE: "sidenote",
319
+ MEIPI: "meipi",
320
+ PIZHU: "pizhu",
321
+ TEXTBOX: "textbox",
322
+ FILL_TEXTBOX: "fillTextbox",
323
+ SPACE: "space",
324
+ COLUMN_BREAK: "columnBreak",
325
+ TAITOU: "taitou",
326
+ NUOTAI: "nuotai",
327
+ SET_INDENT: "setIndent",
328
+ EMPHASIS: "emphasis",
329
+ PROPER_NAME: "properName",
330
+ BOOK_TITLE: "bookTitle",
331
+ INVERTED: "inverted",
332
+ OCTAGON: "octagon",
333
+ CIRCLED: "circled",
334
+ INVERTED_OCTAGON: "invertedOctagon",
335
+ FIX: "fix",
336
+ DECORATE: "decorate",
337
+ LIST: "list",
338
+ LIST_ITEM: "listItem",
339
+ SETUP: "setup",
340
+ STAMP: "stamp",
341
+ MATH: "math",
342
+ PARAGRAPH_BREAK: "paragraphBreak",
343
+ UNKNOWN: "unknown"
344
+ };
345
+ function createNode(type, props = {}) {
346
+ return { type, children: [], ...props };
347
+ }
348
+ function parseKeyValue(str) {
349
+ if (!str || !str.trim()) return {};
350
+ const result = {};
351
+ let depth = 0;
352
+ let currentKey = "";
353
+ let currentValue = "";
354
+ let inValue = false;
355
+ for (let i = 0; i < str.length; i++) {
356
+ const ch = str[i];
357
+ if (ch === "{") depth++;
358
+ if (ch === "}") depth--;
359
+ if (depth === 0 && ch === ",") {
360
+ if (currentKey.trim()) {
361
+ result[currentKey.trim()] = inValue ? currentValue.trim() : "true";
362
+ }
363
+ currentKey = "";
364
+ currentValue = "";
365
+ inValue = false;
366
+ continue;
367
+ } else if (depth === 0 && ch === "=" && !inValue) {
368
+ inValue = true;
369
+ } else if (inValue) {
370
+ currentValue += ch;
371
+ } else {
372
+ currentKey += ch;
373
+ }
374
+ }
375
+ if (currentKey.trim()) {
376
+ if (inValue) {
377
+ result[currentKey.trim()] = currentValue.trim();
378
+ } else {
379
+ result[currentKey.trim()] = "true";
380
+ }
381
+ }
382
+ return result;
383
+ }
384
+
385
+ // src/parser/parser.js
386
+ var Parser = class _Parser {
387
+ constructor(tokens) {
388
+ this.tokens = tokens;
389
+ this.pos = 0;
390
+ this.warnings = [];
391
+ }
392
+ peek() {
393
+ if (this.pos >= this.tokens.length) return { type: TokenType.EOF, value: "" };
394
+ return this.tokens[this.pos];
395
+ }
396
+ advance() {
397
+ const token = this.tokens[this.pos];
398
+ this.pos++;
399
+ return token;
400
+ }
401
+ expect(type) {
402
+ const token = this.peek();
403
+ if (token.type !== type) {
404
+ this.warnings.push(`Expected ${type} but got ${token.type} ("${token.value}") at token ${this.pos}`);
405
+ return null;
406
+ }
407
+ return this.advance();
408
+ }
409
+ /**
410
+ * Read content inside { ... }, handling nested braces.
411
+ * Returns the raw text content.
412
+ */
413
+ readBraceGroup() {
414
+ if (this.peek().type !== TokenType.OPEN_BRACE) return "";
415
+ this.advance();
416
+ let content = "";
417
+ let depth = 1;
418
+ while (this.pos < this.tokens.length && depth > 0) {
419
+ const token = this.peek();
420
+ if (token.type === TokenType.OPEN_BRACE) {
421
+ depth++;
422
+ content += "{";
423
+ this.advance();
424
+ } else if (token.type === TokenType.CLOSE_BRACE) {
425
+ depth--;
426
+ if (depth > 0) content += "}";
427
+ this.advance();
428
+ } else if (token.type === TokenType.EOF) {
429
+ break;
430
+ } else {
431
+ content += token.value;
432
+ this.advance();
433
+ }
434
+ }
435
+ return content;
436
+ }
437
+ /**
438
+ * Read content inside [ ... ], handling nested brackets.
439
+ * Returns the raw text content or null if no bracket group.
440
+ */
441
+ readBracketGroup() {
442
+ if (this.peek().type !== TokenType.OPEN_BRACKET) return null;
443
+ this.advance();
444
+ let content = "";
445
+ let depth = 1;
446
+ while (this.pos < this.tokens.length && depth > 0) {
447
+ const token = this.peek();
448
+ if (token.type === TokenType.OPEN_BRACKET) {
449
+ depth++;
450
+ content += "[";
451
+ this.advance();
452
+ } else if (token.type === TokenType.CLOSE_BRACKET) {
453
+ depth--;
454
+ if (depth > 0) content += "]";
455
+ this.advance();
456
+ } else if (token.type === TokenType.EOF) {
457
+ break;
458
+ } else {
459
+ content += token.value;
460
+ this.advance();
461
+ }
462
+ }
463
+ return content;
464
+ }
465
+ /**
466
+ * Parse the content of a brace group as child nodes (recursive).
467
+ */
468
+ readBraceGroupAsNodes() {
469
+ if (this.peek().type !== TokenType.OPEN_BRACE) return [];
470
+ this.advance();
471
+ const children = [];
472
+ while (this.pos < this.tokens.length) {
473
+ const token = this.peek();
474
+ if (token.type === TokenType.CLOSE_BRACE) {
475
+ this.advance();
476
+ break;
477
+ }
478
+ if (token.type === TokenType.EOF) break;
479
+ const node = this.parseToken();
480
+ if (node) children.push(node);
481
+ }
482
+ return children;
483
+ }
484
+ /**
485
+ * Parse command arguments according to its definition.
486
+ */
487
+ parseCommandArgs(def) {
488
+ let optionalArg = null;
489
+ const requiredArgs = [];
490
+ if (!def.args) return { optionalArg, requiredArgs };
491
+ for (const argType of def.args) {
492
+ if (argType === "optional") {
493
+ optionalArg = this.readBracketGroup();
494
+ } else if (argType === "required") {
495
+ const content = this.readBraceGroup();
496
+ requiredArgs.push(content);
497
+ }
498
+ }
499
+ return { optionalArg, requiredArgs };
500
+ }
501
+ /**
502
+ * Main entry point: parse the full document.
503
+ */
504
+ parse() {
505
+ const doc = createNode(NodeType.DOCUMENT);
506
+ doc.template = "";
507
+ doc.documentClass = "";
508
+ doc.title = "";
509
+ doc.chapter = "";
510
+ doc.setupCommands = [];
511
+ while (this.pos < this.tokens.length) {
512
+ const token = this.peek();
513
+ if (token.type === TokenType.EOF) break;
514
+ const node = this.parseToken(doc);
515
+ if (node) {
516
+ doc.children.push(node);
517
+ }
518
+ }
519
+ return doc;
520
+ }
521
+ /**
522
+ * Parse a single token and return a node (or null).
523
+ * @param {object} doc - The document node (for storing metadata)
524
+ */
525
+ parseToken(doc) {
526
+ const token = this.peek();
527
+ switch (token.type) {
528
+ case TokenType.TEXT:
529
+ this.advance();
530
+ return createNode(NodeType.TEXT, { value: token.value });
531
+ case TokenType.NEWLINE:
532
+ this.advance();
533
+ return createNode(NodeType.NEWLINE);
534
+ case TokenType.MATH:
535
+ this.advance();
536
+ return createNode(NodeType.MATH, { value: token.value });
537
+ case TokenType.PARAGRAPH_BREAK:
538
+ this.advance();
539
+ return createNode(NodeType.PARAGRAPH_BREAK);
540
+ case TokenType.COMMAND:
541
+ return this.parseCommand(doc);
542
+ case TokenType.BEGIN:
543
+ return this.parseEnvironment(doc);
544
+ case TokenType.END:
545
+ return null;
546
+ case TokenType.OPEN_BRACE:
547
+ return this.parseBareGroup();
548
+ default:
549
+ this.advance();
550
+ return null;
551
+ }
552
+ }
553
+ parseBareGroup() {
554
+ const children = this.readBraceGroupAsNodes();
555
+ if (children.length === 0) return null;
556
+ if (children.length === 1) return children[0];
557
+ const group = createNode("group");
558
+ group.children = children;
559
+ return group;
560
+ }
561
+ parseCommand(doc) {
562
+ const token = this.advance();
563
+ const name = token.value;
564
+ const def = resolveCommand(name);
565
+ if (name === "documentclass") {
566
+ const optArg = this.readBracketGroup();
567
+ const reqArg = this.readBraceGroup();
568
+ if (doc) {
569
+ doc.documentClass = reqArg;
570
+ doc.template = optArg || "";
571
+ }
572
+ return null;
573
+ }
574
+ if (name === "title") {
575
+ const content = this.readBraceGroup();
576
+ if (doc) doc.title = content;
577
+ return null;
578
+ }
579
+ if (name === "chapter") {
580
+ const content = this.readBraceGroup();
581
+ if (doc) doc.chapter = content;
582
+ return null;
583
+ }
584
+ if (name === "item") {
585
+ return createNode(NodeType.LIST_ITEM);
586
+ }
587
+ if (!def) {
588
+ this.warnings.push(`Unknown command: \\${name} at token ${this.pos}`);
589
+ let content = "";
590
+ if (this.peek().type === TokenType.OPEN_BRACE) {
591
+ content = this.readBraceGroup();
592
+ }
593
+ return createNode(NodeType.TEXT, { value: content });
594
+ }
595
+ if (def.ignore) {
596
+ this.parseCommandArgs(def);
597
+ return null;
598
+ }
599
+ if (def.node === "setupCmd") {
600
+ const { optionalArg: optionalArg2, requiredArgs: requiredArgs2 } = this.parseCommandArgs(def);
601
+ const params = parseKeyValue(requiredArgs2[0] || optionalArg2 || "");
602
+ const setupNode = createNode(NodeType.SETUP, {
603
+ setupType: def.setupType,
604
+ params
605
+ });
606
+ if (doc) doc.setupCommands.push(setupNode);
607
+ return null;
608
+ }
609
+ const { optionalArg, requiredArgs } = this.parseCommandArgs(def);
610
+ const options = parseKeyValue(optionalArg || "");
611
+ const nodeType = this.mapNodeType(def.node);
612
+ const node = createNode(nodeType, { options });
613
+ if (def.defaultOpt !== void 0 && optionalArg === null) {
614
+ node.options = { value: def.defaultOpt };
615
+ }
616
+ if (requiredArgs.length > 0 && def.node !== "stamp") {
617
+ const { Tokenizer: Tokenizer2 } = require_tokenizer();
618
+ const innerTokens = new Tokenizer2(requiredArgs[0]).tokenize();
619
+ const innerParser = new _Parser(innerTokens);
620
+ while (innerParser.pos < innerParser.tokens.length) {
621
+ const t = innerParser.peek();
622
+ if (t.type === TokenType.EOF) break;
623
+ const child = innerParser.parseToken();
624
+ if (child) node.children.push(child);
625
+ }
626
+ }
627
+ if (def.node === "space" || def.node === "taitou" || def.node === "nuotai" || def.node === "relativeTaitou") {
628
+ node.value = optionalArg || def.defaultOpt || "1";
629
+ }
630
+ if (def.node === "setIndent") {
631
+ node.value = requiredArgs[0] || "0";
632
+ }
633
+ if (def.node === "stamp") {
634
+ node.options = parseKeyValue(optionalArg || "");
635
+ node.src = requiredArgs[0] || "";
636
+ }
637
+ return node;
638
+ }
639
+ parseEnvironment(doc) {
640
+ this.advance();
641
+ const envName = this.readBraceGroup();
642
+ const def = resolveEnvironment(envName);
643
+ if (!def) {
644
+ this.warnings.push(`Unknown environment: ${envName}`);
645
+ const children = this.parseUntilEnd(envName, doc);
646
+ const node2 = createNode(NodeType.UNKNOWN, { envName });
647
+ node2.children = children;
648
+ return node2;
649
+ }
650
+ let options = {};
651
+ if (def.hasOptions) {
652
+ const optArg = this.readBracketGroup();
653
+ if (optArg) options = parseKeyValue(optArg);
654
+ }
655
+ const nodeType = this.mapNodeType(def.node);
656
+ const node = createNode(nodeType, { options });
657
+ if (def.node === "body") {
658
+ node.children = this.parseUntilEnd(envName, doc);
659
+ return node;
660
+ }
661
+ node.children = this.parseUntilEnd(envName, doc);
662
+ if (def.node === "list") {
663
+ node.children = this.groupListItems(node.children);
664
+ }
665
+ return node;
666
+ }
667
+ /**
668
+ * Parse tokens until we encounter \end{envName}.
669
+ */
670
+ parseUntilEnd(envName, doc) {
671
+ const children = [];
672
+ while (this.pos < this.tokens.length) {
673
+ const token = this.peek();
674
+ if (token.type === TokenType.EOF) {
675
+ this.warnings.push(`Unclosed environment: ${envName}`);
676
+ break;
677
+ }
678
+ if (token.type === TokenType.END) {
679
+ this.advance();
680
+ const endName = this.readBraceGroup();
681
+ if (endName === envName) {
682
+ break;
683
+ }
684
+ this.warnings.push(`Mismatched \\end{${endName}}, expected \\end{${envName}}`);
685
+ continue;
686
+ }
687
+ const node = this.parseToken(doc);
688
+ if (node) children.push(node);
689
+ }
690
+ return children;
691
+ }
692
+ /**
693
+ * Group list items: \item separators become container nodes.
694
+ */
695
+ groupListItems(children) {
696
+ const items = [];
697
+ let currentItem = null;
698
+ for (const child of children) {
699
+ if (child.type === NodeType.LIST_ITEM) {
700
+ currentItem = createNode(NodeType.LIST_ITEM);
701
+ items.push(currentItem);
702
+ } else {
703
+ if (!currentItem) {
704
+ currentItem = createNode(NodeType.LIST_ITEM);
705
+ items.push(currentItem);
706
+ }
707
+ currentItem.children.push(child);
708
+ }
709
+ }
710
+ return items;
711
+ }
712
+ mapNodeType(nodeName) {
713
+ const map = {
714
+ "contentBlock": NodeType.CONTENT_BLOCK,
715
+ "paragraph": NodeType.PARAGRAPH,
716
+ "jiazhu": NodeType.JIAZHU,
717
+ "sidenote": NodeType.SIDENOTE,
718
+ "meipi": NodeType.MEIPI,
719
+ "pizhu": NodeType.PIZHU,
720
+ "textbox": NodeType.TEXTBOX,
721
+ "fillTextbox": NodeType.FILL_TEXTBOX,
722
+ "space": NodeType.SPACE,
723
+ "columnBreak": NodeType.COLUMN_BREAK,
724
+ "taitou": NodeType.TAITOU,
725
+ "nuotai": NodeType.NUOTAI,
726
+ "setIndent": NodeType.SET_INDENT,
727
+ "emphasis": NodeType.EMPHASIS,
728
+ "properName": NodeType.PROPER_NAME,
729
+ "bookTitle": NodeType.BOOK_TITLE,
730
+ "inverted": NodeType.INVERTED,
731
+ "octagon": NodeType.OCTAGON,
732
+ "circled": NodeType.CIRCLED,
733
+ "invertedOctagon": NodeType.INVERTED_OCTAGON,
734
+ "fix": NodeType.FIX,
735
+ "decorate": NodeType.DECORATE,
736
+ "list": NodeType.LIST,
737
+ "body": "body",
738
+ "stamp": NodeType.STAMP,
739
+ "relativeTaitou": NodeType.TAITOU
740
+ };
741
+ return map[nodeName] || NodeType.UNKNOWN;
742
+ }
743
+ };
744
+ var _Tokenizer = null;
745
+ function require_tokenizer() {
746
+ if (!_Tokenizer) {
747
+ _Tokenizer = { Tokenizer: null };
748
+ }
749
+ return _Tokenizer;
750
+ }
751
+ function setTokenizer(TokenizerClass) {
752
+ if (!_Tokenizer) _Tokenizer = {};
753
+ _Tokenizer.Tokenizer = TokenizerClass;
754
+ }
755
+
756
+ // src/parser/index.js
757
+ setTokenizer(Tokenizer);
758
+ function parse(source) {
759
+ const tokenizer = new Tokenizer(source);
760
+ const tokens = tokenizer.tokenize();
761
+ const parser = new Parser(tokens);
762
+ const ast = parser.parse();
763
+ return { ast, warnings: parser.warnings };
764
+ }
765
+
766
+ // src/config/templates.js
767
+ var templateCSSMap = {
768
+ "\u56DB\u5E93\u5168\u4E66": "siku-quanshu",
769
+ "\u56DB\u5EAB\u5168\u66F8": "siku-quanshu",
770
+ "\u56DB\u5E93\u5168\u4E66\u5F69\u8272": "siku-quanshu-colored",
771
+ "\u56DB\u5EAB\u5168\u66F8\u5F69\u8272": "siku-quanshu-colored",
772
+ "\u7EA2\u697C\u68A6\u7532\u620C\u672C": "honglou",
773
+ "\u7D05\u6A13\u5922\u7532\u620C\u672C": "honglou",
774
+ "\u6781\u7B80": "minimal",
775
+ "\u6975\u7C21": "minimal",
776
+ "default": "siku-quanshu"
777
+ };
778
+ var templateGridConfig = {
779
+ "siku-quanshu": { nRows: 21, nCols: 8 },
780
+ "siku-quanshu-colored": { nRows: 21, nCols: 8 },
781
+ "honglou": { nRows: 20, nCols: 9 },
782
+ "minimal": { nRows: 21, nCols: 8 }
783
+ };
784
+ function resolveTemplateId(ast) {
785
+ let templateId = templateCSSMap[ast.template] || "siku-quanshu";
786
+ for (const cmd of ast.setupCommands || []) {
787
+ if (cmd.setupType === "guji" && cmd.params?.template) {
788
+ const override = templateCSSMap[cmd.params.template];
789
+ if (override) templateId = override;
790
+ }
791
+ }
792
+ return templateId;
793
+ }
794
+ function getGridConfig(templateId) {
795
+ return templateGridConfig[templateId] || { nRows: 21, nCols: 8 };
796
+ }
797
+
798
+ // src/layout/grid-layout.js
799
+ function getPlainText(children) {
800
+ let text = "";
801
+ for (const child of children) {
802
+ if (child.type === NodeType.TEXT) {
803
+ text += child.value;
804
+ } else if (child.children && child.children.length > 0) {
805
+ text += getPlainText(child.children);
806
+ }
807
+ }
808
+ return text;
809
+ }
810
+ function splitJiazhu(text, align = "outward") {
811
+ const chars = [...text];
812
+ if (chars.length === 0) return { col1: "", col2: "" };
813
+ if (chars.length === 1) return { col1: chars[0], col2: "" };
814
+ const mid = align === "inward" ? Math.floor(chars.length / 2) : Math.ceil(chars.length / 2);
815
+ return {
816
+ col1: chars.slice(0, mid).join(""),
817
+ col2: chars.slice(mid).join("")
818
+ };
819
+ }
820
+ function splitJiazhuMulti(text, maxCharsPerCol = 20, align = "outward", firstMaxPerCol = 0) {
821
+ const first = firstMaxPerCol > 0 ? firstMaxPerCol : maxCharsPerCol;
822
+ const chars = [...text];
823
+ const firstChunkSize = first * 2;
824
+ if (chars.length <= firstChunkSize) {
825
+ return [splitJiazhu(text, align)];
826
+ }
827
+ const segments = [];
828
+ const firstChunk = chars.slice(0, firstChunkSize).join("");
829
+ segments.push(splitJiazhu(firstChunk, align));
830
+ const fullChunkSize = maxCharsPerCol * 2;
831
+ for (let i = firstChunkSize; i < chars.length; i += fullChunkSize) {
832
+ const chunk = chars.slice(i, i + fullChunkSize).join("");
833
+ segments.push(splitJiazhu(chunk, align));
834
+ }
835
+ return segments;
836
+ }
837
+ var LayoutMarker = {
838
+ PARAGRAPH_START: "_paragraphStart",
839
+ PARAGRAPH_END: "_paragraphEnd",
840
+ LIST_START: "_listStart",
841
+ LIST_END: "_listEnd",
842
+ LIST_ITEM_START: "_listItemStart",
843
+ LIST_ITEM_END: "_listItemEnd"
844
+ };
845
+ function newPage() {
846
+ return { items: [], floats: [], halfBoundary: null };
847
+ }
848
+ var GridLayoutEngine = class {
849
+ /**
850
+ * @param {number} nRows Chars per column
851
+ * @param {number} nCols Columns per half-page
852
+ */
853
+ constructor(nRows, nCols) {
854
+ this.nRows = nRows;
855
+ this.nCols = nCols;
856
+ this.colsPerSpread = 2 * nCols;
857
+ this.currentCol = 0;
858
+ this.currentRow = 0;
859
+ this.currentIndent = 0;
860
+ this.pages = [newPage()];
861
+ }
862
+ get currentPage() {
863
+ return this.pages[this.pages.length - 1];
864
+ }
865
+ get effectiveRows() {
866
+ return this.nRows - this.currentIndent;
867
+ }
868
+ /**
869
+ * Check and mark the half-page boundary when crossing from right to left.
870
+ */
871
+ checkHalfBoundary() {
872
+ if (this.currentPage.halfBoundary === null && this.currentCol >= this.nCols) {
873
+ this.currentPage.halfBoundary = this.currentPage.items.length;
874
+ }
875
+ }
876
+ /**
877
+ * Advance to the next column. Triggers page break if needed.
878
+ */
879
+ advanceColumn() {
880
+ this.currentCol++;
881
+ this.currentRow = 0;
882
+ this.checkHalfBoundary();
883
+ if (this.currentCol >= this.colsPerSpread) {
884
+ this.newPageBreak();
885
+ }
886
+ }
887
+ /**
888
+ * Create a new page and reset cursor.
889
+ */
890
+ newPageBreak() {
891
+ if (this.currentPage.halfBoundary === null) {
892
+ this.currentPage.halfBoundary = this.currentPage.items.length;
893
+ }
894
+ this.pages.push(newPage());
895
+ this.currentCol = 0;
896
+ this.currentRow = 0;
897
+ }
898
+ /**
899
+ * Place a node at the current cursor position.
900
+ */
901
+ placeItem(node, extra = {}) {
902
+ this.checkHalfBoundary();
903
+ this.currentPage.items.push({
904
+ node,
905
+ col: this.currentCol,
906
+ row: this.currentRow,
907
+ indent: this.currentIndent,
908
+ ...extra
909
+ });
910
+ }
911
+ /**
912
+ * Place a layout marker (paragraph start/end, list start/end, etc.).
913
+ */
914
+ placeMarker(markerType, data = {}) {
915
+ this.checkHalfBoundary();
916
+ this.currentPage.items.push({
917
+ node: { type: markerType },
918
+ col: this.currentCol,
919
+ row: this.currentRow,
920
+ indent: this.currentIndent,
921
+ ...data
922
+ });
923
+ }
924
+ /**
925
+ * Walk a list of AST child nodes.
926
+ */
927
+ walkChildren(children) {
928
+ for (const child of children) {
929
+ this.walkNode(child);
930
+ }
931
+ }
932
+ /**
933
+ * Advance cursor by a given number of rows, wrapping columns as needed.
934
+ * Preserves the remainder correctly across column and page breaks.
935
+ */
936
+ advanceRows(count) {
937
+ this.currentRow += count;
938
+ while (this.currentRow >= this.effectiveRows) {
939
+ this.currentRow -= this.effectiveRows;
940
+ this.currentCol++;
941
+ this.checkHalfBoundary();
942
+ if (this.currentCol >= this.colsPerSpread) {
943
+ const remainder = this.currentRow;
944
+ this.newPageBreak();
945
+ this.currentRow = remainder;
946
+ }
947
+ }
948
+ }
949
+ /**
950
+ * Walk a single AST node and place it on the grid.
951
+ */
952
+ walkNode(node) {
953
+ if (!node) return;
954
+ switch (node.type) {
955
+ case "body":
956
+ this.walkChildren(node.children);
957
+ break;
958
+ case NodeType.CONTENT_BLOCK:
959
+ this.walkContentBlock(node);
960
+ break;
961
+ case NodeType.PARAGRAPH:
962
+ this.walkParagraph(node);
963
+ break;
964
+ case NodeType.TEXT:
965
+ this.walkText(node);
966
+ break;
967
+ case NodeType.NEWLINE:
968
+ case NodeType.PARAGRAPH_BREAK:
969
+ case NodeType.COLUMN_BREAK:
970
+ this.placeItem(node);
971
+ this.advanceColumn();
972
+ break;
973
+ case NodeType.JIAZHU:
974
+ this.walkJiazhu(node);
975
+ break;
976
+ case NodeType.SPACE:
977
+ case NodeType.NUOTAI: {
978
+ const count = parseInt(node.value, 10) || 1;
979
+ this.placeItem(node);
980
+ this.advanceRows(count);
981
+ break;
982
+ }
983
+ case NodeType.TAITOU: {
984
+ this.advanceColumn();
985
+ const level = parseInt(node.value, 10) || 0;
986
+ this.currentRow = level;
987
+ this.placeItem(node);
988
+ break;
989
+ }
990
+ case NodeType.LIST:
991
+ this.walkList(node);
992
+ break;
993
+ case NodeType.LIST_ITEM:
994
+ this.walkListItem(node);
995
+ break;
996
+ // Floating elements — don't consume grid space
997
+ case NodeType.MEIPI:
998
+ case NodeType.PIZHU:
999
+ case NodeType.STAMP:
1000
+ this.currentPage.floats.push(node);
1001
+ break;
1002
+ // Decorative wrappers — place as single item, count text for cursor
1003
+ case NodeType.EMPHASIS:
1004
+ case NodeType.PROPER_NAME:
1005
+ case NodeType.BOOK_TITLE:
1006
+ case NodeType.INVERTED:
1007
+ case NodeType.OCTAGON:
1008
+ case NodeType.CIRCLED:
1009
+ case NodeType.INVERTED_OCTAGON:
1010
+ case NodeType.FIX:
1011
+ case NodeType.DECORATE:
1012
+ this.placeItem(node);
1013
+ this.advanceRowsByNodeText(node);
1014
+ break;
1015
+ case NodeType.SIDENOTE:
1016
+ this.placeItem(node);
1017
+ break;
1018
+ case NodeType.TEXTBOX:
1019
+ case NodeType.FILL_TEXTBOX: {
1020
+ this.placeItem(node);
1021
+ const height = parseInt(node.options?.height || node.options?.value || "1", 10);
1022
+ this.advanceRows(height);
1023
+ break;
1024
+ }
1025
+ case NodeType.MATH:
1026
+ case NodeType.SET_INDENT:
1027
+ this.placeItem(node);
1028
+ break;
1029
+ default:
1030
+ if (node.children && node.children.length > 0) {
1031
+ this.walkChildren(node.children);
1032
+ }
1033
+ break;
1034
+ }
1035
+ }
1036
+ /**
1037
+ * Walk content block — separate floats from inline content.
1038
+ */
1039
+ walkContentBlock(node) {
1040
+ for (const child of node.children) {
1041
+ if (child.type === NodeType.MEIPI || child.type === NodeType.PIZHU || child.type === NodeType.STAMP) {
1042
+ this.currentPage.floats.push(child);
1043
+ } else {
1044
+ this.walkNode(child);
1045
+ }
1046
+ }
1047
+ }
1048
+ /**
1049
+ * Walk a paragraph node.
1050
+ * Emits start/end markers so the renderer can wrap the content with indent.
1051
+ * Walks children individually so they can span page boundaries.
1052
+ */
1053
+ walkParagraph(node) {
1054
+ const indent = parseInt(node.options?.indent || "0", 10);
1055
+ const prevIndent = this.currentIndent;
1056
+ this.currentIndent = indent;
1057
+ this.placeMarker(LayoutMarker.PARAGRAPH_START, { paragraphNode: node });
1058
+ this.walkChildren(node.children);
1059
+ this.placeMarker(LayoutMarker.PARAGRAPH_END);
1060
+ this.currentIndent = prevIndent;
1061
+ }
1062
+ /**
1063
+ * Walk LIST node — emits start/end markers and walks children.
1064
+ * Tracks whether first item needs advanceColumn or not.
1065
+ */
1066
+ walkList(node) {
1067
+ this.placeMarker(LayoutMarker.LIST_START);
1068
+ let first = true;
1069
+ for (const child of node.children) {
1070
+ if (child.type === NodeType.LIST_ITEM) {
1071
+ this.walkListItem(child, first);
1072
+ first = false;
1073
+ } else {
1074
+ this.walkNode(child);
1075
+ }
1076
+ }
1077
+ this.placeMarker(LayoutMarker.LIST_END);
1078
+ }
1079
+ /**
1080
+ * Walk LIST_ITEM node — emits markers. Advances column for non-first items.
1081
+ */
1082
+ walkListItem(node, isFirst = false) {
1083
+ if (!isFirst) {
1084
+ this.advanceColumn();
1085
+ }
1086
+ this.placeMarker(LayoutMarker.LIST_ITEM_START);
1087
+ this.walkChildren(node.children);
1088
+ this.placeMarker(LayoutMarker.LIST_ITEM_END);
1089
+ }
1090
+ /**
1091
+ * Walk TEXT node — advance cursor row by character count.
1092
+ */
1093
+ walkText(node) {
1094
+ const chars = [...node.value || ""];
1095
+ this.placeItem(node);
1096
+ this.advanceRows(chars.length);
1097
+ }
1098
+ /**
1099
+ * Advance cursor rows by counting text in a node (for compound nodes).
1100
+ */
1101
+ advanceRowsByNodeText(node) {
1102
+ const text = getPlainText(node.children || []);
1103
+ const len = [...text].length;
1104
+ this.advanceRows(len);
1105
+ }
1106
+ /**
1107
+ * Walk jiazhu node. Pre-compute segments based on remaining column space.
1108
+ * Each segment is placed as a separate item so page breaks work correctly.
1109
+ */
1110
+ walkJiazhu(node) {
1111
+ const hasComplexChildren = node.children.some((c) => c.type !== NodeType.TEXT);
1112
+ const text = getPlainText(node.children);
1113
+ const align = node.options?.align || "outward";
1114
+ const maxPerCol = this.effectiveRows;
1115
+ const remaining = maxPerCol - this.currentRow;
1116
+ const firstMax = remaining > 0 && remaining < maxPerCol ? remaining : maxPerCol;
1117
+ if (hasComplexChildren) {
1118
+ this.placeItem(node, { jiazhuSegments: null });
1119
+ const totalChars = [...text].length;
1120
+ this.advanceRows(Math.ceil(totalChars / 2));
1121
+ return;
1122
+ }
1123
+ const jiazhuSegments = splitJiazhuMulti(text, maxPerCol, align, firstMax);
1124
+ if (jiazhuSegments.length <= 1) {
1125
+ this.placeItem(node, { jiazhuSegments });
1126
+ const totalChars = [...text].length;
1127
+ this.advanceRows(Math.ceil(totalChars / 2));
1128
+ return;
1129
+ }
1130
+ this.placeItem(node, {
1131
+ jiazhuSegments: [jiazhuSegments[0]],
1132
+ jiazhuSegmentIndex: 0,
1133
+ jiazhuTotalSegments: jiazhuSegments.length
1134
+ });
1135
+ this.advanceRows(firstMax);
1136
+ for (let i = 1; i < jiazhuSegments.length; i++) {
1137
+ const seg = jiazhuSegments[i];
1138
+ const segRows = Math.max([...seg.col1].length, [...seg.col2].length);
1139
+ this.placeItem(node, {
1140
+ jiazhuSegments: [seg],
1141
+ jiazhuSegmentIndex: i,
1142
+ jiazhuTotalSegments: jiazhuSegments.length
1143
+ });
1144
+ this.advanceRows(segRows);
1145
+ }
1146
+ }
1147
+ };
1148
+ function layout(ast) {
1149
+ const templateId = resolveTemplateId(ast);
1150
+ const { nRows, nCols } = getGridConfig(templateId);
1151
+ const engine = new GridLayoutEngine(nRows, nCols);
1152
+ for (const child of ast.children) {
1153
+ if (child.type === "body") {
1154
+ engine.walkNode(child);
1155
+ }
1156
+ }
1157
+ const lastPage = engine.currentPage;
1158
+ if (lastPage.halfBoundary === null) {
1159
+ lastPage.halfBoundary = lastPage.items.length;
1160
+ }
1161
+ const meta = {
1162
+ title: ast.title || "",
1163
+ chapter: ast.chapter || "",
1164
+ setupCommands: ast.setupCommands || []
1165
+ };
1166
+ return {
1167
+ pages: engine.pages,
1168
+ gridConfig: { nRows, nCols },
1169
+ templateId,
1170
+ meta
1171
+ };
1172
+ }
1173
+
1174
+ // src/renderer/html-renderer.js
1175
+ var setupParamMap = {
1176
+ content: {
1177
+ "font-size": "--wtc-font-size",
1178
+ "line-height": "--wtc-line-height",
1179
+ "letter-spacing": "--wtc-letter-spacing",
1180
+ "font-color": "--wtc-font-color",
1181
+ "border-color": "--wtc-border-color",
1182
+ "border-thickness": "--wtc-border-thickness"
1183
+ },
1184
+ page: {
1185
+ "page-width": "--wtc-page-width",
1186
+ "page-height": "--wtc-page-height",
1187
+ "margin-top": "--wtc-margin-top",
1188
+ "margin-bottom": "--wtc-margin-bottom",
1189
+ "margin-left": "--wtc-margin-left",
1190
+ "margin-right": "--wtc-margin-right",
1191
+ "background": "--wtc-page-background"
1192
+ },
1193
+ banxin: {
1194
+ "width": "--wtc-banxin-width",
1195
+ "font-size": "--wtc-banxin-font-size"
1196
+ },
1197
+ jiazhu: {
1198
+ "font-size": "--wtc-jiazhu-font-size",
1199
+ "color": "--wtc-jiazhu-color",
1200
+ "line-height": "--wtc-jiazhu-line-height",
1201
+ "gap": "--wtc-jiazhu-gap"
1202
+ },
1203
+ sidenode: {
1204
+ "font-size": "--wtc-sidenote-font-size",
1205
+ "color": "--wtc-sidenote-color"
1206
+ },
1207
+ meipi: {
1208
+ "font-size": "--wtc-meipi-font-size",
1209
+ "color": "--wtc-meipi-color"
1210
+ },
1211
+ pizhu: {
1212
+ "font-size": "--wtc-pizhu-font-size",
1213
+ "color": "--wtc-pizhu-color"
1214
+ }
1215
+ };
1216
+ function escapeHTML(str) {
1217
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1218
+ }
1219
+ var HTMLRenderer = class {
1220
+ constructor(ast) {
1221
+ this.ast = ast;
1222
+ this.templateId = resolveTemplateId(ast);
1223
+ this.meipiCount = 0;
1224
+ const grid = getGridConfig(this.templateId);
1225
+ this.nRows = grid.nRows;
1226
+ this.nCols = grid.nCols;
1227
+ this.currentIndent = 0;
1228
+ this.colPos = 0;
1229
+ }
1230
+ /**
1231
+ * Collect CSS variable overrides from setup commands.
1232
+ */
1233
+ getSetupStylesFromCommands(setupCommands) {
1234
+ const overrides = [];
1235
+ for (const cmd of setupCommands || []) {
1236
+ const mapping = setupParamMap[cmd.setupType];
1237
+ if (!mapping || !cmd.params) continue;
1238
+ for (const [param, value] of Object.entries(cmd.params)) {
1239
+ const cssVar = mapping[param];
1240
+ if (cssVar) {
1241
+ overrides.push(`${cssVar}: ${value}`);
1242
+ }
1243
+ }
1244
+ }
1245
+ return overrides.length > 0 ? ` style="${overrides.join("; ")}"` : "";
1246
+ }
1247
+ getSetupStyles() {
1248
+ return this.getSetupStylesFromCommands(this.ast.setupCommands);
1249
+ }
1250
+ // =====================================================================
1251
+ // Legacy render() — walks AST directly (kept for backward compat)
1252
+ // =====================================================================
1253
+ render() {
1254
+ let html = "";
1255
+ for (const child of this.ast.children) {
1256
+ html += this.renderNode(child);
1257
+ }
1258
+ return html;
1259
+ }
1260
+ renderPage() {
1261
+ const content = this.render();
1262
+ return `<!DOCTYPE html>
1263
+ <html lang="zh">
1264
+ <head>
1265
+ <meta charset="UTF-8">
1266
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1267
+ <title>${escapeHTML(this.ast.title || "WebTeX-CN")}</title>
1268
+ <link rel="stylesheet" href="base.css">
1269
+ </head>
1270
+ <body>
1271
+ <div class="wtc-page" data-template="${this.templateId}">
1272
+ ${content}
1273
+ </div>
1274
+ </body>
1275
+ </html>`;
1276
+ }
1277
+ // =====================================================================
1278
+ // New layout-based render pipeline
1279
+ // =====================================================================
1280
+ /**
1281
+ * Render a LayoutResult into multi-page HTML.
1282
+ * Each page becomes one wtc-page div with a complete spread.
1283
+ *
1284
+ * @param {object} layoutResult Output of layout()
1285
+ * @returns {string[]} Array of page HTML strings (one per page)
1286
+ */
1287
+ renderFromLayout(layoutResult) {
1288
+ const setupStyles = this.getSetupStylesFromCommands(layoutResult.meta.setupCommands);
1289
+ const banxin = this.renderBanxinFromMeta(layoutResult.meta);
1290
+ let carryStack = [];
1291
+ return layoutResult.pages.map((page) => {
1292
+ const boundary = page.halfBoundary ?? page.items.length;
1293
+ const rightItems = page.items.slice(0, boundary);
1294
+ const leftItems = page.items.slice(boundary);
1295
+ const right = this.renderLayoutItems(rightItems, carryStack);
1296
+ const left = this.renderLayoutItems(leftItems, right.openStack);
1297
+ carryStack = left.openStack;
1298
+ const rightHTML = right.html;
1299
+ const leftHTML = left.html;
1300
+ const floatsHTML = page.floats.map((f) => this.renderNode(f)).join("\n");
1301
+ return `<div class="wtc-spread"${setupStyles}>
1302
+ ${floatsHTML}<div class="wtc-half-page wtc-half-right"><div class="wtc-content-border"><div class="wtc-content">${rightHTML}</div></div></div>${banxin}<div class="wtc-half-page wtc-half-left"><div class="wtc-content-border"><div class="wtc-content">${leftHTML}</div></div></div>
1303
+ </div>`;
1304
+ });
1305
+ }
1306
+ /**
1307
+ * Get the open tag HTML for a marker item.
1308
+ */
1309
+ markerOpenTag(item) {
1310
+ const type = item.node.type;
1311
+ if (type === LayoutMarker.PARAGRAPH_START) {
1312
+ const indent = parseInt(item.paragraphNode?.options?.indent || "0", 10);
1313
+ if (indent > 0) {
1314
+ return `<span class="wtc-paragraph wtc-paragraph-indent" style="--wtc-paragraph-indent: calc(${indent} * var(--wtc-grid-height)); --wtc-paragraph-indent-height: calc((var(--wtc-n-rows) - ${indent}) * var(--wtc-grid-height))">`;
1315
+ }
1316
+ return '<span class="wtc-paragraph">';
1317
+ }
1318
+ if (type === LayoutMarker.LIST_START) return '<span class="wtc-list">';
1319
+ if (type === LayoutMarker.LIST_ITEM_START) return '<span class="wtc-list-item">';
1320
+ return "";
1321
+ }
1322
+ /**
1323
+ * Get the close tag HTML for a marker type.
1324
+ */
1325
+ markerCloseTag(type) {
1326
+ if (type === LayoutMarker.PARAGRAPH_START) return "</span>";
1327
+ if (type === LayoutMarker.LIST_START) return "</span>";
1328
+ if (type === LayoutMarker.LIST_ITEM_START) return "</span>";
1329
+ return "";
1330
+ }
1331
+ /**
1332
+ * Check if a marker type is an "open" marker.
1333
+ */
1334
+ isOpenMarker(type) {
1335
+ return type === LayoutMarker.PARAGRAPH_START || type === LayoutMarker.LIST_START || type === LayoutMarker.LIST_ITEM_START;
1336
+ }
1337
+ /**
1338
+ * Check if a marker type is a "close" marker, and return its matching open type.
1339
+ */
1340
+ matchingOpenMarker(type) {
1341
+ if (type === LayoutMarker.PARAGRAPH_END) return LayoutMarker.PARAGRAPH_START;
1342
+ if (type === LayoutMarker.LIST_END) return LayoutMarker.LIST_START;
1343
+ if (type === LayoutMarker.LIST_ITEM_END) return LayoutMarker.LIST_ITEM_START;
1344
+ return null;
1345
+ }
1346
+ /**
1347
+ * Render an array of layout items into HTML.
1348
+ * Handles start/end markers for paragraphs, lists, and list items.
1349
+ * markerStack: open markers inherited from a previous slice (for tag balancing).
1350
+ * Returns { html, openStack } where openStack is the unclosed markers at the end.
1351
+ */
1352
+ renderLayoutItems(items, markerStack = []) {
1353
+ let html = "";
1354
+ for (const entry of markerStack) {
1355
+ html += this.markerOpenTag(entry);
1356
+ }
1357
+ const stack = [...markerStack];
1358
+ for (const item of items) {
1359
+ const type = item.node.type;
1360
+ if (this.isOpenMarker(type)) {
1361
+ html += this.markerOpenTag(item);
1362
+ stack.push(item);
1363
+ } else if (this.matchingOpenMarker(type)) {
1364
+ html += this.markerCloseTag(this.matchingOpenMarker(type));
1365
+ for (let i = stack.length - 1; i >= 0; i--) {
1366
+ if (stack[i].node.type === this.matchingOpenMarker(type)) {
1367
+ stack.splice(i, 1);
1368
+ break;
1369
+ }
1370
+ }
1371
+ } else {
1372
+ html += this.renderLayoutItem(item);
1373
+ }
1374
+ }
1375
+ const unclosed = [...stack];
1376
+ for (let i = stack.length - 1; i >= 0; i--) {
1377
+ html += this.markerCloseTag(stack[i].node.type);
1378
+ }
1379
+ return { html, openStack: unclosed };
1380
+ }
1381
+ /**
1382
+ * Render a single layout item.
1383
+ * If the item has pre-computed jiazhuSegments, use those directly.
1384
+ */
1385
+ renderLayoutItem(item) {
1386
+ if (item.jiazhuSegments && item.node.type === NodeType.JIAZHU) {
1387
+ return this.renderJiazhuFromSegments(item.node, item.jiazhuSegments);
1388
+ }
1389
+ return this.renderNode(item.node);
1390
+ }
1391
+ /**
1392
+ * Render jiazhu from pre-computed segments.
1393
+ */
1394
+ renderJiazhuFromSegments(node, segments) {
1395
+ const hasComplexChildren = node.children.some((c) => c.type !== NodeType.TEXT);
1396
+ if (hasComplexChildren) {
1397
+ return this.renderJiazhuComplex(node);
1398
+ }
1399
+ return segments.map(
1400
+ ({ col1, col2 }) => `<span class="wtc-jiazhu"><span class="wtc-jiazhu-col">${escapeHTML(col1)}</span><span class="wtc-jiazhu-col">${escapeHTML(col2)}</span></span>`
1401
+ ).join("");
1402
+ }
1403
+ /**
1404
+ * Render banxin from layout metadata.
1405
+ */
1406
+ renderBanxinFromMeta(meta) {
1407
+ if (!meta.title && !meta.chapter) return "";
1408
+ const title = escapeHTML(meta.title || "");
1409
+ const chapterParts = (meta.chapter || "").split(/\\\\|\n/).map((s) => s.trim()).filter(Boolean);
1410
+ const chapterHTML = chapterParts.map((p) => `<span class="wtc-banxin-chapter-part">${escapeHTML(p)}</span>`).join("");
1411
+ return `<div class="wtc-banxin">
1412
+ <div class="wtc-banxin-section wtc-banxin-upper">
1413
+ <span class="wtc-banxin-book-name">${title}</span>
1414
+ <div class="wtc-yuwei wtc-yuwei-upper"></div>
1415
+ </div>
1416
+ <div class="wtc-banxin-section wtc-banxin-middle">
1417
+ <div class="wtc-banxin-chapter">${chapterHTML}</div>
1418
+ </div>
1419
+ <div class="wtc-banxin-section wtc-banxin-lower">
1420
+ <div class="wtc-yuwei wtc-yuwei-lower"></div>
1421
+ </div>
1422
+ </div>`;
1423
+ }
1424
+ // =====================================================================
1425
+ // Node rendering (shared between legacy and layout pipelines)
1426
+ // =====================================================================
1427
+ renderNode(node) {
1428
+ if (!node) return "";
1429
+ switch (node.type) {
1430
+ case "body":
1431
+ return this.renderChildren(node.children);
1432
+ case NodeType.CONTENT_BLOCK:
1433
+ return this.renderContentBlock(node);
1434
+ case NodeType.PARAGRAPH:
1435
+ return this.renderParagraph(node);
1436
+ case NodeType.TEXT: {
1437
+ const val = node.value || "";
1438
+ this.colPos += [...val].length;
1439
+ return escapeHTML(val);
1440
+ }
1441
+ case NodeType.NEWLINE:
1442
+ this.colPos = 0;
1443
+ return '<br class="wtc-newline">';
1444
+ case NodeType.MATH:
1445
+ return `<span class="wtc-math">${escapeHTML(node.value || "")}</span>`;
1446
+ case NodeType.PARAGRAPH_BREAK:
1447
+ this.colPos = 0;
1448
+ return '<br class="wtc-paragraph-break">';
1449
+ case NodeType.JIAZHU:
1450
+ return this.renderJiazhu(node);
1451
+ case NodeType.SIDENOTE:
1452
+ return this.renderSidenote(node);
1453
+ case NodeType.MEIPI:
1454
+ return this.renderMeipi(node);
1455
+ case NodeType.PIZHU:
1456
+ return this.renderPizhu(node);
1457
+ case NodeType.TEXTBOX:
1458
+ return this.renderTextbox(node);
1459
+ case NodeType.FILL_TEXTBOX:
1460
+ return this.renderFillTextbox(node);
1461
+ case NodeType.SPACE:
1462
+ return this.renderSpace(node);
1463
+ case NodeType.COLUMN_BREAK:
1464
+ this.colPos = 0;
1465
+ return '<br class="wtc-column-break">';
1466
+ case NodeType.TAITOU:
1467
+ return this.renderTaitou(node);
1468
+ case NodeType.NUOTAI:
1469
+ return this.renderNuotai(node);
1470
+ case NodeType.SET_INDENT:
1471
+ return `<span class="wtc-set-indent" data-indent="${node.value || 0}"></span>`;
1472
+ case NodeType.EMPHASIS:
1473
+ return `<span class="wtc-emphasis">${this.renderChildren(node.children)}</span>`;
1474
+ case NodeType.PROPER_NAME:
1475
+ return `<span class="wtc-proper-name">${this.renderChildren(node.children)}</span>`;
1476
+ case NodeType.BOOK_TITLE:
1477
+ return `<span class="wtc-book-title-mark">${this.renderChildren(node.children)}</span>`;
1478
+ case NodeType.INVERTED:
1479
+ return `<span class="wtc-inverted">${this.renderChildren(node.children)}</span>`;
1480
+ case NodeType.OCTAGON:
1481
+ return `<span class="wtc-octagon">${this.renderChildren(node.children)}</span>`;
1482
+ case NodeType.CIRCLED:
1483
+ return `<span class="wtc-circled">${this.renderChildren(node.children)}</span>`;
1484
+ case NodeType.INVERTED_OCTAGON:
1485
+ return `<span class="wtc-inverted wtc-octagon">${this.renderChildren(node.children)}</span>`;
1486
+ case NodeType.FIX:
1487
+ return `<span class="wtc-fix">${this.renderChildren(node.children)}</span>`;
1488
+ case NodeType.DECORATE:
1489
+ return `<span class="wtc-decorate">${this.renderChildren(node.children)}</span>`;
1490
+ case NodeType.LIST:
1491
+ return this.renderList(node);
1492
+ case NodeType.LIST_ITEM:
1493
+ return `<div class="wtc-list-item">${this.renderChildren(node.children)}</div>`;
1494
+ case NodeType.STAMP:
1495
+ return this.renderStamp(node);
1496
+ default:
1497
+ if (node.children && node.children.length > 0) {
1498
+ return this.renderChildren(node.children);
1499
+ }
1500
+ return "";
1501
+ }
1502
+ }
1503
+ renderChildren(children) {
1504
+ return children.map((c) => this.renderNode(c)).join("");
1505
+ }
1506
+ renderContentBlock(node) {
1507
+ const floatingHTML = [];
1508
+ const inlineChildren = [];
1509
+ for (const child of node.children) {
1510
+ if (child.type === NodeType.MEIPI || child.type === NodeType.PIZHU || child.type === NodeType.STAMP) {
1511
+ floatingHTML.push(this.renderNode(child));
1512
+ } else {
1513
+ inlineChildren.push(child);
1514
+ }
1515
+ }
1516
+ const inner = inlineChildren.map((c) => this.renderNode(c)).join("");
1517
+ const floating = floatingHTML.join("\n");
1518
+ const banxin = this.renderBanxin();
1519
+ const setupStyles = this.getSetupStyles();
1520
+ return `<div class="wtc-spread"${setupStyles}>
1521
+ ${floating}<div class="wtc-half-page wtc-half-right"><div class="wtc-content-border"><div class="wtc-content">${inner}</div></div></div>${banxin}<div class="wtc-half-page wtc-half-left"><div class="wtc-content-border"><div class="wtc-content"></div></div></div>
1522
+ </div>`;
1523
+ }
1524
+ renderBanxin() {
1525
+ if (!this.ast.title && !this.ast.chapter) return "";
1526
+ const title = escapeHTML(this.ast.title || "");
1527
+ const chapterParts = (this.ast.chapter || "").split(/\\\\|\n/).map((s) => s.trim()).filter(Boolean);
1528
+ const chapterHTML = chapterParts.map((p) => `<span class="wtc-banxin-chapter-part">${escapeHTML(p)}</span>`).join("");
1529
+ return `<div class="wtc-banxin">
1530
+ <div class="wtc-banxin-section wtc-banxin-upper">
1531
+ <span class="wtc-banxin-book-name">${title}</span>
1532
+ <div class="wtc-yuwei wtc-yuwei-upper"></div>
1533
+ </div>
1534
+ <div class="wtc-banxin-section wtc-banxin-middle">
1535
+ <div class="wtc-banxin-chapter">${chapterHTML}</div>
1536
+ </div>
1537
+ <div class="wtc-banxin-section wtc-banxin-lower">
1538
+ <div class="wtc-yuwei wtc-yuwei-lower"></div>
1539
+ </div>
1540
+ </div>`;
1541
+ }
1542
+ renderParagraph(node) {
1543
+ const indent = parseInt(node.options?.indent || "0", 10);
1544
+ if (indent > 0) {
1545
+ const prevIndent = this.currentIndent;
1546
+ this.currentIndent = indent;
1547
+ const inner = this.renderChildren(node.children);
1548
+ this.currentIndent = prevIndent;
1549
+ return `<span class="wtc-paragraph wtc-paragraph-indent" style="--wtc-paragraph-indent: calc(${indent} * var(--wtc-grid-height)); --wtc-paragraph-indent-height: calc((var(--wtc-n-rows) - ${indent}) * var(--wtc-grid-height))">${inner}</span>`;
1550
+ }
1551
+ return `<span class="wtc-paragraph">${this.renderChildren(node.children)}</span>`;
1552
+ }
1553
+ renderJiazhu(node) {
1554
+ const hasComplexChildren = node.children.some((c) => c.type !== NodeType.TEXT);
1555
+ if (hasComplexChildren) {
1556
+ return this.renderJiazhuComplex(node);
1557
+ }
1558
+ const text = getPlainText(node.children);
1559
+ const align = node.options?.align || "outward";
1560
+ const maxPerCol = this.nRows - this.currentIndent;
1561
+ const remaining = maxPerCol - this.colPos % maxPerCol;
1562
+ const firstMax = remaining > 0 && remaining < maxPerCol ? remaining : maxPerCol;
1563
+ const segments = splitJiazhuMulti(text, maxPerCol, align, firstMax);
1564
+ const totalChars = [...text].length;
1565
+ const firstSegChars = firstMax * 2;
1566
+ if (totalChars <= firstSegChars) {
1567
+ this.colPos += Math.ceil(totalChars / 2);
1568
+ } else {
1569
+ const lastSeg = segments[segments.length - 1];
1570
+ this.colPos = Math.max([...lastSeg.col1].length, [...lastSeg.col2].length);
1571
+ }
1572
+ return segments.map(
1573
+ ({ col1, col2 }) => `<span class="wtc-jiazhu"><span class="wtc-jiazhu-col">${escapeHTML(col1)}</span><span class="wtc-jiazhu-col">${escapeHTML(col2)}</span></span>`
1574
+ ).join("");
1575
+ }
1576
+ renderJiazhuComplex(node) {
1577
+ const text = getPlainText(node.children);
1578
+ const mid = Math.ceil([...text].length / 2);
1579
+ let charCount = 0;
1580
+ let splitIdx = node.children.length;
1581
+ for (let i = 0; i < node.children.length; i++) {
1582
+ const childText = getPlainText([node.children[i]]);
1583
+ charCount += [...childText].length;
1584
+ if (charCount >= mid) {
1585
+ splitIdx = i + 1;
1586
+ break;
1587
+ }
1588
+ }
1589
+ const col1HTML = node.children.slice(0, splitIdx).map((c) => this.renderNode(c)).join("");
1590
+ const col2HTML = node.children.slice(splitIdx).map((c) => this.renderNode(c)).join("");
1591
+ return `<span class="wtc-jiazhu"><span class="wtc-jiazhu-col">${col1HTML}</span><span class="wtc-jiazhu-col">${col2HTML}</span></span>`;
1592
+ }
1593
+ renderSidenote(node) {
1594
+ const opts = node.options || {};
1595
+ let style = this.buildStyleFromOptions(opts, {
1596
+ color: "--wtc-sidenote-color",
1597
+ "font-size": "--wtc-sidenote-font-size"
1598
+ });
1599
+ if (opts.yoffset) {
1600
+ style += `margin-block-start: ${opts.yoffset};`;
1601
+ }
1602
+ return `<span class="wtc-sidenote"${style ? ` style="${style}"` : ""}>${this.renderChildren(node.children)}</span>`;
1603
+ }
1604
+ renderMeipi(node) {
1605
+ const opts = node.options || {};
1606
+ let style = "";
1607
+ if (opts.x) {
1608
+ style += `right: ${opts.x};`;
1609
+ } else {
1610
+ const autoX = this.meipiCount * 2;
1611
+ style += `right: ${autoX}em;`;
1612
+ this.meipiCount++;
1613
+ }
1614
+ if (opts.y) style += `top: ${opts.y};`;
1615
+ if (opts.height) style += `height: ${opts.height};`;
1616
+ if (opts.color) style += `color: ${this.parseColor(opts.color)};`;
1617
+ if (opts["font-size"]) style += `font-size: ${opts["font-size"]};`;
1618
+ return `<div class="wtc-meipi"${style ? ` style="${style}"` : ""}>${this.renderChildren(node.children)}</div>`;
1619
+ }
1620
+ renderPizhu(node) {
1621
+ const opts = node.options || {};
1622
+ let style = "";
1623
+ if (opts.x) style += `right: ${opts.x};`;
1624
+ if (opts.y) style += `top: ${opts.y};`;
1625
+ if (opts.color) style += `color: ${this.parseColor(opts.color)};`;
1626
+ if (opts["font-size"]) style += `font-size: ${opts["font-size"]};`;
1627
+ return `<div class="wtc-pizhu"${style ? ` style="${style}"` : ""}>${this.renderChildren(node.children)}</div>`;
1628
+ }
1629
+ renderTextbox(node) {
1630
+ const opts = node.options || {};
1631
+ let style = "";
1632
+ if (opts.height) {
1633
+ const h = opts.height;
1634
+ if (/^\d+$/.test(h)) {
1635
+ style += `--wtc-textbox-height: ${h};`;
1636
+ } else {
1637
+ style += `inline-size: ${h};`;
1638
+ }
1639
+ }
1640
+ if (opts.border === "true") style += "border: 1px solid var(--wtc-border-color);";
1641
+ if (opts["background-color"]) style += `background-color: ${this.parseColor(opts["background-color"])};`;
1642
+ if (opts["font-color"]) style += `color: ${this.parseColor(opts["font-color"])};`;
1643
+ if (opts["font-size"]) style += `font-size: ${opts["font-size"]};`;
1644
+ return `<span class="wtc-textbox"${style ? ` style="${style}"` : ""}>${this.renderChildren(node.children)}</span>`;
1645
+ }
1646
+ renderFillTextbox(node) {
1647
+ const opts = node.options || {};
1648
+ let style = "";
1649
+ if (opts.height) {
1650
+ style += `--wtc-textbox-height: ${opts.height};`;
1651
+ }
1652
+ if (opts.value && /^\d+$/.test(opts.value)) {
1653
+ style += `--wtc-textbox-height: ${opts.value};`;
1654
+ }
1655
+ return `<span class="wtc-textbox wtc-textbox-fill"${style ? ` style="${style}"` : ""}>${this.renderChildren(node.children)}</span>`;
1656
+ }
1657
+ renderSpace(node) {
1658
+ const count = parseInt(node.value, 10) || 1;
1659
+ return "\u3000".repeat(count);
1660
+ }
1661
+ renderTaitou(node) {
1662
+ const level = node.value || "0";
1663
+ return `<br class="wtc-newline"><span class="wtc-taitou" data-level="${level}"></span>`;
1664
+ }
1665
+ renderNuotai(node) {
1666
+ const count = parseInt(node.value, 10) || 1;
1667
+ return "\u3000".repeat(count);
1668
+ }
1669
+ renderList(node) {
1670
+ return `<div class="wtc-list">${this.renderChildren(node.children)}</div>`;
1671
+ }
1672
+ renderStamp(node) {
1673
+ const opts = node.options || {};
1674
+ let style = "position: absolute;";
1675
+ if (opts.xshift) style += `right: ${opts.xshift};`;
1676
+ if (opts.yshift) style += `top: ${opts.yshift};`;
1677
+ if (opts.width) style += `width: ${opts.width};`;
1678
+ if (opts.opacity) style += `opacity: ${opts.opacity};`;
1679
+ return `<img class="wtc-stamp" src="${escapeHTML(node.src || "")}" style="${style}" alt="stamp">`;
1680
+ }
1681
+ parseColor(colorStr) {
1682
+ if (!colorStr) return "inherit";
1683
+ colorStr = colorStr.replace(/[{}]/g, "").trim();
1684
+ if (/^[a-zA-Z]+$/.test(colorStr)) return colorStr;
1685
+ const parts = colorStr.split(/[\s,]+/).map(Number);
1686
+ if (parts.length === 3) {
1687
+ if (parts.every((v) => v >= 0 && v <= 1)) {
1688
+ return `rgb(${Math.round(parts[0] * 255)}, ${Math.round(parts[1] * 255)}, ${Math.round(parts[2] * 255)})`;
1689
+ }
1690
+ if (parts.every((v) => v >= 0 && v <= 255)) {
1691
+ return `rgb(${parts[0]}, ${parts[1]}, ${parts[2]})`;
1692
+ }
1693
+ }
1694
+ return colorStr;
1695
+ }
1696
+ buildStyleFromOptions(opts, mapping) {
1697
+ if (!opts) return "";
1698
+ let style = "";
1699
+ for (const [key, cssVar] of Object.entries(mapping)) {
1700
+ if (opts[key] && cssVar) {
1701
+ style += `${cssVar}: ${opts[key]};`;
1702
+ }
1703
+ }
1704
+ return style;
1705
+ }
1706
+ };
1707
+
1708
+ // src/index.js
1709
+ function renderToHTML(texSource) {
1710
+ const { ast, warnings } = parse(texSource);
1711
+ if (warnings.length > 0) {
1712
+ console.warn("[WebTeX-CN] Parse warnings:", warnings);
1713
+ }
1714
+ const layoutResult = layout(ast);
1715
+ const renderer = new HTMLRenderer(ast);
1716
+ const pageHTMLs = renderer.renderFromLayout(layoutResult);
1717
+ return pageHTMLs.map(
1718
+ (html) => `<div class="wtc-page" data-template="${layoutResult.templateId}">${html}</div>`
1719
+ ).join("\n");
1720
+ }
1721
+ function renderToPage(texSource) {
1722
+ const { ast, warnings } = parse(texSource);
1723
+ if (warnings.length > 0) {
1724
+ console.warn("[WebTeX-CN] Parse warnings:", warnings);
1725
+ }
1726
+ const renderer = new HTMLRenderer(ast);
1727
+ return renderer.renderPage();
1728
+ }
1729
+ function renderToDOM(texSource, container, options = {}) {
1730
+ const { cssBasePath } = options;
1731
+ const { ast, warnings } = parse(texSource);
1732
+ if (warnings.length > 0) {
1733
+ console.warn("[WebTeX-CN] Parse warnings:", warnings);
1734
+ }
1735
+ const layoutResult = layout(ast);
1736
+ const renderer = new HTMLRenderer(ast);
1737
+ const pageHTMLs = renderer.renderFromLayout(layoutResult);
1738
+ const el = typeof container === "string" ? document.querySelector(container) : container;
1739
+ if (!el) {
1740
+ throw new Error(`[WebTeX-CN] Container not found: ${container}`);
1741
+ }
1742
+ if (cssBasePath && typeof document !== "undefined") {
1743
+ setTemplate(layoutResult.templateId, cssBasePath);
1744
+ }
1745
+ el.innerHTML = pageHTMLs.map(
1746
+ (html) => `<div class="wtc-page" data-template="${layoutResult.templateId}">${html}</div>`
1747
+ ).join("\n");
1748
+ }
1749
+ async function render(url, container, options = {}) {
1750
+ const response = await fetch(url);
1751
+ if (!response.ok) {
1752
+ throw new Error(`[WebTeX-CN] Failed to fetch ${url}: ${response.status}`);
1753
+ }
1754
+ const texSource = await response.text();
1755
+ renderToDOM(texSource, container, options);
1756
+ }
1757
+ function getTemplates() {
1758
+ return [
1759
+ { id: "siku-quanshu", name: "\u56DB\u5E93\u5168\u4E66 (\u9ED1\u767D)" },
1760
+ { id: "siku-quanshu-colored", name: "\u56DB\u5E93\u5168\u4E66 (\u5F69\u8272)" },
1761
+ { id: "honglou", name: "\u7EA2\u697C\u68A6\u7532\u620C\u672C" },
1762
+ { id: "minimal", name: "\u6781\u7B80" }
1763
+ ];
1764
+ }
1765
+ function setTemplate(templateId, basePath = "") {
1766
+ if (typeof document === "undefined") return;
1767
+ const old = document.querySelector("link[data-wtc-template]");
1768
+ if (old) old.remove();
1769
+ const link = document.createElement("link");
1770
+ link.rel = "stylesheet";
1771
+ link.href = `${basePath}${templateId}.css`;
1772
+ link.dataset.wtcTemplate = templateId;
1773
+ document.head.appendChild(link);
1774
+ }
1775
+ if (typeof window !== "undefined") {
1776
+ window.WebTeX = { render, renderToDOM, renderToHTML, renderToPage, parse, getTemplates, setTemplate };
1777
+ }
1778
+ export {
1779
+ HTMLRenderer,
1780
+ getTemplates,
1781
+ layout,
1782
+ parse,
1783
+ render,
1784
+ renderToDOM,
1785
+ renderToHTML,
1786
+ renderToPage,
1787
+ setTemplate
1788
+ };