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.
- package/LICENSE +201 -0
- package/README.md +2 -0
- package/bin/webtex-cn.js +195 -0
- package/dist/base.css +513 -0
- package/dist/honglou.css +35 -0
- package/dist/minimal.css +26 -0
- package/dist/siku-quanshu-colored.css +31 -0
- package/dist/siku-quanshu.css +33 -0
- package/dist/webtex-cn.css +653 -0
- package/dist/webtex-cn.esm.js +1788 -0
- package/dist/webtex-cn.js +1812 -0
- package/dist/webtex-cn.min.js +46 -0
- package/package.json +59 -0
- package/src/config/templates.js +49 -0
- package/src/index.js +129 -0
- package/src/layout/grid-layout.js +484 -0
- package/src/model/nodes.js +94 -0
- package/src/parser/commands.js +156 -0
- package/src/parser/index.js +27 -0
- package/src/parser/parser.js +472 -0
- package/src/parser/tokenizer.js +228 -0
- package/src/renderer/html-renderer.js +643 -0
- package/src/templates/base.css +513 -0
- package/src/templates/honglou.css +35 -0
- package/src/templates/minimal.css +26 -0
- package/src/templates/siku-quanshu-colored.css +31 -0
- package/src/templates/siku-quanshu.css +33 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTML Renderer: converts Document AST (or LayoutResult) to HTML string.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { NodeType } from '../model/nodes.js';
|
|
6
|
+
import { resolveTemplateId, getGridConfig } from '../config/templates.js';
|
|
7
|
+
import { getPlainText, splitJiazhuMulti, LayoutMarker } from '../layout/grid-layout.js';
|
|
8
|
+
|
|
9
|
+
// Setup parameter → CSS variable mapping
|
|
10
|
+
const setupParamMap = {
|
|
11
|
+
content: {
|
|
12
|
+
'font-size': '--wtc-font-size',
|
|
13
|
+
'line-height': '--wtc-line-height',
|
|
14
|
+
'letter-spacing': '--wtc-letter-spacing',
|
|
15
|
+
'font-color': '--wtc-font-color',
|
|
16
|
+
'border-color': '--wtc-border-color',
|
|
17
|
+
'border-thickness': '--wtc-border-thickness',
|
|
18
|
+
},
|
|
19
|
+
page: {
|
|
20
|
+
'page-width': '--wtc-page-width',
|
|
21
|
+
'page-height': '--wtc-page-height',
|
|
22
|
+
'margin-top': '--wtc-margin-top',
|
|
23
|
+
'margin-bottom': '--wtc-margin-bottom',
|
|
24
|
+
'margin-left': '--wtc-margin-left',
|
|
25
|
+
'margin-right': '--wtc-margin-right',
|
|
26
|
+
'background': '--wtc-page-background',
|
|
27
|
+
},
|
|
28
|
+
banxin: {
|
|
29
|
+
'width': '--wtc-banxin-width',
|
|
30
|
+
'font-size': '--wtc-banxin-font-size',
|
|
31
|
+
},
|
|
32
|
+
jiazhu: {
|
|
33
|
+
'font-size': '--wtc-jiazhu-font-size',
|
|
34
|
+
'color': '--wtc-jiazhu-color',
|
|
35
|
+
'line-height': '--wtc-jiazhu-line-height',
|
|
36
|
+
'gap': '--wtc-jiazhu-gap',
|
|
37
|
+
},
|
|
38
|
+
sidenode: {
|
|
39
|
+
'font-size': '--wtc-sidenote-font-size',
|
|
40
|
+
'color': '--wtc-sidenote-color',
|
|
41
|
+
},
|
|
42
|
+
meipi: {
|
|
43
|
+
'font-size': '--wtc-meipi-font-size',
|
|
44
|
+
'color': '--wtc-meipi-color',
|
|
45
|
+
},
|
|
46
|
+
pizhu: {
|
|
47
|
+
'font-size': '--wtc-pizhu-font-size',
|
|
48
|
+
'color': '--wtc-pizhu-color',
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Escape HTML special characters.
|
|
54
|
+
*/
|
|
55
|
+
function escapeHTML(str) {
|
|
56
|
+
return str
|
|
57
|
+
.replace(/&/g, '&')
|
|
58
|
+
.replace(/</g, '<')
|
|
59
|
+
.replace(/>/g, '>')
|
|
60
|
+
.replace(/"/g, '"');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class HTMLRenderer {
|
|
64
|
+
constructor(ast) {
|
|
65
|
+
this.ast = ast;
|
|
66
|
+
this.templateId = resolveTemplateId(ast);
|
|
67
|
+
this.meipiCount = 0;
|
|
68
|
+
|
|
69
|
+
const grid = getGridConfig(this.templateId);
|
|
70
|
+
this.nRows = grid.nRows;
|
|
71
|
+
this.nCols = grid.nCols;
|
|
72
|
+
this.currentIndent = 0;
|
|
73
|
+
this.colPos = 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Collect CSS variable overrides from setup commands.
|
|
78
|
+
*/
|
|
79
|
+
getSetupStylesFromCommands(setupCommands) {
|
|
80
|
+
const overrides = [];
|
|
81
|
+
for (const cmd of (setupCommands || [])) {
|
|
82
|
+
const mapping = setupParamMap[cmd.setupType];
|
|
83
|
+
if (!mapping || !cmd.params) continue;
|
|
84
|
+
for (const [param, value] of Object.entries(cmd.params)) {
|
|
85
|
+
const cssVar = mapping[param];
|
|
86
|
+
if (cssVar) {
|
|
87
|
+
overrides.push(`${cssVar}: ${value}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return overrides.length > 0 ? ` style="${overrides.join('; ')}"` : '';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getSetupStyles() {
|
|
95
|
+
return this.getSetupStylesFromCommands(this.ast.setupCommands);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// =====================================================================
|
|
99
|
+
// Legacy render() — walks AST directly (kept for backward compat)
|
|
100
|
+
// =====================================================================
|
|
101
|
+
|
|
102
|
+
render() {
|
|
103
|
+
let html = '';
|
|
104
|
+
for (const child of this.ast.children) {
|
|
105
|
+
html += this.renderNode(child);
|
|
106
|
+
}
|
|
107
|
+
return html;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
renderPage() {
|
|
111
|
+
const content = this.render();
|
|
112
|
+
return `<!DOCTYPE html>
|
|
113
|
+
<html lang="zh">
|
|
114
|
+
<head>
|
|
115
|
+
<meta charset="UTF-8">
|
|
116
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
117
|
+
<title>${escapeHTML(this.ast.title || 'WebTeX-CN')}</title>
|
|
118
|
+
<link rel="stylesheet" href="base.css">
|
|
119
|
+
</head>
|
|
120
|
+
<body>
|
|
121
|
+
<div class="wtc-page" data-template="${this.templateId}">
|
|
122
|
+
${content}
|
|
123
|
+
</div>
|
|
124
|
+
</body>
|
|
125
|
+
</html>`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =====================================================================
|
|
129
|
+
// New layout-based render pipeline
|
|
130
|
+
// =====================================================================
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Render a LayoutResult into multi-page HTML.
|
|
134
|
+
* Each page becomes one wtc-page div with a complete spread.
|
|
135
|
+
*
|
|
136
|
+
* @param {object} layoutResult Output of layout()
|
|
137
|
+
* @returns {string[]} Array of page HTML strings (one per page)
|
|
138
|
+
*/
|
|
139
|
+
renderFromLayout(layoutResult) {
|
|
140
|
+
const setupStyles = this.getSetupStylesFromCommands(layoutResult.meta.setupCommands);
|
|
141
|
+
const banxin = this.renderBanxinFromMeta(layoutResult.meta);
|
|
142
|
+
|
|
143
|
+
let carryStack = []; // marker stack carried across pages
|
|
144
|
+
return layoutResult.pages.map(page => {
|
|
145
|
+
const boundary = page.halfBoundary ?? page.items.length;
|
|
146
|
+
const rightItems = page.items.slice(0, boundary);
|
|
147
|
+
const leftItems = page.items.slice(boundary);
|
|
148
|
+
|
|
149
|
+
const right = this.renderLayoutItems(rightItems, carryStack);
|
|
150
|
+
const left = this.renderLayoutItems(leftItems, right.openStack);
|
|
151
|
+
carryStack = left.openStack;
|
|
152
|
+
|
|
153
|
+
const rightHTML = right.html;
|
|
154
|
+
const leftHTML = left.html;
|
|
155
|
+
const floatsHTML = page.floats.map(f => this.renderNode(f)).join('\n');
|
|
156
|
+
|
|
157
|
+
return `<div class="wtc-spread"${setupStyles}>
|
|
158
|
+
${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>
|
|
159
|
+
</div>`;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get the open tag HTML for a marker item.
|
|
165
|
+
*/
|
|
166
|
+
markerOpenTag(item) {
|
|
167
|
+
const type = item.node.type;
|
|
168
|
+
if (type === LayoutMarker.PARAGRAPH_START) {
|
|
169
|
+
const indent = parseInt(item.paragraphNode?.options?.indent || '0', 10);
|
|
170
|
+
if (indent > 0) {
|
|
171
|
+
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))">`;
|
|
172
|
+
}
|
|
173
|
+
return '<span class="wtc-paragraph">';
|
|
174
|
+
}
|
|
175
|
+
if (type === LayoutMarker.LIST_START) return '<span class="wtc-list">';
|
|
176
|
+
if (type === LayoutMarker.LIST_ITEM_START) return '<span class="wtc-list-item">';
|
|
177
|
+
return '';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get the close tag HTML for a marker type.
|
|
182
|
+
*/
|
|
183
|
+
markerCloseTag(type) {
|
|
184
|
+
if (type === LayoutMarker.PARAGRAPH_START) return '</span>';
|
|
185
|
+
if (type === LayoutMarker.LIST_START) return '</span>';
|
|
186
|
+
if (type === LayoutMarker.LIST_ITEM_START) return '</span>';
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check if a marker type is an "open" marker.
|
|
192
|
+
*/
|
|
193
|
+
isOpenMarker(type) {
|
|
194
|
+
return type === LayoutMarker.PARAGRAPH_START ||
|
|
195
|
+
type === LayoutMarker.LIST_START ||
|
|
196
|
+
type === LayoutMarker.LIST_ITEM_START;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Check if a marker type is a "close" marker, and return its matching open type.
|
|
201
|
+
*/
|
|
202
|
+
matchingOpenMarker(type) {
|
|
203
|
+
if (type === LayoutMarker.PARAGRAPH_END) return LayoutMarker.PARAGRAPH_START;
|
|
204
|
+
if (type === LayoutMarker.LIST_END) return LayoutMarker.LIST_START;
|
|
205
|
+
if (type === LayoutMarker.LIST_ITEM_END) return LayoutMarker.LIST_ITEM_START;
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Render an array of layout items into HTML.
|
|
211
|
+
* Handles start/end markers for paragraphs, lists, and list items.
|
|
212
|
+
* markerStack: open markers inherited from a previous slice (for tag balancing).
|
|
213
|
+
* Returns { html, openStack } where openStack is the unclosed markers at the end.
|
|
214
|
+
*/
|
|
215
|
+
renderLayoutItems(items, markerStack = []) {
|
|
216
|
+
let html = '';
|
|
217
|
+
|
|
218
|
+
// Re-open tags from inherited stack
|
|
219
|
+
for (const entry of markerStack) {
|
|
220
|
+
html += this.markerOpenTag(entry);
|
|
221
|
+
}
|
|
222
|
+
const stack = [...markerStack];
|
|
223
|
+
|
|
224
|
+
for (const item of items) {
|
|
225
|
+
const type = item.node.type;
|
|
226
|
+
if (this.isOpenMarker(type)) {
|
|
227
|
+
html += this.markerOpenTag(item);
|
|
228
|
+
stack.push(item);
|
|
229
|
+
} else if (this.matchingOpenMarker(type)) {
|
|
230
|
+
html += this.markerCloseTag(this.matchingOpenMarker(type));
|
|
231
|
+
// Pop matching open marker from stack
|
|
232
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
233
|
+
if (stack[i].node.type === this.matchingOpenMarker(type)) {
|
|
234
|
+
stack.splice(i, 1);
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
html += this.renderLayoutItem(item);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Close unclosed tags (in reverse order)
|
|
244
|
+
const unclosed = [...stack];
|
|
245
|
+
for (let i = stack.length - 1; i >= 0; i--) {
|
|
246
|
+
html += this.markerCloseTag(stack[i].node.type);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return { html, openStack: unclosed };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Render a single layout item.
|
|
254
|
+
* If the item has pre-computed jiazhuSegments, use those directly.
|
|
255
|
+
*/
|
|
256
|
+
renderLayoutItem(item) {
|
|
257
|
+
if (item.jiazhuSegments && item.node.type === NodeType.JIAZHU) {
|
|
258
|
+
return this.renderJiazhuFromSegments(item.node, item.jiazhuSegments);
|
|
259
|
+
}
|
|
260
|
+
return this.renderNode(item.node);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Render jiazhu from pre-computed segments.
|
|
265
|
+
*/
|
|
266
|
+
renderJiazhuFromSegments(node, segments) {
|
|
267
|
+
// Check if children are complex (non-text)
|
|
268
|
+
const hasComplexChildren = node.children.some(c => c.type !== NodeType.TEXT);
|
|
269
|
+
if (hasComplexChildren) {
|
|
270
|
+
// Fall back to node-based rendering
|
|
271
|
+
return this.renderJiazhuComplex(node);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return segments.map(({ col1, col2 }) =>
|
|
275
|
+
`<span class="wtc-jiazhu"><span class="wtc-jiazhu-col">${escapeHTML(col1)}</span><span class="wtc-jiazhu-col">${escapeHTML(col2)}</span></span>`
|
|
276
|
+
).join('');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Render banxin from layout metadata.
|
|
281
|
+
*/
|
|
282
|
+
renderBanxinFromMeta(meta) {
|
|
283
|
+
if (!meta.title && !meta.chapter) return '';
|
|
284
|
+
const title = escapeHTML(meta.title || '');
|
|
285
|
+
// Chapter may contain \\ for line breaks → split into separate spans
|
|
286
|
+
const chapterParts = (meta.chapter || '').split(/\\\\|\n/).map(s => s.trim()).filter(Boolean);
|
|
287
|
+
const chapterHTML = chapterParts.map(p => `<span class="wtc-banxin-chapter-part">${escapeHTML(p)}</span>`).join('');
|
|
288
|
+
|
|
289
|
+
return `<div class="wtc-banxin">
|
|
290
|
+
<div class="wtc-banxin-section wtc-banxin-upper">
|
|
291
|
+
<span class="wtc-banxin-book-name">${title}</span>
|
|
292
|
+
<div class="wtc-yuwei wtc-yuwei-upper"></div>
|
|
293
|
+
</div>
|
|
294
|
+
<div class="wtc-banxin-section wtc-banxin-middle">
|
|
295
|
+
<div class="wtc-banxin-chapter">${chapterHTML}</div>
|
|
296
|
+
</div>
|
|
297
|
+
<div class="wtc-banxin-section wtc-banxin-lower">
|
|
298
|
+
<div class="wtc-yuwei wtc-yuwei-lower"></div>
|
|
299
|
+
</div>
|
|
300
|
+
</div>`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// =====================================================================
|
|
304
|
+
// Node rendering (shared between legacy and layout pipelines)
|
|
305
|
+
// =====================================================================
|
|
306
|
+
|
|
307
|
+
renderNode(node) {
|
|
308
|
+
if (!node) return '';
|
|
309
|
+
|
|
310
|
+
switch (node.type) {
|
|
311
|
+
case 'body':
|
|
312
|
+
return this.renderChildren(node.children);
|
|
313
|
+
|
|
314
|
+
case NodeType.CONTENT_BLOCK:
|
|
315
|
+
return this.renderContentBlock(node);
|
|
316
|
+
|
|
317
|
+
case NodeType.PARAGRAPH:
|
|
318
|
+
return this.renderParagraph(node);
|
|
319
|
+
|
|
320
|
+
case NodeType.TEXT: {
|
|
321
|
+
const val = node.value || '';
|
|
322
|
+
this.colPos += [...val].length;
|
|
323
|
+
return escapeHTML(val);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
case NodeType.NEWLINE:
|
|
327
|
+
this.colPos = 0;
|
|
328
|
+
return '<br class="wtc-newline">';
|
|
329
|
+
|
|
330
|
+
case NodeType.MATH:
|
|
331
|
+
return `<span class="wtc-math">${escapeHTML(node.value || '')}</span>`;
|
|
332
|
+
|
|
333
|
+
case NodeType.PARAGRAPH_BREAK:
|
|
334
|
+
this.colPos = 0;
|
|
335
|
+
return '<br class="wtc-paragraph-break">';
|
|
336
|
+
|
|
337
|
+
case NodeType.JIAZHU:
|
|
338
|
+
return this.renderJiazhu(node);
|
|
339
|
+
|
|
340
|
+
case NodeType.SIDENOTE:
|
|
341
|
+
return this.renderSidenote(node);
|
|
342
|
+
|
|
343
|
+
case NodeType.MEIPI:
|
|
344
|
+
return this.renderMeipi(node);
|
|
345
|
+
|
|
346
|
+
case NodeType.PIZHU:
|
|
347
|
+
return this.renderPizhu(node);
|
|
348
|
+
|
|
349
|
+
case NodeType.TEXTBOX:
|
|
350
|
+
return this.renderTextbox(node);
|
|
351
|
+
|
|
352
|
+
case NodeType.FILL_TEXTBOX:
|
|
353
|
+
return this.renderFillTextbox(node);
|
|
354
|
+
|
|
355
|
+
case NodeType.SPACE:
|
|
356
|
+
return this.renderSpace(node);
|
|
357
|
+
|
|
358
|
+
case NodeType.COLUMN_BREAK:
|
|
359
|
+
this.colPos = 0;
|
|
360
|
+
return '<br class="wtc-column-break">';
|
|
361
|
+
|
|
362
|
+
case NodeType.TAITOU:
|
|
363
|
+
return this.renderTaitou(node);
|
|
364
|
+
|
|
365
|
+
case NodeType.NUOTAI:
|
|
366
|
+
return this.renderNuotai(node);
|
|
367
|
+
|
|
368
|
+
case NodeType.SET_INDENT:
|
|
369
|
+
return `<span class="wtc-set-indent" data-indent="${node.value || 0}"></span>`;
|
|
370
|
+
|
|
371
|
+
case NodeType.EMPHASIS:
|
|
372
|
+
return `<span class="wtc-emphasis">${this.renderChildren(node.children)}</span>`;
|
|
373
|
+
|
|
374
|
+
case NodeType.PROPER_NAME:
|
|
375
|
+
return `<span class="wtc-proper-name">${this.renderChildren(node.children)}</span>`;
|
|
376
|
+
|
|
377
|
+
case NodeType.BOOK_TITLE:
|
|
378
|
+
return `<span class="wtc-book-title-mark">${this.renderChildren(node.children)}</span>`;
|
|
379
|
+
|
|
380
|
+
case NodeType.INVERTED:
|
|
381
|
+
return `<span class="wtc-inverted">${this.renderChildren(node.children)}</span>`;
|
|
382
|
+
|
|
383
|
+
case NodeType.OCTAGON:
|
|
384
|
+
return `<span class="wtc-octagon">${this.renderChildren(node.children)}</span>`;
|
|
385
|
+
|
|
386
|
+
case NodeType.CIRCLED:
|
|
387
|
+
return `<span class="wtc-circled">${this.renderChildren(node.children)}</span>`;
|
|
388
|
+
|
|
389
|
+
case NodeType.INVERTED_OCTAGON:
|
|
390
|
+
return `<span class="wtc-inverted wtc-octagon">${this.renderChildren(node.children)}</span>`;
|
|
391
|
+
|
|
392
|
+
case NodeType.FIX:
|
|
393
|
+
return `<span class="wtc-fix">${this.renderChildren(node.children)}</span>`;
|
|
394
|
+
|
|
395
|
+
case NodeType.DECORATE:
|
|
396
|
+
return `<span class="wtc-decorate">${this.renderChildren(node.children)}</span>`;
|
|
397
|
+
|
|
398
|
+
case NodeType.LIST:
|
|
399
|
+
return this.renderList(node);
|
|
400
|
+
|
|
401
|
+
case NodeType.LIST_ITEM:
|
|
402
|
+
return `<div class="wtc-list-item">${this.renderChildren(node.children)}</div>`;
|
|
403
|
+
|
|
404
|
+
case NodeType.STAMP:
|
|
405
|
+
return this.renderStamp(node);
|
|
406
|
+
|
|
407
|
+
default:
|
|
408
|
+
if (node.children && node.children.length > 0) {
|
|
409
|
+
return this.renderChildren(node.children);
|
|
410
|
+
}
|
|
411
|
+
return '';
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
renderChildren(children) {
|
|
416
|
+
return children.map(c => this.renderNode(c)).join('');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
renderContentBlock(node) {
|
|
420
|
+
const floatingHTML = [];
|
|
421
|
+
const inlineChildren = [];
|
|
422
|
+
|
|
423
|
+
for (const child of node.children) {
|
|
424
|
+
if (child.type === NodeType.MEIPI || child.type === NodeType.PIZHU || child.type === NodeType.STAMP) {
|
|
425
|
+
floatingHTML.push(this.renderNode(child));
|
|
426
|
+
} else {
|
|
427
|
+
inlineChildren.push(child);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const inner = inlineChildren.map(c => this.renderNode(c)).join('');
|
|
432
|
+
const floating = floatingHTML.join('\n');
|
|
433
|
+
const banxin = this.renderBanxin();
|
|
434
|
+
const setupStyles = this.getSetupStyles();
|
|
435
|
+
|
|
436
|
+
return `<div class="wtc-spread"${setupStyles}>
|
|
437
|
+
${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>
|
|
438
|
+
</div>`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
renderBanxin() {
|
|
442
|
+
if (!this.ast.title && !this.ast.chapter) return '';
|
|
443
|
+
const title = escapeHTML(this.ast.title || '');
|
|
444
|
+
const chapterParts = (this.ast.chapter || '').split(/\\\\|\n/).map(s => s.trim()).filter(Boolean);
|
|
445
|
+
const chapterHTML = chapterParts.map(p => `<span class="wtc-banxin-chapter-part">${escapeHTML(p)}</span>`).join('');
|
|
446
|
+
|
|
447
|
+
return `<div class="wtc-banxin">
|
|
448
|
+
<div class="wtc-banxin-section wtc-banxin-upper">
|
|
449
|
+
<span class="wtc-banxin-book-name">${title}</span>
|
|
450
|
+
<div class="wtc-yuwei wtc-yuwei-upper"></div>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="wtc-banxin-section wtc-banxin-middle">
|
|
453
|
+
<div class="wtc-banxin-chapter">${chapterHTML}</div>
|
|
454
|
+
</div>
|
|
455
|
+
<div class="wtc-banxin-section wtc-banxin-lower">
|
|
456
|
+
<div class="wtc-yuwei wtc-yuwei-lower"></div>
|
|
457
|
+
</div>
|
|
458
|
+
</div>`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
renderParagraph(node) {
|
|
462
|
+
const indent = parseInt(node.options?.indent || '0', 10);
|
|
463
|
+
if (indent > 0) {
|
|
464
|
+
const prevIndent = this.currentIndent;
|
|
465
|
+
this.currentIndent = indent;
|
|
466
|
+
const inner = this.renderChildren(node.children);
|
|
467
|
+
this.currentIndent = prevIndent;
|
|
468
|
+
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>`;
|
|
469
|
+
}
|
|
470
|
+
return `<span class="wtc-paragraph">${this.renderChildren(node.children)}</span>`;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
renderJiazhu(node) {
|
|
474
|
+
const hasComplexChildren = node.children.some(c => c.type !== NodeType.TEXT);
|
|
475
|
+
|
|
476
|
+
if (hasComplexChildren) {
|
|
477
|
+
return this.renderJiazhuComplex(node);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const text = getPlainText(node.children);
|
|
481
|
+
const align = node.options?.align || 'outward';
|
|
482
|
+
const maxPerCol = this.nRows - this.currentIndent;
|
|
483
|
+
const remaining = maxPerCol - (this.colPos % maxPerCol);
|
|
484
|
+
const firstMax = remaining > 0 && remaining < maxPerCol ? remaining : maxPerCol;
|
|
485
|
+
const segments = splitJiazhuMulti(text, maxPerCol, align, firstMax);
|
|
486
|
+
|
|
487
|
+
const totalChars = [...text].length;
|
|
488
|
+
const firstSegChars = firstMax * 2;
|
|
489
|
+
if (totalChars <= firstSegChars) {
|
|
490
|
+
this.colPos += Math.ceil(totalChars / 2);
|
|
491
|
+
} else {
|
|
492
|
+
const lastSeg = segments[segments.length - 1];
|
|
493
|
+
this.colPos = Math.max([...lastSeg.col1].length, [...lastSeg.col2].length);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return segments.map(({ col1, col2 }) =>
|
|
497
|
+
`<span class="wtc-jiazhu"><span class="wtc-jiazhu-col">${escapeHTML(col1)}</span><span class="wtc-jiazhu-col">${escapeHTML(col2)}</span></span>`
|
|
498
|
+
).join('');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
renderJiazhuComplex(node) {
|
|
502
|
+
const text = getPlainText(node.children);
|
|
503
|
+
const mid = Math.ceil([...text].length / 2);
|
|
504
|
+
let charCount = 0;
|
|
505
|
+
let splitIdx = node.children.length;
|
|
506
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
507
|
+
const childText = getPlainText([node.children[i]]);
|
|
508
|
+
charCount += [...childText].length;
|
|
509
|
+
if (charCount >= mid) {
|
|
510
|
+
splitIdx = i + 1;
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
const col1HTML = node.children.slice(0, splitIdx).map(c => this.renderNode(c)).join('');
|
|
515
|
+
const col2HTML = node.children.slice(splitIdx).map(c => this.renderNode(c)).join('');
|
|
516
|
+
return `<span class="wtc-jiazhu"><span class="wtc-jiazhu-col">${col1HTML}</span><span class="wtc-jiazhu-col">${col2HTML}</span></span>`;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
renderSidenote(node) {
|
|
520
|
+
const opts = node.options || {};
|
|
521
|
+
let style = this.buildStyleFromOptions(opts, {
|
|
522
|
+
color: '--wtc-sidenote-color',
|
|
523
|
+
'font-size': '--wtc-sidenote-font-size',
|
|
524
|
+
});
|
|
525
|
+
if (opts.yoffset) {
|
|
526
|
+
style += `margin-block-start: ${opts.yoffset};`;
|
|
527
|
+
}
|
|
528
|
+
return `<span class="wtc-sidenote"${style ? ` style="${style}"` : ''}>${this.renderChildren(node.children)}</span>`;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
renderMeipi(node) {
|
|
532
|
+
const opts = node.options || {};
|
|
533
|
+
let style = '';
|
|
534
|
+
if (opts.x) {
|
|
535
|
+
style += `right: ${opts.x};`;
|
|
536
|
+
} else {
|
|
537
|
+
const autoX = this.meipiCount * 2;
|
|
538
|
+
style += `right: ${autoX}em;`;
|
|
539
|
+
this.meipiCount++;
|
|
540
|
+
}
|
|
541
|
+
if (opts.y) style += `top: ${opts.y};`;
|
|
542
|
+
if (opts.height) style += `height: ${opts.height};`;
|
|
543
|
+
if (opts.color) style += `color: ${this.parseColor(opts.color)};`;
|
|
544
|
+
if (opts['font-size']) style += `font-size: ${opts['font-size']};`;
|
|
545
|
+
return `<div class="wtc-meipi"${style ? ` style="${style}"` : ''}>${this.renderChildren(node.children)}</div>`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
renderPizhu(node) {
|
|
549
|
+
const opts = node.options || {};
|
|
550
|
+
let style = '';
|
|
551
|
+
if (opts.x) style += `right: ${opts.x};`;
|
|
552
|
+
if (opts.y) style += `top: ${opts.y};`;
|
|
553
|
+
if (opts.color) style += `color: ${this.parseColor(opts.color)};`;
|
|
554
|
+
if (opts['font-size']) style += `font-size: ${opts['font-size']};`;
|
|
555
|
+
return `<div class="wtc-pizhu"${style ? ` style="${style}"` : ''}>${this.renderChildren(node.children)}</div>`;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
renderTextbox(node) {
|
|
559
|
+
const opts = node.options || {};
|
|
560
|
+
let style = '';
|
|
561
|
+
if (opts.height) {
|
|
562
|
+
const h = opts.height;
|
|
563
|
+
if (/^\d+$/.test(h)) {
|
|
564
|
+
style += `--wtc-textbox-height: ${h};`;
|
|
565
|
+
} else {
|
|
566
|
+
style += `inline-size: ${h};`;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (opts.border === 'true') style += 'border: 1px solid var(--wtc-border-color);';
|
|
570
|
+
if (opts['background-color']) style += `background-color: ${this.parseColor(opts['background-color'])};`;
|
|
571
|
+
if (opts['font-color']) style += `color: ${this.parseColor(opts['font-color'])};`;
|
|
572
|
+
if (opts['font-size']) style += `font-size: ${opts['font-size']};`;
|
|
573
|
+
return `<span class="wtc-textbox"${style ? ` style="${style}"` : ''}>${this.renderChildren(node.children)}</span>`;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
renderFillTextbox(node) {
|
|
577
|
+
const opts = node.options || {};
|
|
578
|
+
let style = '';
|
|
579
|
+
if (opts.height) {
|
|
580
|
+
style += `--wtc-textbox-height: ${opts.height};`;
|
|
581
|
+
}
|
|
582
|
+
if (opts.value && /^\d+$/.test(opts.value)) {
|
|
583
|
+
style += `--wtc-textbox-height: ${opts.value};`;
|
|
584
|
+
}
|
|
585
|
+
return `<span class="wtc-textbox wtc-textbox-fill"${style ? ` style="${style}"` : ''}>${this.renderChildren(node.children)}</span>`;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
renderSpace(node) {
|
|
589
|
+
const count = parseInt(node.value, 10) || 1;
|
|
590
|
+
return '\u3000'.repeat(count);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
renderTaitou(node) {
|
|
594
|
+
const level = node.value || '0';
|
|
595
|
+
return `<br class="wtc-newline"><span class="wtc-taitou" data-level="${level}"></span>`;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
renderNuotai(node) {
|
|
599
|
+
const count = parseInt(node.value, 10) || 1;
|
|
600
|
+
return '\u3000'.repeat(count);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
renderList(node) {
|
|
604
|
+
return `<div class="wtc-list">${this.renderChildren(node.children)}</div>`;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
renderStamp(node) {
|
|
608
|
+
const opts = node.options || {};
|
|
609
|
+
let style = 'position: absolute;';
|
|
610
|
+
if (opts.xshift) style += `right: ${opts.xshift};`;
|
|
611
|
+
if (opts.yshift) style += `top: ${opts.yshift};`;
|
|
612
|
+
if (opts.width) style += `width: ${opts.width};`;
|
|
613
|
+
if (opts.opacity) style += `opacity: ${opts.opacity};`;
|
|
614
|
+
return `<img class="wtc-stamp" src="${escapeHTML(node.src || '')}" style="${style}" alt="stamp">`;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
parseColor(colorStr) {
|
|
618
|
+
if (!colorStr) return 'inherit';
|
|
619
|
+
colorStr = colorStr.replace(/[{}]/g, '').trim();
|
|
620
|
+
if (/^[a-zA-Z]+$/.test(colorStr)) return colorStr;
|
|
621
|
+
const parts = colorStr.split(/[\s,]+/).map(Number);
|
|
622
|
+
if (parts.length === 3) {
|
|
623
|
+
if (parts.every(v => v >= 0 && v <= 1)) {
|
|
624
|
+
return `rgb(${Math.round(parts[0] * 255)}, ${Math.round(parts[1] * 255)}, ${Math.round(parts[2] * 255)})`;
|
|
625
|
+
}
|
|
626
|
+
if (parts.every(v => v >= 0 && v <= 255)) {
|
|
627
|
+
return `rgb(${parts[0]}, ${parts[1]}, ${parts[2]})`;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
return colorStr;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
buildStyleFromOptions(opts, mapping) {
|
|
634
|
+
if (!opts) return '';
|
|
635
|
+
let style = '';
|
|
636
|
+
for (const [key, cssVar] of Object.entries(mapping)) {
|
|
637
|
+
if (opts[key] && cssVar) {
|
|
638
|
+
style += `${cssVar}: ${opts[key]};`;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return style;
|
|
642
|
+
}
|
|
643
|
+
}
|