@lotics/docx 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.
Files changed (107) hide show
  1. package/package.json +40 -0
  2. package/src/fixtures/.gitkeep +0 -0
  3. package/src/fixtures/lotics_generated_contract.docx +0 -0
  4. package/src/fonts/bundled.ts +123 -0
  5. package/src/fonts/registry.test.ts +233 -0
  6. package/src/fonts/registry.ts +219 -0
  7. package/src/fonts/types.ts +83 -0
  8. package/src/index.ts +16 -0
  9. package/src/layout/engine.test.ts +430 -0
  10. package/src/layout/engine.ts +566 -0
  11. package/src/layout/page_geometry.ts +43 -0
  12. package/src/layout/types.ts +159 -0
  13. package/src/load.test.ts +144 -0
  14. package/src/load.ts +142 -0
  15. package/src/model/default_numbering.ts +101 -0
  16. package/src/model/default_styles.ts +201 -0
  17. package/src/model/numbering_table.ts +52 -0
  18. package/src/model/properties.ts +328 -0
  19. package/src/model/sections.ts +94 -0
  20. package/src/model/style_resolution.test.ts +219 -0
  21. package/src/model/style_resolution.ts +113 -0
  22. package/src/model/style_table.ts +22 -0
  23. package/src/model/theme.ts +156 -0
  24. package/src/model/types.ts +55 -0
  25. package/src/parse/drawing.ts +157 -0
  26. package/src/parse/font_table.ts +132 -0
  27. package/src/parse/footnotes.ts +60 -0
  28. package/src/parse/header_footer.test.ts +264 -0
  29. package/src/parse/header_footer.ts +66 -0
  30. package/src/parse/numbering.ts +187 -0
  31. package/src/parse/parser.ts +184 -0
  32. package/src/parse/relationships.ts +83 -0
  33. package/src/parse/sections.test.ts +192 -0
  34. package/src/parse/sections.ts +182 -0
  35. package/src/parse/styles.ts +149 -0
  36. package/src/parse/theme.test.ts +86 -0
  37. package/src/parse/theme.ts +112 -0
  38. package/src/pm/bubble_menu.ts +117 -0
  39. package/src/pm/commands.test.ts +185 -0
  40. package/src/pm/commands.ts +697 -0
  41. package/src/pm/commands_insert.test.ts +183 -0
  42. package/src/pm/docx_to_pm.test.ts +330 -0
  43. package/src/pm/docx_to_pm.ts +643 -0
  44. package/src/pm/drag_handle.ts +166 -0
  45. package/src/pm/format_painter.test.ts +91 -0
  46. package/src/pm/format_painter.ts +109 -0
  47. package/src/pm/header_footer_doc.ts +24 -0
  48. package/src/pm/hyperlinks.test.ts +234 -0
  49. package/src/pm/image_registry.test.ts +81 -0
  50. package/src/pm/image_registry.ts +100 -0
  51. package/src/pm/images.test.ts +257 -0
  52. package/src/pm/link_popover.ts +159 -0
  53. package/src/pm/mark_commands.ts +60 -0
  54. package/src/pm/marks.ts +169 -0
  55. package/src/pm/nodes.ts +258 -0
  56. package/src/pm/numbering.test.ts +210 -0
  57. package/src/pm/numbering_plugin.test.ts +71 -0
  58. package/src/pm/numbering_plugin.ts +96 -0
  59. package/src/pm/outline.ts +41 -0
  60. package/src/pm/page_break.test.ts +80 -0
  61. package/src/pm/page_layout.test.ts +87 -0
  62. package/src/pm/pagination_plugin.test.ts +155 -0
  63. package/src/pm/pagination_plugin.ts +590 -0
  64. package/src/pm/phase5.test.ts +271 -0
  65. package/src/pm/phase6.test.ts +215 -0
  66. package/src/pm/placeholder_plugin.ts +24 -0
  67. package/src/pm/plugins.ts +91 -0
  68. package/src/pm/pm_to_docx.ts +0 -0
  69. package/src/pm/roundtrip.test.ts +332 -0
  70. package/src/pm/schema.test.ts +188 -0
  71. package/src/pm/schema.ts +79 -0
  72. package/src/pm/search.ts +46 -0
  73. package/src/pm/table_attrs.ts +48 -0
  74. package/src/pm/table_borders.test.ts +117 -0
  75. package/src/pm/table_borders.ts +130 -0
  76. package/src/pm/table_convert.test.ts +221 -0
  77. package/src/pm/table_convert.ts +541 -0
  78. package/src/pm/table_decorations.ts +132 -0
  79. package/src/pm/table_handles.ts +163 -0
  80. package/src/pm/template_marker.ts +47 -0
  81. package/src/pm/template_plugin.ts +65 -0
  82. package/src/pm/templates.test.ts +162 -0
  83. package/src/render/clipboard.test.ts +115 -0
  84. package/src/render/clipboard.ts +200 -0
  85. package/src/render/editable_view.test.ts +173 -0
  86. package/src/render/footnotes_view.ts +94 -0
  87. package/src/render/header_footer_view.ts +95 -0
  88. package/src/render/link_mark_view.ts +26 -0
  89. package/src/render/media_resolver.ts +61 -0
  90. package/src/render/node_views.ts +296 -0
  91. package/src/render/numbering_counter.ts +149 -0
  92. package/src/render/page_chrome.test.ts +262 -0
  93. package/src/render/page_chrome.ts +343 -0
  94. package/src/render/page_styles.ts +234 -0
  95. package/src/render/paragraph_view.test.ts +162 -0
  96. package/src/render/paragraph_view.ts +141 -0
  97. package/src/render/ruler.ts +110 -0
  98. package/src/render/style_registry.ts +33 -0
  99. package/src/render/table_dom.test.ts +171 -0
  100. package/src/render/table_dom.ts +288 -0
  101. package/src/render/units.ts +18 -0
  102. package/src/render/view.test.ts +165 -0
  103. package/src/render/view.ts +607 -0
  104. package/src/roundtrip.test.ts +179 -0
  105. package/src/serialize/default_parts.ts +128 -0
  106. package/src/serialize/header_footer_pm.ts +82 -0
  107. package/src/serialize/serializer.ts +114 -0
