@v1hz/md2docx 1.6.0 → 2.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/src/web/app.js DELETED
@@ -1,880 +0,0 @@
1
- const configForm = document.querySelector("#config-form");
2
- const configIndex = document.querySelector("#config-index");
3
- const styleIndex = document.querySelector("#style-index");
4
- const styleEditor = document.querySelector("#style-editor");
5
- const styleSearch = document.querySelector("#style-search");
6
- const status = document.querySelector("#status");
7
- const statusIndicator = document.querySelector("#status-indicator");
8
- const saveButton = document.querySelector("#save-button");
9
- const resetButton = document.querySelector("#reset-button");
10
- const previewButton = document.querySelector("#preview-button");
11
-
12
- let activeView = "config";
13
- let schema;
14
- let originalConfig;
15
- let currentConfig;
16
- let originalStyles;
17
- let currentStyles;
18
- let styleCatalog;
19
- let styleRevision;
20
- let selectedStyle = { kind: "default" };
21
-
22
- const styleFields = [
23
- { section: "文字", label: "中文字体", path: ["run", "font", "eastAsia"], type: "text" },
24
- {
25
- section: "文字",
26
- label: "西文字体",
27
- path: ["run", "font", "ascii"],
28
- type: "text",
29
- mirror: ["run", "font", "hAnsi"],
30
- },
31
- {
32
- section: "文字",
33
- label: "字号",
34
- path: ["run", "size"],
35
- type: "number",
36
- unit: "pt",
37
- scale: 2,
38
- min: 1,
39
- step: 0.5,
40
- mirror: ["run", "sizeComplexScript"],
41
- },
42
- { section: "文字", label: "文字颜色", path: ["run", "color"], type: "color" },
43
- { section: "文字", label: "加粗", path: ["run", "bold"], type: "boolean" },
44
- { section: "文字", label: "斜体", path: ["run", "italics"], type: "boolean" },
45
- {
46
- section: "段落",
47
- label: "对齐方式",
48
- path: ["paragraph", "alignment"],
49
- type: "select",
50
- options: [
51
- ["left", "左对齐"],
52
- ["center", "居中"],
53
- ["right", "右对齐"],
54
- ["both", "两端对齐"],
55
- ],
56
- },
57
- {
58
- section: "段落",
59
- label: "段前间距",
60
- path: ["paragraph", "spacing", "before"],
61
- type: "number",
62
- unit: "pt",
63
- scale: 20,
64
- min: 0,
65
- step: 1,
66
- },
67
- {
68
- section: "段落",
69
- label: "段后间距",
70
- path: ["paragraph", "spacing", "after"],
71
- type: "number",
72
- unit: "pt",
73
- scale: 20,
74
- min: 0,
75
- step: 1,
76
- },
77
- {
78
- section: "段落",
79
- label: "行距",
80
- path: ["paragraph", "spacing", "line"],
81
- type: "select-number",
82
- options: [
83
- [240, "单倍"],
84
- [300, "1.25 倍"],
85
- [360, "1.5 倍"],
86
- [480, "2 倍"],
87
- ],
88
- },
89
- {
90
- section: "段落",
91
- label: "首行缩进",
92
- path: ["paragraph", "indent", "firstLineChars"],
93
- type: "number",
94
- unit: "字符",
95
- scale: 100,
96
- min: 0,
97
- step: 0.5,
98
- },
99
- { section: "分页", label: "与下段同页", path: ["paragraph", "keepNext"], type: "boolean" },
100
- { section: "分页", label: "段中不分页", path: ["paragraph", "keepLines"], type: "boolean" },
101
- { section: "分页", label: "段前分页", path: ["paragraph", "pageBreakBefore"], type: "boolean" },
102
- ];
103
-
104
- const propertySectionDescriptions = {
105
- 文字: "字体、字号与字形",
106
- 段落: "对齐、间距与缩进",
107
- 分页: "段落分页行为",
108
- 表格: "字体、对齐与单元格留白",
109
- };
110
-
111
- initialize();
112
- document.body.dataset.view = activeView;
113
-
114
- styleSearch.addEventListener("input", filterStyleIndex);
115
-
116
- async function apiFetch(url, body) {
117
- const response = await fetch(
118
- url,
119
- body
120
- ? {
121
- method: "PUT",
122
- headers: { "Content-Type": "application/json" },
123
- body: JSON.stringify(body),
124
- }
125
- : void 0,
126
- );
127
- const result = body ? await response.json().catch(() => ({})) : void 0;
128
- if (!response.ok) throw new Error(result?.error ?? `${response.status} ${response.statusText}`);
129
- return result;
130
- }
131
-
132
- async function initialize() {
133
- try {
134
- const [configResponse, stylesResponse] = await Promise.all([
135
- fetch("/api/config"),
136
- fetch("/api/styles"),
137
- ]);
138
- if (!configResponse.ok) throw new Error("无法读取转换配置");
139
- if (!stylesResponse.ok) throw new Error("无法读取文档样式");
140
- ({ schema, config: originalConfig } = await configResponse.json());
141
- const styleData = await stylesResponse.json();
142
- delete originalConfig.$schema;
143
- currentConfig = structuredClone(originalConfig);
144
- originalStyles = styleData.styles;
145
- currentStyles = structuredClone(originalStyles);
146
- styleCatalog = styleData.catalog;
147
- styleRevision = styleData.revision;
148
- enforceHeadingDependency(originalConfig);
149
- enforceHeadingDependency(currentConfig);
150
- renderConfigForm();
151
- renderStyleIndex();
152
- selectFirstStyle();
153
- updateProof();
154
- updateView();
155
- } catch (error) {
156
- configForm.innerHTML = `<div class="loading error-copy">${escapeHtml(error.message)}</div>`;
157
- styleEditor.innerHTML = `<div class="loading error-copy">${escapeHtml(error.message)}</div>`;
158
- setStatus(error.message, true);
159
- }
160
- }
161
-
162
- document.querySelectorAll(".mode-tab").forEach((tab) => {
163
- tab.addEventListener("click", () => switchView(tab.dataset.view));
164
- });
165
-
166
- function switchView(view) {
167
- activeView = view;
168
- document.body.dataset.view = view;
169
- document
170
- .querySelectorAll(".mode-tab")
171
- .forEach((tab) => tab.classList.toggle("active", tab.dataset.view === view));
172
- document.querySelectorAll(".view").forEach((panel) => {
173
- const active = panel.id === `${view}-view`;
174
- panel.classList.toggle("active", active);
175
- panel.hidden = !active;
176
- });
177
- previewButton.hidden = view !== "styles";
178
- saveButton.textContent = view === "styles" ? "保存文档样式" : "保存转换配置";
179
- saveButton.removeAttribute("form");
180
- if (view === "config") saveButton.setAttribute("form", "config-form");
181
- updateView();
182
- }
183
-
184
- function renderConfigForm() {
185
- configForm.innerHTML = "";
186
- configIndex.innerHTML = "";
187
- for (const [groupName, groupSchema] of Object.entries(schema.properties)) {
188
- const title = groupSchema.description ?? groupName;
189
- const sectionId = `section-${groupName}`;
190
- const link = document.createElement("a");
191
- link.href = `#${sectionId}`;
192
- link.textContent = title;
193
- configIndex.append(link);
194
-
195
- const group = document.createElement("section");
196
- group.className = "config-group";
197
- group.id = sectionId;
198
- group.innerHTML = `<header><p>转换规则</p><h2>${escapeHtml(title)}</h2></header><div class="config-fields"></div>`;
199
- const fields = group.querySelector(".config-fields");
200
- for (const [name, fieldSchema] of Object.entries(groupSchema.properties)) {
201
- fields.append(createConfigField(groupName, name, fieldSchema));
202
- }
203
- configForm.append(group);
204
- }
205
- }
206
-
207
- function createConfigField(group, name, field) {
208
- const path = `${group}.${name}`;
209
- const div = document.createElement("div");
210
- div.className = "config-field";
211
- const description = field.description ?? name;
212
- div.innerHTML = `<label for="${path}">${escapeHtml(description)}</label>`;
213
- let input;
214
- if (field.type === "boolean") {
215
- const checked = currentConfig[group][name];
216
- div.innerHTML += `<label class="switch"><input type="checkbox" id="${path}" ${checked ? "checked" : ""}><span class="switch-track"></span></label>`;
217
- input = div.querySelector("input");
218
- input.addEventListener("change", () => {
219
- currentConfig[group][name] = input.checked;
220
- enforceHeadingDependency(currentConfig);
221
- syncHeadingDependencyControls();
222
- updateView();
223
- });
224
- } else {
225
- const wrapper = document.createElement("div");
226
- wrapper.className = "property-control";
227
- if (field.type === "string" || field.type === "text") {
228
- input = document.createElement("input");
229
- input.type = "text";
230
- input.id = path;
231
- input.value = currentConfig[group][name] ?? "";
232
- input.addEventListener("input", () => {
233
- currentConfig[group][name] = input.value;
234
- updateView();
235
- });
236
- } else {
237
- input = document.createElement("select");
238
- input.id = path;
239
- if (field.enum) {
240
- for (const option of field.enum) {
241
- const opt = document.createElement("option");
242
- opt.value = option;
243
- opt.textContent = option;
244
- if (currentConfig[group][name] === option) opt.selected = true;
245
- input.append(opt);
246
- }
247
- input.addEventListener("change", () => {
248
- currentConfig[group][name] = input.value;
249
- updateView();
250
- });
251
- } else if (field.type === "integer") {
252
- input = document.createElement("input");
253
- input.type = "number";
254
- input.id = path;
255
- input.min = field.minimum ?? 0;
256
- input.value = currentConfig[group][name] ?? "";
257
- input.addEventListener("input", () => {
258
- currentConfig[group][name] = input.value === "" ? "" : Number(input.value);
259
- updateView();
260
- });
261
- }
262
- }
263
- wrapper.append(input);
264
- div.append(wrapper);
265
- }
266
- return div;
267
- }
268
-
269
- function renderStyleIndex() {
270
- styleIndex.innerHTML = "";
271
- const bySection = {};
272
- for (const entry of styleCatalog ?? []) {
273
- const section = entry.section ?? "其他";
274
- if (!bySection[section]) bySection[section] = [];
275
- bySection[section].push(entry);
276
- }
277
- for (const [section, entries] of Object.entries(bySection)) {
278
- const group = document.createElement("div");
279
- group.className = section === "其他" ? "style-nav-group advanced-nav" : "style-nav-group";
280
- group.innerHTML = `<h2>${escapeHtml(section)}</h2><div></div>`;
281
- const list = group.querySelector("div");
282
- for (const entry of entries) {
283
- const button = document.createElement("button");
284
- button.type = "button";
285
- button.dataset.kind = entry.kind;
286
- button.dataset.key = entry.key;
287
- button.dataset.collection = entry.collection ?? "";
288
- button.dataset.id = entry.id ?? "";
289
- button.textContent = entry.label;
290
- button.addEventListener("click", () => selectStyle(button));
291
- list.append(button);
292
- }
293
- styleIndex.append(group);
294
- }
295
- document.querySelectorAll(`.style-nav-group button`).forEach((b) => b.classList.remove("active"));
296
- }
297
-
298
- function filterStyleIndex() {
299
- const query = styleSearch.value.toLowerCase();
300
- document.querySelectorAll(".style-nav-group").forEach((group) => {
301
- const match =
302
- query === "" ||
303
- Array.from(group.querySelectorAll("button")).some((btn) =>
304
- btn.textContent.toLowerCase().includes(query),
305
- );
306
- group.hidden = !match;
307
- });
308
- }
309
-
310
- function selectFirstStyle() {
311
- const first = styleIndex.querySelector("button");
312
- if (first) selectStyle(first);
313
- }
314
-
315
- function selectStyle(button) {
316
- selectedStyle = {
317
- kind: button.dataset.kind,
318
- collection: button.dataset.collection,
319
- id: button.dataset.id,
320
- key: button.dataset.key,
321
- };
322
- document.querySelectorAll(".style-nav-group button").forEach((b) => b.classList.remove("active"));
323
- button.classList.add("active");
324
- renderStyleEditor();
325
- updateProof();
326
- }
327
-
328
- function renderStyleEditor() {
329
- styleEditor.innerHTML = "";
330
- if (!selectedStyle) return;
331
- const entry = selectedEntry();
332
- const name = selectedStyle.key ?? "";
333
- const label = styleCatalog?.find((c) => c.key === name)?.label ?? name;
334
- const inheritedFrom = entry?.basedOn ? styleNameById(entry.basedOn) : null;
335
-
336
- const container = document.createElement("div");
337
- container.innerHTML = `<div class="editor-heading"><div><p class="eyebrow">正在编辑</p><h1>${escapeHtml(label)}</h1>${inheritedFrom ? `<p>基于 <strong>${escapeHtml(displayStyleName(inheritedFrom))}</strong></p>` : `<p>文档默认样式</p>`}</div>${inheritedFrom ? '<span class="inherit-chip">继承样式</span>' : ""}</div>`;
338
-
339
- if (selectedStyle.collection === "tableStyles") {
340
- container.append(createTableFields());
341
- } else {
342
- const groups = {};
343
- for (const field of styleFields) {
344
- if (!groups[field.section]) groups[field.section] = [];
345
- groups[field.section].push(field);
346
- }
347
- for (const [section, fields] of Object.entries(groups)) {
348
- const fieldset = createPropertyGroup(section, fields, entry);
349
- if (fieldset) container.append(fieldset);
350
- }
351
- }
352
- styleEditor.append(container);
353
- }
354
-
355
- function createStyleField(field, entry) {
356
- const explicit = deepGet(entry, field.path);
357
- const effective = effectiveValue(entry, field.path);
358
- const row = document.createElement("div");
359
- row.className = "property-row";
360
- const inherited = effective.source !== (entry.name ? displayStyleName(entry.name) : "文档默认");
361
-
362
- row.innerHTML = `<div class="property-label"><label for="${field.path.join(".")}">${escapeHtml(field.label)}</label><small>${inherited ? `继承自 ${escapeHtml(effective.source)}` : " "}</small></div>`;
363
- const control = document.createElement("div");
364
- control.className = "property-control";
365
- const input = makeStyleInput(field, entry);
366
- control.append(input);
367
- row.append(control);
368
- const reset = document.createElement("button");
369
- reset.className = "reset-property";
370
- reset.type = "button";
371
- reset.disabled = explicit === undefined;
372
- reset.textContent = "↺";
373
- reset.title = "恢复继承值";
374
- reset.addEventListener("click", () => {
375
- deepDelete(entry, field.path);
376
- renderStyleEditor();
377
- updateProof();
378
- updateView();
379
- });
380
- row.append(reset);
381
- return row;
382
- }
383
-
384
- function makeStyleInput(field, entry) {
385
- let input;
386
- const value = deepGet(entry, field.path);
387
- if (field.type === "boolean") {
388
- const wrapper = document.createElement("label");
389
- wrapper.className = "check-control";
390
- input = document.createElement("input");
391
- input.type = "checkbox";
392
- input.checked = value ?? false;
393
- input.addEventListener("change", () => {
394
- deepSet(entry, field.path, input.checked);
395
- renderStyleEditor();
396
- updateProof();
397
- updateView();
398
- });
399
- wrapper.append(
400
- input,
401
- field.mirror
402
- ? document.createTextNode("同步到等宽字号")
403
- : document.createTextNode(field.label),
404
- );
405
- return wrapper;
406
- }
407
- if (field.type === "color") {
408
- const wrapper = document.createElement("div");
409
- wrapper.className = "color-control";
410
- input = document.createElement("input");
411
- input.type = "color";
412
- input.value = normalizeColor(value);
413
- const text = document.createElement("input");
414
- text.type = "text";
415
- text.value = value ?? "";
416
- text.placeholder = "无";
417
- const apply = () => {
418
- deepSet(entry, field.path, text.value.toUpperCase());
419
- renderStyleEditor();
420
- updateProof();
421
- updateView();
422
- };
423
- input.addEventListener("input", () => {
424
- text.value = input.value.slice(1);
425
- apply();
426
- });
427
- text.addEventListener("change", apply);
428
- wrapper.append(input, text);
429
- return wrapper;
430
- }
431
- if (field.type === "select") {
432
- input = document.createElement("select");
433
- for (const [val, label] of field.options) {
434
- const opt = document.createElement("option");
435
- opt.value = val;
436
- opt.textContent = label;
437
- if (value === val || (!value && val === field.default)) opt.selected = true;
438
- input.append(opt);
439
- }
440
- input.addEventListener("change", () => {
441
- deepSet(entry, field.path, input.value || undefined);
442
- renderStyleEditor();
443
- updateProof();
444
- updateView();
445
- });
446
- return input;
447
- }
448
- if (field.type === "select-number") {
449
- input = document.createElement("select");
450
- for (const [val, label] of field.options) {
451
- const opt = document.createElement("option");
452
- opt.value = val;
453
- opt.textContent = label;
454
- if (value == val) opt.selected = true;
455
- input.append(opt);
456
- }
457
- const customOpt = document.createElement("option");
458
- customOpt.value = "custom";
459
- customOpt.textContent = "自定义";
460
- input.append(customOpt);
461
- input.addEventListener("change", () => {
462
- if (input.value !== "custom") deepSet(entry, field.path, Number(input.value));
463
- renderStyleEditor();
464
- updateProof();
465
- updateView();
466
- });
467
- // Show custom input if current value not in options
468
- const inOptions = field.options.some(([v]) => v == value);
469
- if (value !== undefined && !inOptions) {
470
- const wrapper = document.createElement("div");
471
- wrapper.className = "unit-control";
472
- wrapper.append(input);
473
- const customInput = document.createElement("input");
474
- customInput.type = "number";
475
- customInput.value = value;
476
- customInput.step = field.step ?? 1;
477
- customInput.addEventListener("change", () => {
478
- deepSet(entry, field.path, Number(customInput.value));
479
- renderStyleEditor();
480
- updateProof();
481
- updateView();
482
- });
483
- wrapper.append(customInput);
484
- if (field.unit) {
485
- const unit = document.createElement("span");
486
- unit.textContent = field.unit;
487
- wrapper.append(unit);
488
- }
489
- return wrapper;
490
- }
491
- return input;
492
- }
493
- if (field.unit) {
494
- const wrapper = document.createElement("div");
495
- wrapper.className = "unit-control";
496
- input = document.createElement("input");
497
- input.type = "number";
498
- input.min = field.min;
499
- input.step = field.step ?? 1;
500
- input.placeholder = "无";
501
- const raw = value !== undefined ? value / field.scale : "";
502
- // Preserve decimals for small values
503
- input.value = field.scale === 100 && raw === 0 ? "0" : raw === "" ? "" : String(raw);
504
- const apply = () => {
505
- const v = input.value === "" ? undefined : Math.round(Number(input.value) * field.scale);
506
- deepSet(entry, field.path, v);
507
- updateProof();
508
- updateView();
509
- };
510
- input.addEventListener("change", apply);
511
- wrapper.append(input);
512
- if (field.unit) {
513
- const unit = document.createElement("span");
514
- unit.textContent = field.unit;
515
- wrapper.append(unit);
516
- }
517
- return wrapper;
518
- }
519
- input = document.createElement("input");
520
- input.type = "text";
521
- input.value = value ?? "";
522
- input.placeholder = "继承";
523
- input.addEventListener("change", () => {
524
- deepSet(entry, field.path, input.value || undefined);
525
- renderStyleEditor();
526
- updateProof();
527
- updateView();
528
- });
529
- return input;
530
- }
531
-
532
- function createTableFields() {
533
- const fieldset = document.createElement("div");
534
- const entry = selectedEntry();
535
- if (!entry) return fieldset;
536
-
537
- const definitions = [
538
- { label: "中文字体", path: ["run", "font", "eastAsia"], type: "text" },
539
- {
540
- label: "西文字体",
541
- path: ["run", "font", "ascii"],
542
- type: "text",
543
- mirror: ["run", "font", "hAnsi"],
544
- },
545
- {
546
- label: "字号",
547
- path: ["run", "size"],
548
- type: "number",
549
- unit: "pt",
550
- scale: 2,
551
- min: 1,
552
- step: 0.5,
553
- mirror: ["run", "sizeComplexScript"],
554
- },
555
- {
556
- label: "表格对齐",
557
- path: ["table", "alignment"],
558
- type: "select",
559
- options: [
560
- ["left", "左对齐"],
561
- ["center", "居中"],
562
- ["right", "右对齐"],
563
- ],
564
- },
565
- {
566
- label: "表格字号",
567
- path: ["run", "size"],
568
- type: "number",
569
- unit: "pt",
570
- scale: 2,
571
- min: 1,
572
- step: 0.5,
573
- mirror: ["run", "sizeComplexScript"],
574
- },
575
- {
576
- label: "上边距",
577
- path: ["table", "cellMargin", "top"],
578
- type: "number",
579
- unit: "pt",
580
- scale: 20,
581
- min: 0,
582
- step: 1,
583
- default: 0,
584
- },
585
- {
586
- label: "下边距",
587
- path: ["table", "cellMargin", "bottom"],
588
- type: "number",
589
- unit: "pt",
590
- scale: 20,
591
- min: 0,
592
- step: 1,
593
- default: 0,
594
- },
595
- {
596
- label: "左边距",
597
- path: ["table", "cellMargin", "left"],
598
- type: "number",
599
- unit: "pt",
600
- scale: 20,
601
- min: 0,
602
- step: 1,
603
- default: 0,
604
- },
605
- {
606
- label: "右边距",
607
- path: ["table", "cellMargin", "right"],
608
- type: "number",
609
- unit: "pt",
610
- scale: 20,
611
- min: 0,
612
- step: 1,
613
- default: 0,
614
- },
615
- ];
616
- for (const def of definitions) {
617
- const field = createStyleField(def, entry);
618
- if (field) fieldset.append(field);
619
- }
620
- return fieldset;
621
- }
622
-
623
- function createPropertyGroup(section, fields, entry) {
624
- if (!fields.length) return null;
625
- const fieldset = document.createElement("fieldset");
626
- fieldset.className = "property-group";
627
- fieldset.innerHTML = `<legend>${escapeHtml(section)}</legend><p class="group-note">${escapeHtml(propertySectionDescriptions[section] ?? "")}</p>`;
628
- for (const field of fields) fieldset.append(createStyleField(field, entry));
629
- return fieldset;
630
- }
631
-
632
- // Event listeners
633
- resetButton.addEventListener("click", () => {
634
- if (activeView === "config") {
635
- currentConfig = structuredClone(originalConfig);
636
- renderConfigForm();
637
- } else {
638
- currentStyles = structuredClone(originalStyles);
639
- renderStyleIndex();
640
- selectFirstStyle();
641
- updateProof();
642
- }
643
- updateView();
644
- });
645
-
646
- saveButton.addEventListener("click", async (event) => {
647
- if (activeView === "styles") {
648
- event.preventDefault();
649
- setBusy(true, "正在验证样式并重建 Word 模板…");
650
- try {
651
- const result = await apiFetch("/api/styles", {
652
- styles: currentStyles,
653
- revision: styleRevision,
654
- });
655
- originalStyles = structuredClone(currentStyles);
656
- styleRevision = result.revision;
657
- setStatus("文档样式已保存,Word 模板已重建");
658
- } catch (error) {
659
- setStatus(error.message, true);
660
- } finally {
661
- setBusy(false);
662
- updateView();
663
- }
664
- }
665
- });
666
-
667
- document.querySelector("#config-form")?.addEventListener("submit", async (event) => {
668
- event.preventDefault();
669
- setBusy(true, "正在保存转换配置…");
670
- try {
671
- await apiFetch("/api/config", currentConfig);
672
- originalConfig = structuredClone(currentConfig);
673
- setStatus("转换配置已保存");
674
- } catch (error) {
675
- setStatus(error.message, true);
676
- } finally {
677
- setBusy(false);
678
- updateView();
679
- }
680
- });
681
-
682
- previewButton.addEventListener("click", async () => {
683
- setBusy(true, "正在生成真实 DOCX 校样…");
684
- try {
685
- const response = await fetch("/api/styles/preview", {
686
- method: "POST",
687
- headers: { "Content-Type": "application/json" },
688
- body: JSON.stringify({ styles: currentStyles }),
689
- });
690
- if (!response.ok) {
691
- const result = await response.json().catch(() => ({}));
692
- throw new Error(result.error ?? "预览生成失败");
693
- }
694
- const url = URL.createObjectURL(await response.blob());
695
- const link = document.createElement("a");
696
- link.href = url;
697
- link.download = "md2docx-style-preview.docx";
698
- link.click();
699
- URL.revokeObjectURL(url);
700
- setStatus("真实 DOCX 校样已生成");
701
- } catch (error) {
702
- setStatus(error.message, true);
703
- } finally {
704
- setBusy(false);
705
- updateView();
706
- }
707
- });
708
-
709
- function updateView() {
710
- if (!originalConfig || !originalStyles) return;
711
- const changed =
712
- activeView === "config"
713
- ? JSON.stringify(originalConfig) !== JSON.stringify(currentConfig)
714
- : JSON.stringify(originalStyles) !== JSON.stringify(currentStyles);
715
- saveButton.disabled = !changed;
716
- resetButton.disabled = !changed;
717
- if (changed)
718
- setStatus(activeView === "config" ? "转换配置有尚未保存的修改" : "文档样式有尚未保存的修改");
719
- else if (!status.textContent.includes("已保存") && !status.textContent.includes("已生成"))
720
- setStatus(activeView === "config" ? "转换配置尚未修改" : "文档样式尚未修改");
721
- }
722
-
723
- function updateProof() {
724
- if (!currentStyles) return;
725
- document
726
- .querySelectorAll("[data-proof]")
727
- .forEach((node) => applyProofStyle(node, node.dataset.proof));
728
- }
729
-
730
- function applyProofStyle(node, name) {
731
- const style = findStyleAny(name);
732
- if (!style) return;
733
- const font =
734
- effectiveValue(style, ["run", "font", "eastAsia"]).value ??
735
- effectiveValue(style, ["run", "font", "ascii"]).value;
736
- const size = effectiveValue(style, ["run", "size"]).value;
737
- const color = effectiveValue(style, ["run", "color"]).value;
738
- const bold = effectiveValue(style, ["run", "bold"]).value;
739
- const italics = effectiveValue(style, ["run", "italics"]).value;
740
- const align = effectiveValue(style, ["paragraph", "alignment"]).value;
741
- const line = effectiveValue(style, ["paragraph", "spacing", "line"]).value;
742
- const before = effectiveValue(style, ["paragraph", "spacing", "before"]).value;
743
- const after = effectiveValue(style, ["paragraph", "spacing", "after"]).value;
744
- const indent = effectiveValue(style, ["paragraph", "indent", "firstLineChars"]).value;
745
- node.style.fontFamily = font ? `"${font}", serif` : "";
746
- node.style.fontSize = size ? `${size / 2}pt` : "";
747
- node.style.color = color && color !== "auto" ? `#${color}` : "";
748
- node.style.fontWeight = bold ? "700" : "400";
749
- node.style.fontStyle = italics ? "italic" : "normal";
750
- node.style.textAlign = align === "both" ? "justify" : (align ?? "");
751
- node.style.lineHeight = line ? String(line / 240) : "";
752
- node.style.marginTop = before !== undefined ? `${before / 20}pt` : "";
753
- node.style.marginBottom = after !== undefined ? `${after / 20}pt` : "";
754
- node.style.textIndent = indent ? `${indent / 100}em` : "";
755
- }
756
-
757
- function selectedEntry() {
758
- if (selectedStyle.kind === "default") return currentStyles.default?.document;
759
- if (selectedStyle.kind === "style")
760
- return collectionEntries(selectedStyle.collection).find(
761
- (entry) => entry.id === selectedStyle.id,
762
- );
763
- return null;
764
- }
765
-
766
- function findStyleAny(name) {
767
- return ["paragraphStyles", "characterStyles", "tableStyles"]
768
- .flatMap(collectionEntries)
769
- .find((entry) => entry.name === name);
770
- }
771
- function collectionEntries(collection) {
772
- return Array.isArray(currentStyles?.[collection]) ? currentStyles[collection] : [];
773
- }
774
- function styleNameById(id) {
775
- return (
776
- ["paragraphStyles", "characterStyles", "tableStyles"]
777
- .flatMap(collectionEntries)
778
- .find((entry) => entry.id === id)?.name ?? id
779
- );
780
- }
781
-
782
- function effectiveValue(entry, path, visited = new Set()) {
783
- const explicit = deepGet(entry, path);
784
- if (explicit !== undefined)
785
- return { value: explicit, source: displayStyleName(entry.name ?? "文档默认") };
786
- if (entry.basedOn && !visited.has(entry.basedOn)) {
787
- visited.add(entry.basedOn);
788
- const parent = ["paragraphStyles", "characterStyles", "tableStyles"]
789
- .flatMap(collectionEntries)
790
- .find((candidate) => candidate.id === entry.basedOn);
791
- if (parent) {
792
- const inherited = effectiveValue(parent, path, visited);
793
- if (inherited.value !== undefined) return inherited;
794
- }
795
- }
796
- const fallback = deepGet(currentStyles.default?.document, path);
797
- return { value: fallback, source: "文档默认" };
798
- }
799
-
800
- function displayStyleName(name) {
801
- const labels = {
802
- "heading 1": "一级标题",
803
- "heading 2": "二级标题",
804
- "heading 3": "三级标题",
805
- "heading 4": "四级标题",
806
- "heading 5": "五级标题",
807
- "heading 6": "六级标题",
808
- "TOC Heading": "目录标题",
809
- "Body Text": "正文",
810
- "First Paragraph": "首段正文",
811
- "Block Text": "块引用",
812
- "Source Code": "代码块",
813
- "Inline Code": "行内代码",
814
- "Table Caption": "表格题注",
815
- "Image Caption": "图片题注",
816
- "Captioned Figure": "带题注图片",
817
- "footnote text": "脚注正文",
818
- "footnote reference": "脚注引用",
819
- header: "页眉",
820
- footer: "页脚",
821
- caption: "通用题注",
822
- };
823
- return labels[name] ?? name;
824
- }
825
-
826
- function deepGet(source, path) {
827
- return path.reduce((value, key) => value?.[key], source);
828
- }
829
- function deepSet(source, path, value) {
830
- let current = source;
831
- for (const key of path.slice(0, -1)) {
832
- if (!current[key] || typeof current[key] !== "object") current[key] = {};
833
- current = current[key];
834
- }
835
- current[path.at(-1)] = value;
836
- }
837
- function deepDelete(source, path) {
838
- const parents = [source];
839
- let current = source;
840
- for (const key of path.slice(0, -1)) {
841
- current = current?.[key];
842
- if (!current) return;
843
- parents.push(current);
844
- }
845
- delete current[path.at(-1)];
846
- for (let index = path.length - 2; index >= 0; index--) {
847
- const parent = parents[index];
848
- const key = path[index];
849
- if (parent[key] && Object.keys(parent[key]).length === 0) delete parent[key];
850
- else break;
851
- }
852
- }
853
- function normalizeColor(value) {
854
- return typeof value === "string" && /^[0-9a-f]{6}$/i.test(value) ? `#${value}` : "#202B3C";
855
- }
856
- function enforceHeadingDependency(config) {
857
- if (config.numberHeadings.enabled) config.normalizeHeadings.enabled = true;
858
- }
859
- function syncHeadingDependencyControls() {
860
- const input = document.getElementById("normalizeHeadings.enabled");
861
- input.checked = currentConfig.normalizeHeadings.enabled;
862
- input.disabled = currentConfig.numberHeadings.enabled;
863
- input.title = currentConfig.numberHeadings.enabled ? "标题编号开启时无法关闭" : "";
864
- }
865
- function setBusy(busy, message) {
866
- saveButton.disabled = busy;
867
- resetButton.disabled = busy;
868
- previewButton.disabled = busy;
869
- if (message) setStatus(message);
870
- }
871
- function setStatus(message, error = false) {
872
- status.textContent = message;
873
- status.classList.toggle("error-copy", error);
874
- statusIndicator.classList.toggle("error", error);
875
- }
876
- function escapeHtml(value) {
877
- const node = document.createElement("span");
878
- node.textContent = value;
879
- return node.innerHTML;
880
- }