@@ -0,0 +1,219 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseXml, getTagName, getChildren } from "@lotics/ooxml/xml";
3
+ import {
4
+ EMPTY_PARAGRAPH_PROPERTIES,
5
+ EMPTY_RUN_PROPERTIES,
6
+ extractParagraphProperties,
7
+ extractRunProperties,
8
+ } from "./properties";
9
+ import { parseStyleTable } from "../parse/styles";
10
+ import {
11
+ resolveParagraphProperties,
12
+ resolveRunProperties,
13
+ } from "./style_resolution";
14
+ import type { StyleTable } from "./style_table";
15
+
16
+ function partsFromStylesXml(xml: string): Map<string, Uint8Array> {
17
+ return new Map([
18
+ ["word/styles.xml", new TextEncoder().encode(xml)],
19
+ ]);
20
+ }
21
+
22
+ const STYLES_XML = `<?xml version="1.0"?>
23
+ <w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
24
+ <w:docDefaults>
25
+ <w:rPrDefault>
26
+ <w:rPr>
27
+ <w:rFonts w:ascii="Calibri" w:hAnsi="Calibri"/>
28
+ <w:sz w:val="22"/>
29
+ </w:rPr>
30
+ </w:rPrDefault>
31
+ <w:pPrDefault>
32
+ <w:pPr>
33
+ <w:spacing w:after="160" w:line="259" w:lineRule="auto"/>
34
+ </w:pPr>
35
+ </w:pPrDefault>
36
+ </w:docDefaults>
37
+ <w:style w:type="paragraph" w:styleId="Normal" w:default="1">
38
+ <w:name w:val="Normal"/>
39
+ </w:style>
40
+ <w:style w:type="paragraph" w:styleId="Heading1">
41
+ <w:name w:val="heading 1"/>
42
+ <w:basedOn w:val="Normal"/>
43
+ <w:pPr>
44
+ <w:keepNext/>
45
+ <w:spacing w:before="240" w:after="0"/>
46
+ <w:outlineLvl w:val="0"/>
47
+ </w:pPr>
48
+ <w:rPr>
49
+ <w:b/>
50
+ <w:sz w:val="32"/>
51
+ </w:rPr>
52
+ </w:style>
53
+ <w:style w:type="paragraph" w:styleId="Heading2">
54
+ <w:name w:val="heading 2"/>
55
+ <w:basedOn w:val="Heading1"/>
56
+ <w:pPr>
57
+ <w:outlineLvl w:val="1"/>
58
+ </w:pPr>
59
+ <w:rPr>
60
+ <w:sz w:val="26"/>
61
+ </w:rPr>
62
+ </w:style>
63
+ <w:style w:type="character" w:styleId="Emphasis">
64
+ <w:name w:val="Emphasis"/>
65
+ <w:rPr>
66
+ <w:i/>
67
+ </w:rPr>
68
+ </w:style>
69
+ </w:styles>`;
70
+
71
+ function loadTable(): StyleTable {
72
+ return parseStyleTable(partsFromStylesXml(STYLES_XML));
73
+ }
74
+
75
+ describe("parseStyleTable", () => {
76
+ it("extracts doc defaults", () => {
77
+ const table = loadTable();
78
+ expect(table.runDefaults.fontAscii).toBe("Calibri");
79
+ expect(table.runDefaults.size).toBe(22);
80
+ expect(table.paragraphDefaults.spacing).toEqual({
81
+ before: null,
82
+ after: 160,
83
+ line: 259,
84
+ lineRule: "auto",
85
+ });
86
+ });
87
+
88
+ it("extracts styles with type, basedOn, link, pPr, rPr", () => {
89
+ const table = loadTable();
90
+ expect(table.styles.size).toBe(4);
91
+ const heading1 = table.styles.get("Heading1")!;
92
+ expect(heading1.type).toBe("paragraph");
93
+ expect(heading1.basedOn).toBe("Normal");
94
+ expect(heading1.runProperties?.bold).toBe(true);
95
+ expect(heading1.runProperties?.size).toBe(32);
96
+ expect(heading1.paragraphProperties?.keepNext).toBe(true);
97
+ expect(heading1.paragraphProperties?.outlineLevel).toBe(0);
98
+
99
+ const emphasis = table.styles.get("Emphasis")!;
100
+ expect(emphasis.type).toBe("character");
101
+ expect(emphasis.runProperties?.italic).toBe(true);
102
+ });
103
+
104
+ it("identifies the default paragraph style", () => {
105
+ const table = loadTable();
106
+ expect(table.defaultParagraphStyleId).toBe("Normal");
107
+ });
108
+ });
109
+
110
+ describe("resolveParagraphProperties", () => {
111
+ it("applies defaults when no style or direct properties", () => {
112
+ const table = loadTable();
113
+ const resolved = resolveParagraphProperties(table, {
114
+ ...EMPTY_PARAGRAPH_PROPERTIES,
115
+ });
116
+ expect(resolved.spacing?.after).toBe(160);
117
+ expect(resolved.spacing?.line).toBe(259);
118
+ });
119
+
120
+ it("walks the basedOn chain and overrides", () => {
121
+ const table = loadTable();
122
+ const resolved = resolveParagraphProperties(table, {
123
+ ...EMPTY_PARAGRAPH_PROPERTIES,
124
+ styleId: "Heading2",
125
+ });
126
+ expect(resolved.spacing?.before).toBe(240);
127
+ expect(resolved.spacing?.after).toBe(0);
128
+ expect(resolved.outlineLevel).toBe(1);
129
+ expect(resolved.keepNext).toBe(true);
130
+ expect(resolved.styleId).toBe("Heading2");
131
+ });
132
+
133
+ it("direct properties override style chain", () => {
134
+ const table = loadTable();
135
+ const resolved = resolveParagraphProperties(table, {
136
+ ...EMPTY_PARAGRAPH_PROPERTIES,
137
+ styleId: "Heading1",
138
+ alignment: "center",
139
+ });
140
+ expect(resolved.alignment).toBe("center");
141
+ expect(resolved.outlineLevel).toBe(0);
142
+ });
143
+ });
144
+
145
+ describe("resolveRunProperties", () => {
146
+ it("applies run defaults", () => {
147
+ const table = loadTable();
148
+ const resolved = resolveRunProperties(table, null, {
149
+ ...EMPTY_RUN_PROPERTIES,
150
+ });
151
+ expect(resolved.fontAscii).toBe("Calibri");
152
+ expect(resolved.size).toBe(22);
153
+ });
154
+
155
+ it("inherits from paragraph style's run properties", () => {
156
+ const table = loadTable();
157
+ const resolved = resolveRunProperties(table, "Heading1", {
158
+ ...EMPTY_RUN_PROPERTIES,
159
+ });
160
+ expect(resolved.bold).toBe(true);
161
+ expect(resolved.size).toBe(32);
162
+ expect(resolved.fontAscii).toBe("Calibri");
163
+ });
164
+
165
+ it("character style overrides paragraph style", () => {
166
+ const table = loadTable();
167
+ const resolved = resolveRunProperties(table, "Heading1", {
168
+ ...EMPTY_RUN_PROPERTIES,
169
+ styleId: "Emphasis",
170
+ });
171
+ expect(resolved.bold).toBe(true);
172
+ expect(resolved.italic).toBe(true);
173
+ expect(resolved.size).toBe(32);
174
+ });
175
+
176
+ it("direct properties override everything", () => {
177
+ const table = loadTable();
178
+ const resolved = resolveRunProperties(table, "Heading1", {
179
+ ...EMPTY_RUN_PROPERTIES,
180
+ bold: false,
181
+ size: 18,
182
+ });
183
+ expect(resolved.bold).toBe(false);
184
+ expect(resolved.size).toBe(18);
185
+ });
186
+ });
187
+
188
+ describe("extract* property helpers", () => {
189
+ it("treats <w:b/> as toggle on, <w:b w:val=\"0\"/> as off", () => {
190
+ const xmlOn = parseXml(`<w:rPr xmlns:w="x"><w:b/></w:rPr>`)[0];
191
+ const xmlOff = parseXml(
192
+ `<w:rPr xmlns:w="x"><w:b w:val="0"/></w:rPr>`,
193
+ )[0];
194
+ expect(getTagName(xmlOn)).toBe("w:rPr");
195
+ expect(extractRunProperties(getChildren(xmlOn)).bold).toBe(true);
196
+ expect(extractRunProperties(getChildren(xmlOff)).bold).toBe(false);
197
+ });
198
+
199
+ it("parses indent attributes including start/end aliases", () => {
200
+ const el = parseXml(
201
+ `<w:pPr xmlns:w="x"><w:ind w:start="720" w:firstLine="360"/></w:pPr>`,
202
+ )[0];
203
+ const props = extractParagraphProperties(getChildren(el));
204
+ expect(props.indent).toEqual({
205
+ left: 720,
206
+ right: null,
207
+ firstLine: 360,
208
+ hanging: null,
209
+ });
210
+ });
211
+
212
+ it("parses numPr (numId + ilvl)", () => {
213
+ const el = parseXml(
214
+ `<w:pPr xmlns:w="x"><w:numPr><w:ilvl w:val="2"/><w:numId w:val="3"/></w:numPr></w:pPr>`,
215
+ )[0];
216
+ const props = extractParagraphProperties(getChildren(el));
217
+ expect(props.numbering).toEqual({ numId: 3, ilvl: 2 });
218
+ });
219
+ });
@@ -0,0 +1,113 @@
1
+ import {
2
+ EMPTY_PARAGRAPH_PROPERTIES,
3
+ EMPTY_RUN_PROPERTIES,
4
+ type ParagraphProperties,
5
+ type RunProperties,
6
+ } from "./properties";
7
+ import type { Style, StyleTable } from "./style_table";
8
+
9
+ function mergeParagraph(
10
+ base: ParagraphProperties,
11
+ overlay: ParagraphProperties,
12
+ ): ParagraphProperties {
13
+ return {
14
+ styleId: overlay.styleId ?? base.styleId,
15
+ alignment: overlay.alignment ?? base.alignment,
16
+ indent: overlay.indent ?? base.indent,
17
+ spacing: overlay.spacing ?? base.spacing,
18
+ numbering: overlay.numbering ?? base.numbering,
19
+ pageBreakBefore: overlay.pageBreakBefore ?? base.pageBreakBefore,
20
+ keepLines: overlay.keepLines ?? base.keepLines,
21
+ keepNext: overlay.keepNext ?? base.keepNext,
22
+ widowControl: overlay.widowControl ?? base.widowControl,
23
+ outlineLevel: overlay.outlineLevel ?? base.outlineLevel,
24
+ };
25
+ }
26
+
27
+ function mergeRun(base: RunProperties, overlay: RunProperties): RunProperties {
28
+ return {
29
+ styleId: overlay.styleId ?? base.styleId,
30
+ bold: overlay.bold ?? base.bold,
31
+ italic: overlay.italic ?? base.italic,
32
+ underline: overlay.underline ?? base.underline,
33
+ strike: overlay.strike ?? base.strike,
34
+ doubleStrike: overlay.doubleStrike ?? base.doubleStrike,
35
+ caps: overlay.caps ?? base.caps,
36
+ smallCaps: overlay.smallCaps ?? base.smallCaps,
37
+ color: overlay.color ?? base.color,
38
+ themeColor: overlay.themeColor ?? base.themeColor,
39
+ themeTint: overlay.themeTint ?? base.themeTint,
40
+ themeShade: overlay.themeShade ?? base.themeShade,
41
+ highlight: overlay.highlight ?? base.highlight,
42
+ size: overlay.size ?? base.size,
43
+ fontAscii: overlay.fontAscii ?? base.fontAscii,
44
+ fontHAnsi: overlay.fontHAnsi ?? base.fontHAnsi,
45
+ fontEastAsia: overlay.fontEastAsia ?? base.fontEastAsia,
46
+ fontComplexScript: overlay.fontComplexScript ?? base.fontComplexScript,
47
+ vertAlign: overlay.vertAlign ?? base.vertAlign,
48
+ };
49
+ }
50
+
51
+ function styleChain(table: StyleTable, styleId: string | null): Style[] {
52
+ if (!styleId) return [];
53
+ const chain: Style[] = [];
54
+ const visited = new Set<string>();
55
+ let current: string | null = styleId;
56
+ while (current && !visited.has(current)) {
57
+ visited.add(current);
58
+ const style = table.styles.get(current);
59
+ if (!style) break;
60
+ chain.unshift(style);
61
+ current = style.basedOn;
62
+ }
63
+ return chain;
64
+ }
65
+
66
+ export function resolveParagraphProperties(
67
+ table: StyleTable,
68
+ direct: ParagraphProperties,
69
+ ): ParagraphProperties {
70
+ let result = { ...table.paragraphDefaults };
71
+
72
+ const referencedStyleId = direct.styleId ?? table.defaultParagraphStyleId;
73
+ for (const style of styleChain(table, referencedStyleId)) {
74
+ if (style.paragraphProperties) {
75
+ result = mergeParagraph(result, style.paragraphProperties);
76
+ }
77
+ }
78
+
79
+ result = mergeParagraph(result, direct);
80
+ return result;
81
+ }
82
+
83
+ export function resolveRunProperties(
84
+ table: StyleTable,
85
+ paragraphStyleId: string | null,
86
+ directRun: RunProperties,
87
+ ): RunProperties {
88
+ let result = { ...table.runDefaults };
89
+
90
+ for (const style of styleChain(table, paragraphStyleId)) {
91
+ if (style.runProperties) {
92
+ result = mergeRun(result, style.runProperties);
93
+ }
94
+ }
95
+
96
+ const characterStyleId = directRun.styleId;
97
+ for (const style of styleChain(table, characterStyleId)) {
98
+ if (style.runProperties) {
99
+ result = mergeRun(result, style.runProperties);
100
+ }
101
+ }
102
+
103
+ result = mergeRun(result, directRun);
104
+ return result;
105
+ }
106
+
107
+ export function emptyResolvedParagraphProperties(): ParagraphProperties {
108
+ return { ...EMPTY_PARAGRAPH_PROPERTIES };
109
+ }
110
+
111
+ export function emptyResolvedRunProperties(): RunProperties {
112
+ return { ...EMPTY_RUN_PROPERTIES };
113
+ }
@@ -0,0 +1,22 @@
1
+ import type { ParagraphProperties, RunProperties } from "./properties";
2
+
3
+ export type StyleType = "paragraph" | "character" | "table" | "numbering";
4
+
5
+ export type Style = {
6
+ id: string;
7
+ name: string | null;
8
+ type: StyleType;
9
+ basedOn: string | null;
10
+ linkedStyleId: string | null;
11
+ paragraphProperties: ParagraphProperties | null;
12
+ runProperties: RunProperties | null;
13
+ isDefault: boolean;
14
+ };
15
+
16
+ export type StyleTable = {
17
+ paragraphDefaults: ParagraphProperties;
18
+ runDefaults: RunProperties;
19
+ styles: ReadonlyMap<string, Style>;
20
+ defaultParagraphStyleId: string | null;
21
+ defaultCharacterStyleId: string | null;
22
+ };
@@ -0,0 +1,156 @@
1
+ export type ThemeColorName =
2
+ | "dk1"
3
+ | "lt1"
4
+ | "dk2"
5
+ | "lt2"
6
+ | "accent1"
7
+ | "accent2"
8
+ | "accent3"
9
+ | "accent4"
10
+ | "accent5"
11
+ | "accent6"
12
+ | "hlink"
13
+ | "folHlink";
14
+
15
+ export type ThemeFontVariants = {
16
+ latin: string | null;
17
+ eastAsia: string | null;
18
+ complexScript: string | null;
19
+ };
20
+
21
+ export type ThemeTable = {
22
+ colors: ReadonlyMap<ThemeColorName, string>;
23
+ majorFonts: ThemeFontVariants;
24
+ minorFonts: ThemeFontVariants;
25
+ };
26
+
27
+ export const EMPTY_THEME_TABLE: ThemeTable = {
28
+ colors: new Map(),
29
+ majorFonts: { latin: null, eastAsia: null, complexScript: null },
30
+ minorFonts: { latin: null, eastAsia: null, complexScript: null },
31
+ };
32
+
33
+ const W_TO_THEME: Record<string, ThemeColorName> = {
34
+ text1: "dk1",
35
+ background1: "lt1",
36
+ text2: "dk2",
37
+ background2: "lt2",
38
+ accent1: "accent1",
39
+ accent2: "accent2",
40
+ accent3: "accent3",
41
+ accent4: "accent4",
42
+ accent5: "accent5",
43
+ accent6: "accent6",
44
+ hyperlink: "hlink",
45
+ followedHyperlink: "folHlink",
46
+ dark1: "dk1",
47
+ dark2: "dk2",
48
+ light1: "lt1",
49
+ light2: "lt2",
50
+ };
51
+
52
+ export function wThemeColorToScheme(name: string): ThemeColorName | null {
53
+ return W_TO_THEME[name] ?? null;
54
+ }
55
+
56
+ function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
57
+ const cleaned = hex.replace(/^#/, "");
58
+ if (cleaned.length !== 6) return null;
59
+ const r = Number.parseInt(cleaned.slice(0, 2), 16);
60
+ const g = Number.parseInt(cleaned.slice(2, 4), 16);
61
+ const b = Number.parseInt(cleaned.slice(4, 6), 16);
62
+ if (![r, g, b].every((v) => Number.isFinite(v))) return null;
63
+ return { r, g, b };
64
+ }
65
+
66
+ function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
67
+ const rn = r / 255;
68
+ const gn = g / 255;
69
+ const bn = b / 255;
70
+ const max = Math.max(rn, gn, bn);
71
+ const min = Math.min(rn, gn, bn);
72
+ const l = (max + min) / 2;
73
+ let h = 0;
74
+ let s = 0;
75
+ if (max !== min) {
76
+ const d = max - min;
77
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
78
+ switch (max) {
79
+ case rn:
80
+ h = (gn - bn) / d + (gn < bn ? 6 : 0);
81
+ break;
82
+ case gn:
83
+ h = (bn - rn) / d + 2;
84
+ break;
85
+ default:
86
+ h = (rn - gn) / d + 4;
87
+ }
88
+ h /= 6;
89
+ }
90
+ return { h, s, l };
91
+ }
92
+
93
+ function hueToRgb(p: number, q: number, t: number): number {
94
+ let tt = t;
95
+ if (tt < 0) tt += 1;
96
+ if (tt > 1) tt -= 1;
97
+ if (tt < 1 / 6) return p + (q - p) * 6 * tt;
98
+ if (tt < 1 / 2) return q;
99
+ if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
100
+ return p;
101
+ }
102
+
103
+ function hslToRgb(h: number, s: number, l: number): { r: number; g: number; b: number } {
104
+ if (s === 0) {
105
+ const v = Math.round(l * 255);
106
+ return { r: v, g: v, b: v };
107
+ }
108
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
109
+ const p = 2 * l - q;
110
+ return {
111
+ r: Math.round(hueToRgb(p, q, h + 1 / 3) * 255),
112
+ g: Math.round(hueToRgb(p, q, h) * 255),
113
+ b: Math.round(hueToRgb(p, q, h - 1 / 3) * 255),
114
+ };
115
+ }
116
+
117
+ function rgbToHex(rgb: { r: number; g: number; b: number }): string {
118
+ const to2 = (v: number) =>
119
+ Math.max(0, Math.min(255, Math.round(v))).toString(16).padStart(2, "0").toUpperCase();
120
+ return `${to2(rgb.r)}${to2(rgb.g)}${to2(rgb.b)}`;
121
+ }
122
+
123
+ function applyLumMod(
124
+ hex: string,
125
+ tint: number | null,
126
+ shade: number | null,
127
+ ): string {
128
+ const rgb = hexToRgb(hex);
129
+ if (!rgb) return hex;
130
+ const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
131
+ let newL = hsl.l;
132
+ if (tint !== null && tint > 0) {
133
+ const t = tint / 255;
134
+ newL = hsl.l * (1 - t) + t;
135
+ } else if (shade !== null && shade > 0) {
136
+ const s = shade / 255;
137
+ newL = hsl.l * s;
138
+ }
139
+ newL = Math.max(0, Math.min(1, newL));
140
+ return rgbToHex(hslToRgb(hsl.h, hsl.s, newL));
141
+ }
142
+
143
+ export function resolveThemeColor(
144
+ theme: ThemeTable,
145
+ themeColorName: string,
146
+ themeTint: string | null,
147
+ themeShade: string | null,
148
+ ): string | null {
149
+ const scheme = wThemeColorToScheme(themeColorName);
150
+ if (!scheme) return null;
151
+ const base = theme.colors.get(scheme);
152
+ if (!base) return null;
153
+ const tintNum = themeTint ? Number.parseInt(themeTint, 16) : null;
154
+ const shadeNum = themeShade ? Number.parseInt(themeShade, 16) : null;
155
+ return applyLumMod(base, tintNum, shadeNum);
156
+ }
@@ -0,0 +1,55 @@
1
+ import type { XmlElement } from "@lotics/ooxml/xml";
2
+
3
+ export type DocxDocument = {
4
+ readonly parts: ReadonlyMap<string, Uint8Array>;
5
+ readonly documentAttrs: Readonly<Record<string, string>>;
6
+ readonly body: Body;
7
+ };
8
+
9
+ export type Body = {
10
+ readonly children: readonly Block[];
11
+ };
12
+
13
+ export type Block = Paragraph | OpaqueBlock | BodySectPr;
14
+
15
+ export type Paragraph = {
16
+ readonly kind: "paragraph";
17
+ readonly properties: readonly XmlElement[] | null;
18
+ readonly content: readonly Inline[];
19
+ };
20
+
21
+ export type Inline = Run | OpaqueInline;
22
+
23
+ export type Run = {
24
+ readonly kind: "run";
25
+ readonly properties: readonly XmlElement[] | null;
26
+ readonly content: readonly RunChild[];
27
+ };
28
+
29
+ export type RunChild = TextNode | OpaqueRunChild;
30
+
31
+ export type TextNode = {
32
+ readonly kind: "text";
33
+ readonly value: string;
34
+ readonly preserveSpace: boolean;
35
+ };
36
+
37
+ export type OpaqueBlock = {
38
+ readonly kind: "opaque_block";
39
+ readonly xml: XmlElement;
40
+ };
41
+
42
+ export type OpaqueInline = {
43
+ readonly kind: "opaque_inline";
44
+ readonly xml: XmlElement;
45
+ };
46
+
47
+ export type OpaqueRunChild = {
48
+ readonly kind: "opaque_run_child";
49
+ readonly xml: XmlElement;
50
+ };
51
+
52
+ export type BodySectPr = {
53
+ readonly kind: "body_sect_pr";
54
+ readonly xml: XmlElement;
55
+ };