@a3s-lab/office 0.36.0 → 0.37.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/dist/4121.js CHANGED
@@ -68,2146 +68,2146 @@ function nonNegativeInteger(value) {
68
68
  const number = Number(value);
69
69
  return Number.isSafeInteger(number) && number >= 0 ? number : null;
70
70
  }
71
- function normalizeCssColor(source) {
72
- const value = source?.trim().toLowerCase();
71
+ const DEFAULT_DOCUMENT_INDEX_OPTIONS = {
72
+ columns: 1,
73
+ format: 'indented',
74
+ rightAlignPageNumbers: true,
75
+ leader: 'dot'
76
+ };
77
+ const MAX_DOCUMENT_INDEX_ENTRIES = 512;
78
+ const MAX_DOCUMENT_INDEX_MARKERS = 2048;
79
+ const MAX_DOCUMENT_INDEX_TERM_LENGTH = 240;
80
+ const INDEX_ENTRY_SELECTOR = '[data-document-index-entry]';
81
+ const INDEX_SELECTOR = '[data-document-index]';
82
+ const INDEX_ID_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,127}$/i;
83
+ function normalizeDocumentIndexEntryDraft(source) {
84
+ const mainEntry = normalizedIndexTerm(source?.mainEntry);
85
+ if (!mainEntry) return null;
86
+ const subEntry = normalizedIndexTerm(source?.subEntry);
87
+ const crossReference = normalizedIndexTerm(source?.crossReference);
88
+ return {
89
+ mainEntry,
90
+ subEntry,
91
+ crossReference,
92
+ pageBold: !crossReference && Boolean(source?.pageBold),
93
+ pageItalic: !crossReference && Boolean(source?.pageItalic)
94
+ };
95
+ }
96
+ function normalizeDocumentIndexEntry(source, fallbackId = 'index-entry') {
97
+ const value = normalizeDocumentIndexEntryDraft(source);
73
98
  if (!value) return null;
74
- if ('transparent' === value) return 'transparent';
75
- const shortHex = /^#([0-9a-f]{3})$/i.exec(value);
76
- if (shortHex?.[1]) return `#${Array.from(shortHex[1]).map((channel)=>`${channel}${channel}`).join('')}`;
77
- if (/^#[0-9a-f]{6}$/i.test(value)) return value;
78
- const rgb = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*([\d.]+))?\s*\)$/i.exec(value);
79
- if (!rgb) return null;
80
- const channels = rgb.slice(1, 4).map(Number);
81
- if (channels.some((channel)=>channel < 0 || channel > 255)) return null;
82
- if (void 0 !== rgb[4] && 0 === Number(rgb[4])) return 'transparent';
83
- if (void 0 !== rgb[4] && 1 !== Number(rgb[4])) return null;
84
- return `#${channels.map((channel)=>channel.toString(16).padStart(2, '0')).join('')}`;
99
+ return {
100
+ id: validIndexId(source?.id) ?? fallbackId,
101
+ ...value
102
+ };
85
103
  }
86
- function decodeXmlBytes(bytes, label) {
87
- let encoding = 'utf-8';
88
- let offset = 0;
89
- if (0xef === bytes[0] && 0xbb === bytes[1] && 0xbf === bytes[2]) offset = 3;
90
- else if (0xff === bytes[0] && 0xfe === bytes[1]) {
91
- encoding = 'utf-16le';
92
- offset = 2;
93
- } else if (0xfe === bytes[0] && 0xff === bytes[1]) {
94
- encoding = 'utf-16be';
95
- offset = 2;
96
- } else if (0x3c === bytes[0] && 0 === bytes[1] && 0x3f === bytes[2] && 0 === bytes[3]) encoding = 'utf-16le';
97
- else if (0 === bytes[0] && 0x3c === bytes[1] && 0 === bytes[2] && 0x3f === bytes[3]) encoding = 'utf-16be';
98
- try {
99
- return new TextDecoder(encoding, {
100
- fatal: true
101
- }).decode(bytes.subarray(offset));
102
- } catch {
103
- throw new Error(`${label} uses an invalid ${encoding} XML encoding.`);
104
- }
104
+ function normalizeDocumentIndexOptions(source) {
105
+ return {
106
+ columns: boundedColumns(source?.columns),
107
+ format: source?.format === 'run-in' ? 'run-in' : 'indented',
108
+ rightAlignPageNumbers: source?.rightAlignPageNumbers !== false,
109
+ leader: indexLeader(source?.leader) ?? 'dot'
110
+ };
105
111
  }
106
- function serializeUtf8Xml(document) {
107
- const serialized = new XMLSerializer().serializeToString(document);
108
- const body = serialized.replace(/^\s*<\?xml[^?]*\?>\s*/i, '');
109
- return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>${body}`;
112
+ function normalizeDocumentIndexValue(source) {
113
+ return {
114
+ id: validIndexId(source.id) ?? 'document-index',
115
+ options: normalizeDocumentIndexOptions(source.options),
116
+ entries: normalizeGeneratedEntries(source.entries),
117
+ truncated: Boolean(source.truncated)
118
+ };
110
119
  }
111
- class OoxmlPackage {
112
- zip;
113
- textCache = new Map();
114
- constructor(zip){
115
- this.zip = zip;
116
- }
117
- static async load(buffer) {
118
- return new OoxmlPackage(await jszip.loadAsync(buffer));
119
- }
120
- has(partPath) {
121
- return Boolean(this.zip.file(partPath));
122
- }
123
- paths(prefix) {
124
- return Object.keys(this.zip.files).filter((path)=>path.startsWith(prefix) && !this.zip.files[path]?.dir);
125
- }
126
- async text(partPath) {
127
- const cached = this.textCache.get(partPath);
128
- if (cached) return cached;
129
- const entry = this.zip.file(partPath);
130
- if (!entry) throw new Error(`Office package part is missing: ${partPath}`);
131
- const pending = entry.async('uint8array').then((bytes)=>decodeXmlBytes(bytes, partPath));
132
- this.textCache.set(partPath, pending);
133
- try {
134
- return await pending;
135
- } catch (error) {
136
- this.textCache.delete(partPath);
137
- throw error;
138
- }
139
- }
140
- async xml(partPath) {
141
- return parseXml(await this.text(partPath), partPath);
142
- }
143
- async bytes(partPath) {
144
- const entry = this.zip.file(partPath);
145
- if (!entry) throw new Error(`Office package part is missing: ${partPath}`);
146
- return entry.async('uint8array');
147
- }
148
- async relationships(sourcePart) {
149
- const partPath = relationshipsPartPath(sourcePart);
150
- if (!this.has(partPath)) return new Map();
151
- const document = await this.xml(partPath);
152
- return new Map(descendants(document, 'Relationship').map((element)=>{
153
- const relationship = {
154
- id: attribute(element, 'Id') ?? '',
155
- target: resolvePartTarget(sourcePart, attribute(element, 'Target') ?? ''),
156
- type: attribute(element, 'Type') ?? '',
157
- targetMode: attribute(element, 'TargetMode') ?? void 0
120
+ function buildDocumentIndexEntries(document, options = {}) {
121
+ const grouped = new Map();
122
+ let markerCount = 0;
123
+ document.descendants((node, position)=>{
124
+ if ('documentIndexEntry' !== node.type.name) return;
125
+ markerCount += 1;
126
+ if (markerCount > MAX_DOCUMENT_INDEX_MARKERS) return;
127
+ const marker = normalizeDocumentIndexEntry(node.attrs, `index-entry-${markerCount}`);
128
+ if (!marker) return;
129
+ const key = indexEntryKey(marker);
130
+ let group = grouped.get(key);
131
+ if (!group) {
132
+ group = {
133
+ entry: {
134
+ mainEntry: marker.mainEntry,
135
+ subEntry: marker.subEntry,
136
+ crossReference: marker.crossReference,
137
+ pages: []
138
+ },
139
+ pages: new Map()
158
140
  };
159
- return [
160
- relationship.id,
161
- relationship
162
- ];
163
- }));
164
- }
141
+ grouped.set(key, group);
142
+ }
143
+ if (marker.crossReference) return;
144
+ const pageNumber = positiveInteger(options.resolveContext?.(position)?.pageNumber) ?? fallbackDocumentPageNumber(document, position);
145
+ const existing = group.pages.get(pageNumber);
146
+ if (existing) {
147
+ existing.pageBold ||= marker.pageBold;
148
+ existing.pageItalic ||= marker.pageItalic;
149
+ if (!existing.targetIds.includes(marker.id)) existing.targetIds.push(marker.id);
150
+ return;
151
+ }
152
+ group.pages.set(pageNumber, {
153
+ pageNumber,
154
+ pageBold: marker.pageBold,
155
+ pageItalic: marker.pageItalic,
156
+ targetIds: [
157
+ marker.id
158
+ ]
159
+ });
160
+ });
161
+ const collator = new Intl.Collator(void 0, {
162
+ numeric: true,
163
+ sensitivity: 'base',
164
+ usage: 'sort'
165
+ });
166
+ const allEntries = Array.from(grouped.values()).map(({ entry, pages })=>({
167
+ ...entry,
168
+ pages: Array.from(pages.values()).sort((left, right)=>left.pageNumber - right.pageNumber)
169
+ })).sort((left, right)=>collator.compare(left.mainEntry, right.mainEntry) || collator.compare(left.subEntry, right.subEntry) || collator.compare(left.crossReference, right.crossReference));
170
+ return {
171
+ entries: allEntries.slice(0, MAX_DOCUMENT_INDEX_ENTRIES),
172
+ truncated: markerCount > MAX_DOCUMENT_INDEX_MARKERS || allEntries.length > MAX_DOCUMENT_INDEX_ENTRIES
173
+ };
165
174
  }
166
- function parseXml(source, label = 'Office XML') {
167
- const document = new DOMParser().parseFromString(source, 'application/xml');
168
- const error = descendants(document, 'parsererror')[0];
169
- if (error) throw new Error(`${label} is not valid XML: ${error.textContent?.trim() || 'parse error'}`);
170
- return document;
175
+ function documentIndexEntryHtml(source) {
176
+ const value = normalizeDocumentIndexEntry(source);
177
+ if (!value) return '';
178
+ const detail = indexEntryDisplay(value);
179
+ return [
180
+ `<span data-document-index-entry="true" data-index-entry-id="${escapeHtmlAttribute(value.id)}" data-index-main-entry="${escapeHtmlAttribute(value.mainEntry)}" data-index-sub-entry="${escapeHtmlAttribute(value.subEntry)}" data-index-cross-reference="${escapeHtmlAttribute(value.crossReference)}" data-index-page-bold="${String(value.pageBold)}" data-index-page-italic="${String(value.pageItalic)}" class="work-document-index-entry" contenteditable="false" aria-label="索引项:${escapeHtmlAttribute(detail)}">`,
181
+ '<span aria-hidden="true">索引项</span>',
182
+ `<strong>${escapeHtml(detail)}</strong>`,
183
+ '</span>'
184
+ ].join('');
171
185
  }
172
- const xmlElementPatterns = new Map();
173
- function xmlContainsAnyElement(source, localNames) {
174
- if (!source || !localNames.length) return false;
175
- const key = localNames.join('\u0000');
176
- let pattern = xmlElementPatterns.get(key);
177
- if (!pattern) {
178
- const alternatives = localNames.map((name)=>name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
179
- pattern = new RegExp(`<(?:[A-Za-z_][\\w.-]*:)?(?:${alternatives})(?=[\\s/>])`);
180
- xmlElementPatterns.set(key, pattern);
181
- }
182
- return pattern.test(source);
186
+ function documentIndexHtml(source) {
187
+ const value = normalizeDocumentIndexValue(source);
188
+ const rows = value.entries.length ? value.entries.map(documentIndexRowHtml) : [
189
+ '<li class="work-document-index-empty">没有已标记的索引项</li>'
190
+ ];
191
+ const status = value.truncated ? `仅显示前 ${MAX_DOCUMENT_INDEX_ENTRIES} 项` : `${value.entries.length} 项`;
192
+ return [
193
+ `<div data-document-index="true" data-index-id="${escapeHtmlAttribute(value.id)}" data-index-columns="${value.options.columns}" data-index-format="${value.options.format}" data-index-right-align-page-numbers="${String(value.options.rightAlignPageNumbers)}" data-index-leader="${value.options.leader}" data-index-entries="${escapeHtmlAttribute(JSON.stringify(value.entries))}" data-index-truncated="${String(value.truncated)}" class="work-document-index" contenteditable="false" aria-label="索引">`,
194
+ `<div class="work-document-index-header"><strong>索引</strong><span>${status}</span></div>`,
195
+ '<ol class="work-document-index-list">',
196
+ ...rows,
197
+ '</ol></div>'
198
+ ].join('');
183
199
  }
184
- function attribute(element, name) {
185
- const direct = element.getAttribute(name);
186
- if (null !== direct) return direct;
187
- const localName = name.includes(':') ? name.slice(name.indexOf(':') + 1) : name;
188
- return Array.from(element.attributes).find((item)=>{
189
- const itemLocalName = item.localName.includes(':') ? item.localName.slice(item.localName.indexOf(':') + 1) : item.localName;
190
- return itemLocalName === localName && (!name.includes(':') || item.name === name);
191
- })?.value ?? null;
200
+ function documentIndexEntryFromElement(element, index = 1) {
201
+ return normalizeDocumentIndexEntry({
202
+ id: element.dataset.indexEntryId,
203
+ mainEntry: element.dataset.indexMainEntry,
204
+ subEntry: element.dataset.indexSubEntry,
205
+ crossReference: element.dataset.indexCrossReference,
206
+ pageBold: 'true' === element.dataset.indexPageBold,
207
+ pageItalic: 'true' === element.dataset.indexPageItalic
208
+ }, `index-entry-${index}`);
192
209
  }
193
- function xmlNamespacePrefix(element, namespace) {
194
- if (!namespace) return element.prefix;
195
- if ('function' == typeof element.lookupPrefix) {
196
- const prefix = element.lookupPrefix(namespace);
197
- if (prefix) return prefix;
198
- }
199
- let current = element;
200
- while(current){
201
- if (current.namespaceURI === namespace && current.prefix) return current.prefix;
202
- const declaration = Array.from(current.attributes).find((item)=>item.value === namespace && ('xmlns' === item.name || item.name.startsWith('xmlns:')));
203
- if (declaration?.name.startsWith('xmlns:')) return declaration.name.slice(6);
204
- current = current.parentElement;
205
- }
206
- return null;
207
- }
208
- function directChildren(parent, localName) {
209
- return Array.from(parent.children).filter((element)=>!localName || element.localName === localName);
210
- }
211
- function directChild(parent, localName) {
212
- return directChildren(parent, localName)[0];
213
- }
214
- function descendants(parent, localName) {
215
- return Array.from(parent.querySelectorAll('*')).filter((element)=>element.localName === localName);
216
- }
217
- function firstDescendant(parent, localName) {
218
- if (!parent) return;
219
- return descendants(parent, localName)[0];
210
+ function documentIndexValueFromElement(element, index = 1) {
211
+ return normalizeDocumentIndexValue({
212
+ id: validIndexId(element.dataset.indexId) ?? `document-index-${index}`,
213
+ options: {
214
+ columns: Number(element.dataset.indexColumns),
215
+ format: 'run-in' === element.dataset.indexFormat ? 'run-in' : 'indented',
216
+ rightAlignPageNumbers: 'false' !== element.dataset.indexRightAlignPageNumbers,
217
+ leader: indexLeader(element.dataset.indexLeader) ?? 'dot'
218
+ },
219
+ entries: parseGeneratedEntries(element.dataset.indexEntries),
220
+ truncated: 'true' === element.dataset.indexTruncated
221
+ });
220
222
  }
221
- function childPath(parent, ...localNames) {
222
- let current = parent;
223
- for (const name of localNames){
224
- if (!current) return;
225
- current = directChild(current, name);
223
+ function normalizeDocumentIndexesHtml(source) {
224
+ const document = new DOMParser().parseFromString(source, 'text/html');
225
+ const usedEntryIds = new Set();
226
+ for (const [index, element] of Array.from(document.body.querySelectorAll(INDEX_ENTRY_SELECTOR)).entries()){
227
+ const value = documentIndexEntryFromElement(element, index + 1);
228
+ if (!value) {
229
+ element.remove();
230
+ continue;
231
+ }
232
+ value.id = uniqueIndexId(value.id, 'index-entry', index + 1, usedEntryIds);
233
+ element.replaceWith(document.createRange().createContextualFragment(documentIndexEntryHtml(value)));
226
234
  }
227
- return current instanceof Element ? current : void 0;
228
- }
229
- function resolvePartTarget(sourcePart, target) {
230
- if (/^[a-z][a-z0-9+.-]*:/i.test(target)) return target;
231
- const segments = target.startsWith('/') ? [] : sourcePart.split('/').slice(0, -1);
232
- for (const segment of target.replace(/^\/+/, '').split('/'))if (segment && '.' !== segment) if ('..' === segment) segments.pop();
233
- else segments.push(segment);
234
- return segments.join('/');
235
- }
236
- function contentTypeForPart(partPath) {
237
- const extension = partPath.split('.').pop()?.toLowerCase();
238
- const types = {
239
- apng: 'image/apng',
240
- bmp: 'image/bmp',
241
- emf: 'image/emf',
242
- gif: 'image/gif',
243
- jpeg: 'image/jpeg',
244
- jpg: 'image/jpeg',
245
- png: 'image/png',
246
- svg: 'image/svg+xml',
247
- tif: 'image/tiff',
248
- tiff: 'image/tiff',
249
- webp: 'image/webp',
250
- wmf: 'image/wmf'
251
- };
252
- return types[extension ?? ''] ?? 'application/octet-stream';
235
+ const usedIndexIds = new Set();
236
+ for (const [index, element] of Array.from(document.body.querySelectorAll(INDEX_SELECTOR)).entries()){
237
+ const value = documentIndexValueFromElement(element, index + 1);
238
+ value.id = uniqueIndexId(value.id, 'document-index', index + 1, usedIndexIds);
239
+ element.replaceWith(document.createRange().createContextualFragment(documentIndexHtml(value)));
240
+ }
241
+ return document.body.innerHTML;
253
242
  }
254
- function bytesToDataUrl(bytes, contentType) {
255
- let binary = '';
256
- const chunkSize = 32768;
257
- for(let offset = 0; offset < bytes.length; offset += chunkSize)binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
258
- return `data:${contentType};base64,${btoa(binary)}`;
243
+ function indexLeader(value) {
244
+ return 'dot' === value || 'dash' === value || 'underline' === value || 'none' === value ? value : null;
259
245
  }
260
- function relationshipsPartPath(sourcePart) {
261
- const separator = sourcePart.lastIndexOf('/');
262
- const directory = separator >= 0 ? sourcePart.slice(0, separator + 1) : '';
263
- const fileName = separator >= 0 ? sourcePart.slice(separator + 1) : sourcePart;
264
- return `${directory}_rels/${fileName}.rels`;
246
+ function documentIndexRowHtml(entry) {
247
+ const term = entry.subEntry ? `<span class="work-document-index-main">${escapeHtml(entry.mainEntry)}</span><span class="work-document-index-sub">${escapeHtml(entry.subEntry)}</span>` : `<span class="work-document-index-main">${escapeHtml(entry.mainEntry)}</span>`;
248
+ const pages = entry.crossReference ? `<span class="work-document-index-cross-reference">参见 ${escapeHtml(entry.crossReference)}</span>` : entry.pages.map((page)=>{
249
+ const target = page.targetIds[0] ?? '';
250
+ const classes = [
251
+ page.pageBold ? 'bold' : '',
252
+ page.pageItalic ? 'italic' : ''
253
+ ].filter(Boolean).join(' ');
254
+ return `<a href="#${escapeHtmlAttribute(target)}" data-index-target="${escapeHtmlAttribute(target)}" tabindex="-1"${classes ? ` class="${classes}"` : ''}>${page.pageNumber}</a>`;
255
+ }).join('<span aria-hidden="true">, </span>');
256
+ return `<li data-index-main-entry="${escapeHtmlAttribute(entry.mainEntry)}" data-index-sub-entry="${escapeHtmlAttribute(entry.subEntry)}">${term}<i aria-hidden="true"></i><span class="work-document-index-pages">${pages}</span></li>`;
265
257
  }
266
- const WORD_NAMESPACE = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
267
- class DocxThemePatchCollector {
268
- patches = [];
269
- nextMarker = 1;
270
- usedColors;
271
- constructor(sourceHtml){
272
- this.usedColors = sourceColors(sourceHtml);
273
- }
274
- marker(kind, reference, currentColor) {
275
- if (!reference || normalizeColor(currentColor) !== reference.resolved) return null;
276
- let marker = '';
277
- do {
278
- marker = (0xf00000 + this.nextMarker).toString(16).padStart(6, '0').toUpperCase();
279
- this.nextMarker += 1;
280
- }while (this.usedColors.has(marker))
281
- this.usedColors.add(marker);
282
- this.patches.push({
283
- kind,
284
- marker,
285
- reference
258
+ function normalizeGeneratedEntries(source) {
259
+ const entries = [];
260
+ for (const item of source.slice(0, MAX_DOCUMENT_INDEX_ENTRIES)){
261
+ if (!item || 'object' != typeof item) continue;
262
+ const candidate = item;
263
+ const draft = normalizeDocumentIndexEntryDraft({
264
+ mainEntry: normalizedIndexTerm(candidate.mainEntry),
265
+ subEntry: normalizedIndexTerm(candidate.subEntry),
266
+ crossReference: normalizedIndexTerm(candidate.crossReference)
267
+ });
268
+ if (draft) entries.push({
269
+ mainEntry: draft.mainEntry,
270
+ subEntry: draft.subEntry,
271
+ crossReference: draft.crossReference,
272
+ pages: draft.crossReference ? [] : normalizeIndexPages(candidate.pages)
286
273
  });
287
- return marker;
288
274
  }
275
+ return entries;
289
276
  }
290
- function serializeDocxThemeReference(reference) {
291
- return reference ? JSON.stringify(reference) : void 0;
277
+ function normalizeIndexPages(source) {
278
+ if (!Array.isArray(source)) return [];
279
+ const pages = [];
280
+ const seen = new Set();
281
+ for (const item of source.slice(0, MAX_DOCUMENT_INDEX_MARKERS)){
282
+ if (!item || 'object' != typeof item) continue;
283
+ const candidate = item;
284
+ const pageNumber = positiveInteger(candidate.pageNumber);
285
+ if (!pageNumber || seen.has(pageNumber)) continue;
286
+ const targetIds = Array.isArray(candidate.targetIds) ? candidate.targetIds.flatMap((value)=>validIndexId(value) ? [
287
+ String(value)
288
+ ] : []).slice(0, 64) : [];
289
+ if (targetIds.length) {
290
+ seen.add(pageNumber);
291
+ pages.push({
292
+ pageNumber,
293
+ pageBold: Boolean(candidate.pageBold),
294
+ pageItalic: Boolean(candidate.pageItalic),
295
+ targetIds
296
+ });
297
+ }
298
+ }
299
+ return pages.sort((left, right)=>left.pageNumber - right.pageNumber);
292
300
  }
293
- function parseDocxThemeReference(value) {
294
- if (!value) return null;
301
+ function parseGeneratedEntries(source) {
302
+ if (!source) return [];
295
303
  try {
296
- const parsed = JSON.parse(value);
297
- const theme = 'string' == typeof parsed.theme ? parsed.theme.trim() : '';
298
- const resolved = normalizeColor('string' == typeof parsed.resolved ? parsed.resolved : null);
299
- const tint = byteHex(parsed.tint);
300
- const shade = byteHex(parsed.shade);
301
- if (!theme || !resolved) return null;
302
- return {
303
- theme,
304
- resolved,
305
- ...tint ? {
306
- tint
307
- } : {},
308
- ...shade ? {
309
- shade
310
- } : {}
311
- };
304
+ const parsed = JSON.parse(source);
305
+ return Array.isArray(parsed) ? normalizeGeneratedEntries(parsed) : [];
312
306
  } catch {
313
- return null;
307
+ return [];
314
308
  }
315
309
  }
316
- async function patchDocxThemeReferences(buffer, patches) {
317
- if (!patches.length) return buffer;
318
- const archive = await jszip.loadAsync(buffer);
319
- const byMarker = new Map(patches.map((patch)=>[
320
- patch.marker,
321
- patch
322
- ]));
323
- const entries = Object.values(archive.files).filter((entry)=>!entry.dir && /^word\/(?:document|header\d+|footer\d+|footnotes|endnotes|comments)\.xml$/.test(entry.name));
324
- for (const entry of entries){
325
- const document = parseXml(await entry.async('text'), entry.name);
326
- let changed = false;
327
- for (const element of Array.from(document.getElementsByTagName('*')))for (const target of themePatchTargets(element.localName)){
328
- const marker = wordAttribute(element, target.directAttribute)?.toUpperCase();
329
- const patch = marker ? byMarker.get(marker) : void 0;
330
- if (patch && patch.kind === target.kind) {
331
- setWordAttribute(document, element, target.directAttribute, patch.reference.resolved.slice(1).toUpperCase());
332
- setWordAttribute(document, element, target.themeAttribute, patch.reference.theme);
333
- setOptionalWordAttribute(document, element, target.tintAttribute, patch.reference.tint);
334
- setOptionalWordAttribute(document, element, target.shadeAttribute, patch.reference.shade);
335
- changed = true;
336
- }
337
- }
338
- if (changed) archive.file(entry.name, new XMLSerializer().serializeToString(document));
339
- }
340
- return archive.generateAsync({
341
- type: 'arraybuffer'
310
+ function fallbackDocumentPageNumber(document, position) {
311
+ let pageNumber = 1;
312
+ document.descendants((node, offset)=>{
313
+ if (offset >= position) return false;
314
+ if ('pageBreak' === node.type.name) pageNumber += 1;
315
+ return true;
342
316
  });
317
+ return pageNumber;
343
318
  }
344
- function themePatchTargets(localName) {
345
- if ('color' === localName) return [
346
- {
347
- kind: 'color',
348
- directAttribute: 'val',
349
- themeAttribute: 'themeColor',
350
- tintAttribute: 'themeTint',
351
- shadeAttribute: 'themeShade'
352
- }
353
- ];
354
- if ('u' === localName) return [
355
- {
356
- kind: 'underline',
357
- directAttribute: 'color',
358
- themeAttribute: 'themeColor',
359
- tintAttribute: 'themeTint',
360
- shadeAttribute: 'themeShade'
361
- }
362
- ];
363
- if ('shd' === localName) return [
364
- {
365
- kind: 'fill',
366
- directAttribute: 'fill',
367
- themeAttribute: 'themeFill',
368
- tintAttribute: 'themeFillTint',
369
- shadeAttribute: 'themeFillShade'
370
- },
371
- {
372
- kind: 'shadingColor',
373
- directAttribute: 'color',
374
- themeAttribute: 'themeColor',
375
- tintAttribute: 'themeTint',
376
- shadeAttribute: 'themeShade'
377
- }
378
- ];
319
+ function indexEntryKey(entry) {
379
320
  return [
380
- 'top',
381
- 'right',
382
- 'bottom',
383
- 'left',
384
- 'start',
385
- 'end'
386
- ].includes(localName) ? [
387
- {
388
- kind: 'border',
389
- directAttribute: 'color',
390
- themeAttribute: 'themeColor',
391
- tintAttribute: 'themeTint',
392
- shadeAttribute: 'themeShade'
393
- }
394
- ] : [];
321
+ entry.mainEntry,
322
+ entry.subEntry,
323
+ entry.crossReference
324
+ ].map((value)=>value.normalize('NFKC').toLocaleLowerCase()).join('\u0000');
395
325
  }
396
- function normalizeColor(value) {
397
- const normalized = value?.trim().toLowerCase();
398
- if (!normalized || !/^#[0-9a-f]{6}$/.test(normalized)) return null;
399
- return normalized;
326
+ function indexEntryDisplay(entry) {
327
+ const term = entry.subEntry ? `${entry.mainEntry} › ${entry.subEntry}` : entry.mainEntry;
328
+ return entry.crossReference ? `${term} · 参见 ${entry.crossReference}` : term;
400
329
  }
401
- function sourceColors(source) {
402
- const colors = new Set();
403
- for (const match of source.matchAll(/#([0-9a-f]{6})\b/gi))if (match[1]) colors.add(match[1].toUpperCase());
404
- for (const match of source.matchAll(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/gi)){
405
- const channels = match.slice(1, 4).map(Number);
406
- if (!channels.some((channel)=>channel < 0 || channel > 255)) colors.add(channels.map((channel)=>channel.toString(16).padStart(2, '0')).join('').toUpperCase());
330
+ function normalizedIndexTerm(value) {
331
+ return 'string' == typeof value ? value.replace(/\s+/g, ' ').trim().slice(0, MAX_DOCUMENT_INDEX_TERM_LENGTH) : '';
332
+ }
333
+ function boundedColumns(value) {
334
+ const number = Number(value);
335
+ return Number.isInteger(number) && number >= 1 && number <= 4 ? number : 1;
336
+ }
337
+ function positiveInteger(value) {
338
+ const number = Number(value);
339
+ return Number.isSafeInteger(number) && number > 0 ? Math.min(999999, number) : null;
340
+ }
341
+ function validIndexId(value) {
342
+ return 'string' == typeof value && INDEX_ID_PATTERN.test(value) ? value : null;
343
+ }
344
+ function uniqueIndexId(source, prefix, index, used) {
345
+ if (!used.has(source)) {
346
+ used.add(source);
347
+ return source;
407
348
  }
408
- return colors;
349
+ let suffix = index;
350
+ while(used.has(`${prefix}-${suffix}`))suffix += 1;
351
+ const id = `${prefix}-${suffix}`;
352
+ used.add(id);
353
+ return id;
409
354
  }
410
- function byteHex(value) {
411
- if ('string' != typeof value) return;
412
- const normalized = value.trim().toUpperCase();
413
- return /^[0-9A-F]{2}$/.test(normalized) ? normalized : void 0;
355
+ function escapeHtml(value) {
356
+ return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
414
357
  }
415
- function wordAttribute(element, name) {
416
- return element.getAttributeNS(WORD_NAMESPACE, name) ?? element.getAttribute(`w:${name}`);
358
+ function escapeHtmlAttribute(value) {
359
+ return escapeHtml(value).replaceAll('"', '&quot;').replaceAll("'", '&#39;');
417
360
  }
418
- function setWordAttribute(document, element, name, value) {
419
- const prefix = xmlNamespacePrefix(document.documentElement, WORD_NAMESPACE) ?? 'w';
420
- element.setAttributeNS(WORD_NAMESPACE, `${prefix}:${name}`, value);
361
+ const DOCUMENT_SCRIPT_FONTS_ATTRIBUTE = "data-office-script-fonts";
362
+ const DOCUMENT_SCRIPT_FONT_SLOT_ATTRIBUTE = "data-office-script-font-slot";
363
+ const MAX_FONT_NAME_LENGTH = 127;
364
+ const MAX_SERIALIZED_SCRIPT_FONTS_LENGTH = 4096;
365
+ const SCRIPT_FONT_KEYS = new Set([
366
+ 'ascii',
367
+ 'highAnsi',
368
+ 'eastAsia',
369
+ 'complexScript',
370
+ 'hint'
371
+ ]);
372
+ const SCRIPT_FONT_FACE_KEYS = new Set([
373
+ 'name',
374
+ 'theme',
375
+ 'resolved'
376
+ ]);
377
+ const SCRIPT_FONT_HINTS = new Set([
378
+ 'default',
379
+ 'eastAsia',
380
+ 'cs'
381
+ ]);
382
+ const THEME_FONTS = new Set([
383
+ 'majorEastAsia',
384
+ 'majorBidi',
385
+ 'majorAscii',
386
+ 'majorHAnsi',
387
+ 'minorEastAsia',
388
+ 'minorBidi',
389
+ 'minorAscii',
390
+ 'minorHAnsi'
391
+ ]);
392
+ const SLOT_FALLBACK_ORDER = {
393
+ ascii: [
394
+ 'ascii',
395
+ 'highAnsi',
396
+ 'eastAsia',
397
+ 'complexScript'
398
+ ],
399
+ highAnsi: [
400
+ 'highAnsi',
401
+ 'ascii',
402
+ 'eastAsia',
403
+ 'complexScript'
404
+ ],
405
+ eastAsia: [
406
+ 'eastAsia',
407
+ 'highAnsi',
408
+ 'ascii',
409
+ 'complexScript'
410
+ ],
411
+ complexScript: [
412
+ 'complexScript',
413
+ 'highAnsi',
414
+ 'ascii',
415
+ 'eastAsia'
416
+ ]
417
+ };
418
+ const NEUTRAL_SCRIPT_CHARACTER = /^[\p{Cc}\p{Cf}\p{M}\p{N}\p{P}\p{S}\p{Z}]$/u;
419
+ function normalizeDocumentScriptFonts(source) {
420
+ if (!isRecordWithKeys(source, SCRIPT_FONT_KEYS)) return null;
421
+ const normalized = {};
422
+ for (const slot of scriptFontSlots){
423
+ if (void 0 === source[slot]) continue;
424
+ const face = normalizeDocumentScriptFontFace(source[slot]);
425
+ if (!face) return null;
426
+ normalized[slot] = face;
427
+ }
428
+ if (void 0 !== source.hint) {
429
+ const hint = normalizeDocumentScriptFontHint(source.hint);
430
+ if (!hint) return null;
431
+ normalized.hint = hint;
432
+ }
433
+ return Object.keys(normalized).length ? normalized : null;
421
434
  }
422
- function setOptionalWordAttribute(document, element, name, value) {
423
- if (value) setWordAttribute(document, element, name, value);
424
- else element.removeAttributeNS(WORD_NAMESPACE, name);
435
+ function serializeDocumentScriptFonts(source) {
436
+ const fonts = normalizeDocumentScriptFonts(source);
437
+ return fonts ? JSON.stringify(fonts) : null;
425
438
  }
426
- const DOCUMENT_PARAGRAPH_BORDERS_ATTRIBUTE = 'data-office-paragraph-borders';
427
- const DOCUMENT_PARAGRAPH_BORDER_EDGES = [
428
- 'top',
429
- 'left',
430
- 'bottom',
431
- 'right',
432
- 'between',
433
- 'bar'
434
- ];
435
- const DOCUMENT_PARAGRAPH_BORDER_STYLES = [
436
- 'nil',
437
- 'none',
438
- 'single',
439
- 'thick',
440
- 'double',
441
- 'dotted',
442
- 'dashed',
443
- 'dotDash',
444
- 'dotDotDash',
445
- 'triple',
446
- 'thinThickSmallGap',
447
- 'thickThinSmallGap',
448
- 'thinThickThinSmallGap',
449
- 'thinThickMediumGap',
450
- 'thickThinMediumGap',
451
- 'thinThickThinMediumGap',
452
- 'thinThickLargeGap',
453
- 'thickThinLargeGap',
454
- 'thinThickThinLargeGap',
455
- 'wave',
456
- 'doubleWave',
457
- 'dashSmallGap',
458
- 'dashDotStroked',
459
- 'threeDEmboss',
460
- 'threeDEngrave',
461
- 'outset',
462
- 'inset',
463
- 'apples',
464
- 'archedScallops',
465
- 'babyPacifier',
466
- 'babyRattle',
467
- 'balloons3Colors',
468
- 'balloonsHotAir',
469
- 'basicBlackDashes',
470
- 'basicBlackDots',
471
- 'basicBlackSquares',
472
- 'basicThinLines',
473
- 'basicWhiteDashes',
474
- 'basicWhiteDots',
475
- 'basicWhiteSquares',
476
- 'basicWideInline',
477
- 'basicWideMidline',
478
- 'basicWideOutline',
479
- 'bats',
480
- 'birds',
481
- 'birdsFlight',
482
- 'cabins',
483
- 'cakeSlice',
484
- 'candyCorn',
485
- 'celticKnotwork',
486
- 'certificateBanner',
487
- 'chainLink',
488
- 'champagneBottle',
489
- 'checkedBarBlack',
490
- 'checkedBarColor',
491
- 'checkered',
492
- 'christmasTree',
493
- 'circlesLines',
494
- 'circlesRectangles',
495
- 'classicalWave',
496
- 'clocks',
497
- 'compass',
498
- 'confetti',
499
- 'confettiGrays',
500
- 'confettiOutline',
501
- 'confettiStreamers',
502
- 'confettiWhite',
503
- 'cornerTriangles',
504
- 'couponCutoutDashes',
505
- 'couponCutoutDots',
506
- 'crazyMaze',
507
- 'creaturesButterfly',
508
- 'creaturesFish',
509
- 'creaturesInsects',
510
- 'creaturesLadyBug',
511
- 'crossStitch',
512
- 'cup',
513
- 'decoArch',
514
- 'decoArchColor',
515
- 'decoBlocks',
516
- 'diamondsGray',
517
- 'doubleD',
518
- 'doubleDiamonds',
519
- 'earth1',
520
- 'earth2',
521
- 'eclipsingSquares1',
522
- 'eclipsingSquares2',
523
- 'eggsBlack',
524
- 'fans',
525
- 'film',
526
- 'firecrackers',
527
- 'flowersBlockPrint',
528
- 'flowersDaisies',
529
- 'flowersModern1',
530
- 'flowersModern2',
531
- 'flowersPansy',
532
- 'flowersRedRose',
533
- 'flowersRoses',
534
- 'flowersTeacup',
535
- 'flowersTiny',
536
- 'gems',
537
- 'gingerbreadMan',
538
- 'gradient',
539
- 'handmade1',
540
- 'handmade2',
541
- 'heartBalloon',
542
- 'heartGray',
543
- 'hearts',
544
- 'heebieJeebies',
545
- 'holly',
546
- 'houseFunky',
547
- 'hypnotic',
548
- 'iceCreamCones',
549
- 'lightBulb',
550
- 'lightning1',
551
- 'lightning2',
552
- 'mapPins',
553
- 'mapleLeaf',
554
- 'mapleMuffins',
555
- 'marquee',
556
- 'marqueeToothed',
557
- 'moons',
558
- 'mosaic',
559
- 'musicNotes',
560
- 'northwest',
561
- 'ovals',
562
- 'packages',
563
- 'palmsBlack',
564
- 'palmsColor',
565
- 'paperClips',
566
- 'papyrus',
567
- 'partyFavor',
568
- 'partyGlass',
569
- 'pencils',
570
- 'people',
571
- 'peopleWaving',
572
- 'peopleHats',
573
- 'poinsettias',
574
- 'postageStamp',
575
- 'pumpkin1',
576
- 'pushPinNote2',
577
- 'pushPinNote1',
578
- 'pyramids',
579
- 'pyramidsAbove',
580
- 'quadrants',
581
- 'rings',
582
- 'safari',
583
- 'sawtooth',
584
- 'sawtoothGray',
585
- 'scaredCat',
586
- 'seattle',
587
- 'shadowedSquares',
588
- 'sharksTeeth',
589
- 'shorebirdTracks',
590
- 'skyrocket',
591
- 'snowflakeFancy',
592
- 'snowflakes',
593
- 'sombrero',
594
- 'southwest',
595
- 'stars',
596
- 'starsTop',
597
- 'stars3d',
598
- 'starsBlack',
599
- 'starsShadowed',
600
- 'sun',
601
- 'swirligig',
602
- 'tornPaper',
603
- 'tornPaperBlack',
604
- 'trees',
605
- 'triangleParty',
606
- 'triangles',
607
- 'tribal1',
608
- 'tribal2',
609
- 'tribal3',
610
- 'tribal4',
611
- 'tribal5',
612
- 'tribal6',
613
- 'triangle1',
614
- 'triangle2',
615
- 'triangleCircle1',
616
- 'triangleCircle2',
617
- 'shapes1',
618
- 'shapes2',
619
- 'twistedLines1',
620
- 'twistedLines2',
621
- 'vine',
622
- 'waveline',
623
- 'weavingAngles',
624
- 'weavingBraid',
625
- 'weavingRibbon',
626
- 'weavingStrips',
627
- 'whiteFlowers',
628
- 'woodwork',
629
- 'xIllusions',
630
- 'zanyTriangles',
631
- 'zigZag',
632
- 'zigZagStitch'
633
- ];
634
- const BORDER_STYLE_SET = new Set(DOCUMENT_PARAGRAPH_BORDER_STYLES);
635
- const BORDER_EDGE_SET = new Set(DOCUMENT_PARAGRAPH_BORDER_EDGES);
636
- const BORDER_PROPERTY_SET = new Set([
637
- 'style',
638
- 'color',
639
- 'size',
640
- 'space',
641
- 'shadow',
642
- 'frame'
643
- ]);
644
- const LINE_BORDER_STYLES = new Set(DOCUMENT_PARAGRAPH_BORDER_STYLES.slice(0, 27));
645
- const DASHED_BORDER_STYLES = new Set([
646
- 'dashed',
647
- 'dashSmallGap',
648
- 'dashDotStroked',
649
- 'dotDash',
650
- 'dotDotDash'
651
- ]);
652
- const DOUBLE_BORDER_STYLES = new Set([
653
- 'double',
654
- 'triple',
655
- 'thinThickSmallGap',
656
- 'thickThinSmallGap',
657
- 'thinThickThinSmallGap',
658
- 'thinThickMediumGap',
659
- 'thickThinMediumGap',
660
- 'thinThickThinMediumGap',
661
- 'thinThickLargeGap',
662
- 'thickThinLargeGap',
663
- 'thinThickThinLargeGap',
664
- 'doubleWave'
665
- ]);
666
- const MAX_SERIALIZED_PARAGRAPH_BORDERS = 32768;
667
- const POINTS_TO_PIXELS = 96 / 72;
668
- function normalizeDocumentParagraphBorders(source) {
669
- if (!source || 'object' != typeof source || Array.isArray(source)) return null;
670
- const record = source;
671
- if (Object.keys(record).some((key)=>!BORDER_EDGE_SET.has(key))) return null;
672
- const borders = {};
673
- for (const edge of DOCUMENT_PARAGRAPH_BORDER_EDGES){
674
- if (void 0 === record[edge]) continue;
675
- const border = normalizeDocumentParagraphBorder(record[edge]);
676
- if (!border) return null;
677
- borders[edge] = border;
439
+ function parseDocumentScriptFonts(source) {
440
+ if (!source || source.length > MAX_SERIALIZED_SCRIPT_FONTS_LENGTH) return null;
441
+ try {
442
+ return normalizeDocumentScriptFonts(JSON.parse(source));
443
+ } catch {
444
+ return null;
678
445
  }
679
- return Object.keys(borders).length ? borders : null;
680
446
  }
681
- function normalizeDocumentParagraphBorder(source) {
682
- if (!source || 'object' != typeof source || Array.isArray(source)) return null;
683
- const record = source;
684
- if (Object.keys(record).some((key)=>!BORDER_PROPERTY_SET.has(key))) return null;
685
- const style = record.style;
686
- if ('string' != typeof style || !BORDER_STYLE_SET.has(style)) return null;
687
- const normalizedStyle = style;
688
- const color = normalizeBorderColor(record.color);
689
- if (void 0 !== record.color && !color) return null;
690
- const size = optionalInteger(record.size);
691
- if (null === size || void 0 !== size && !validBorderSize(normalizedStyle, size)) return null;
692
- const space = optionalInteger(record.space);
693
- if (null === space || void 0 !== space && (space < 0 || space > 31)) return null;
694
- const shadow = optionalBoolean(record.shadow);
695
- const frame = optionalBoolean(record.frame);
696
- if (null === shadow || null === frame) return null;
447
+ function documentScriptFontsFromElement(element) {
448
+ return parseDocumentScriptFonts(element.getAttribute(DOCUMENT_SCRIPT_FONTS_ATTRIBUTE));
449
+ }
450
+ function documentScriptFontSlotFromElement(element) {
451
+ return normalizeDocumentScriptFontSlot(element.getAttribute(DOCUMENT_SCRIPT_FONT_SLOT_ATTRIBUTE));
452
+ }
453
+ function documentScriptFontsDomAttributes(source, slot) {
454
+ const fonts = normalizeDocumentScriptFonts(source);
455
+ const normalizedSlot = normalizeDocumentScriptFontSlot(slot);
456
+ if (!fonts) return {};
457
+ const serialized = serializeDocumentScriptFonts(fonts);
458
+ if (!serialized) return {};
459
+ const family = documentScriptFontFamily(fonts, normalizedSlot ?? documentScriptFontSlotFromHint(fonts.hint));
697
460
  return {
698
- style: normalizedStyle,
699
- ...color ? {
700
- color
701
- } : {},
702
- ...void 0 !== size ? {
703
- size
704
- } : {},
705
- ...void 0 !== space ? {
706
- space
707
- } : {},
708
- ...void 0 !== shadow ? {
709
- shadow
461
+ [DOCUMENT_SCRIPT_FONTS_ATTRIBUTE]: serialized,
462
+ ...normalizedSlot ? {
463
+ [DOCUMENT_SCRIPT_FONT_SLOT_ATTRIBUTE]: normalizedSlot
710
464
  } : {},
711
- ...void 0 !== frame ? {
712
- frame
465
+ ...family ? {
466
+ style: `font-family: ${family}`
713
467
  } : {}
714
468
  };
715
469
  }
716
- function parseDocumentParagraphBorders(source) {
717
- if ('string' != typeof source) return normalizeDocumentParagraphBorders(source);
718
- if (!source.trim() || source.length > MAX_SERIALIZED_PARAGRAPH_BORDERS) return null;
719
- try {
720
- return normalizeDocumentParagraphBorders(JSON.parse(source));
721
- } catch {
722
- return null;
470
+ function documentScriptFontFamily(source, slot) {
471
+ const fonts = normalizeDocumentScriptFonts(source);
472
+ if (!fonts) return;
473
+ const families = [];
474
+ const seen = new Set();
475
+ for (const candidate of documentScriptFontFallbackSlots(slot)){
476
+ const family = documentScriptFontFaceFamily(fonts[candidate]);
477
+ const key = family?.toLocaleLowerCase();
478
+ if (!(!family || !key || seen.has(key))) {
479
+ seen.add(key);
480
+ families.push(cssFontFamily(family));
481
+ }
723
482
  }
483
+ return families.length ? families.join(', ') : void 0;
724
484
  }
725
- function serializeDocumentParagraphBorders(source) {
726
- const borders = normalizeDocumentParagraphBorders(source);
727
- if (!borders) return;
728
- return JSON.stringify(Object.fromEntries(DOCUMENT_PARAGRAPH_BORDER_EDGES.flatMap((edge)=>{
729
- const border = borders[edge];
730
- return border ? [
731
- [
732
- edge,
733
- serializedBorder(border)
734
- ]
735
- ] : [];
736
- })));
485
+ function documentScriptFontFallbackSlots(slot) {
486
+ return SLOT_FALLBACK_ORDER[slot];
737
487
  }
738
- function parseDocumentParagraphBordersElement(element) {
739
- const semantic = parseDocumentParagraphBorders(element.getAttribute(DOCUMENT_PARAGRAPH_BORDERS_ATTRIBUTE));
740
- if (!semantic) return paragraphBordersFromCss(element);
741
- const edited = {
742
- ...semantic
488
+ function documentScriptFontFamilyForRendering(source, slot, currentFontFamily) {
489
+ const projected = documentScriptFontFamily(source, slot);
490
+ const safeCurrent = safeCssFontFamilyList(currentFontFamily);
491
+ if (!safeCurrent || !projected) return projected;
492
+ const currentPrimary = documentFontNameFromCssFamily(safeCurrent);
493
+ const projectedPrimary = documentFontNameFromCssFamily(projected);
494
+ return currentPrimary && projectedPrimary && currentPrimary.toLocaleLowerCase() === projectedPrimary.toLocaleLowerCase() ? safeCurrent : projected;
495
+ }
496
+ function documentScriptFontDirectFamily(source, slot) {
497
+ const fonts = normalizeDocumentScriptFonts(source);
498
+ return fonts ? documentScriptFontFaceFamily(fonts[slot]) ?? null : null;
499
+ }
500
+ function documentScriptFontsForAllText(fontFamily) {
501
+ const name = documentFontNameFromCssFamily(fontFamily);
502
+ if (!name) return null;
503
+ const face = {
504
+ name,
505
+ resolved: name
743
506
  };
744
- for (const edge of [
745
- 'top',
746
- 'left',
747
- 'bottom',
748
- 'right'
749
- ]){
750
- const border = semantic[edge];
751
- const css = cssBorder(element, edge);
752
- if (!(!css || border && sameBorderPresentation(border, css))) {
753
- if ('none' === css.style) {
754
- edited[edge] = {
755
- style: 'nil'
756
- };
757
- continue;
758
- }
759
- edited[edge] = {
760
- ...border ?? {
761
- style: 'single'
762
- },
763
- style: cssStyleToBorderStyle(css.style),
764
- color: {
765
- value: css.color
766
- },
767
- size: cssWidthToEighthPoints(css.width)
768
- };
507
+ return {
508
+ ascii: face,
509
+ highAnsi: face,
510
+ eastAsia: face,
511
+ complexScript: face,
512
+ hint: 'default'
513
+ };
514
+ }
515
+ function patchDocumentScriptFonts(source, patch, fallbackFontFamily) {
516
+ const current = normalizeDocumentScriptFonts(source) ?? documentScriptFontsForAllText(fallbackFontFamily) ?? {};
517
+ const next = {
518
+ ...current
519
+ };
520
+ if (void 0 !== patch.latin) {
521
+ const face = directFontFace(patch.latin);
522
+ if (face) {
523
+ next.ascii = face;
524
+ next.highAnsi = face;
525
+ } else {
526
+ delete next.ascii;
527
+ delete next.highAnsi;
769
528
  }
770
529
  }
771
- return normalizeDocumentParagraphBorders(edited);
530
+ if (void 0 !== patch.eastAsia) {
531
+ const face = directFontFace(patch.eastAsia);
532
+ if (face) next.eastAsia = face;
533
+ else delete next.eastAsia;
534
+ }
535
+ if (void 0 !== patch.complexScript) {
536
+ const face = directFontFace(patch.complexScript);
537
+ if (face) next.complexScript = face;
538
+ else delete next.complexScript;
539
+ }
540
+ return normalizeDocumentScriptFonts(next);
772
541
  }
773
- function documentParagraphBordersDomAttributes(source) {
774
- const borders = normalizeDocumentParagraphBorders(source);
775
- const serialized = serializeDocumentParagraphBorders(borders);
776
- if (!borders || !serialized) return {};
777
- const styles = [];
778
- const shadows = [];
779
- for (const edge of [
780
- 'top',
781
- 'left',
782
- 'bottom',
783
- 'right'
784
- ]){
785
- const border = borders[edge];
786
- if (!border) continue;
787
- const presentation = documentBorderPresentation(border);
788
- styles.push(`border-${edge}: ${formatPixels(presentation.width)}px ${presentation.style} ${presentation.color}`);
789
- if (border.space) styles.push(`padding-${edge}: ${formatPixels(border.space * POINTS_TO_PIXELS)}px`);
790
- if (border.shadow && presentation.width > 0) shadows.push(`2px 2px 0 ${presentation.color}`);
542
+ function documentScriptFontSegments(text, hint = 'default', forceComplexScript = false) {
543
+ if (!text) return [];
544
+ if (forceComplexScript) return [
545
+ {
546
+ from: 0,
547
+ to: text.length,
548
+ slot: 'complexScript'
549
+ }
550
+ ];
551
+ const characters = [];
552
+ let offset = 0;
553
+ for (const character of text){
554
+ const from = offset;
555
+ offset += character.length;
556
+ characters.push({
557
+ from,
558
+ to: offset,
559
+ slot: strongDocumentScriptFontSlot(character)
560
+ });
791
561
  }
792
- const between = borders.between ? documentBorderPresentation(borders.between) : null;
793
- if (between && between.width > 0) shadows.push(`inset 0 -${formatPixels(between.width)}px 0 ${between.color}`);
794
- const bar = borders.bar ? documentBorderPresentation(borders.bar) : null;
795
- if (bar && bar.width > 0) shadows.push(`inset ${formatPixels(bar.width)}px 0 0 ${bar.color}`);
796
- if (shadows.length) styles.push(`box-shadow: ${shadows.join(', ')}`);
797
- return {
798
- [DOCUMENT_PARAGRAPH_BORDERS_ATTRIBUTE]: serialized,
799
- ...styles.length ? {
800
- style: styles.join('; ')
801
- } : {}
802
- };
562
+ const fallback = documentScriptFontSlotFromHint(hint);
563
+ let previous = null;
564
+ for(let index = 0; index < characters.length; index += 1){
565
+ const entry = characters[index];
566
+ if (!entry) continue;
567
+ if (entry.slot) {
568
+ previous = entry.slot;
569
+ continue;
570
+ }
571
+ let next = previous;
572
+ if (!next) for(let cursor = index + 1; cursor < characters.length; cursor += 1){
573
+ const candidate = characters[cursor]?.slot;
574
+ if (candidate) {
575
+ next = candidate;
576
+ break;
577
+ }
578
+ }
579
+ entry.slot = next ?? fallback;
580
+ }
581
+ const segments = [];
582
+ for (const entry of characters){
583
+ const slot = entry.slot ?? fallback;
584
+ const prior = segments[segments.length - 1];
585
+ if (prior?.slot === slot && prior.to === entry.from) prior.to = entry.to;
586
+ else segments.push({
587
+ from: entry.from,
588
+ to: entry.to,
589
+ slot
590
+ });
591
+ }
592
+ return segments;
803
593
  }
804
- function isDocumentParagraphArtBorderStyle(style) {
805
- return !LINE_BORDER_STYLES.has(style);
594
+ function normalizeDocumentScriptFontSlot(value) {
595
+ return scriptFontSlots.includes(value) ? value : null;
806
596
  }
807
- function normalizeBorderColor(source) {
808
- if (!source || 'object' != typeof source || Array.isArray(source)) return null;
809
- const record = source;
810
- if (Object.keys(record).some((key)=>'value' !== key && 'theme' !== key)) return null;
811
- const theme = parseDocxThemeReference('string' == typeof record.theme ? record.theme : record.theme ? JSON.stringify(record.theme) : void 0);
812
- const direct = 'auto' === record.value ? 'auto' : 'string' == typeof record.value ? normalizeCssColor(record.value) : null;
813
- const resolved = direct ?? theme?.resolved ?? null;
814
- if (!resolved || 'transparent' === resolved) return null;
815
- if (theme && resolved !== theme.resolved && !('auto' === resolved && 'none' === theme.theme && '#000000' === theme.resolved)) return null;
816
- return {
817
- value: resolved,
818
- ...theme ? {
819
- theme
820
- } : {}
821
- };
597
+ function normalizeDocumentScriptFontHint(value) {
598
+ return SCRIPT_FONT_HINTS.has(value) ? value : null;
822
599
  }
823
- function serializedBorder(border) {
824
- const color = border.color ? serializedBorderColor(border.color) : void 0;
825
- return {
826
- style: border.style,
827
- ...color ? {
828
- color
829
- } : {},
830
- ...void 0 !== border.size ? {
831
- size: border.size
832
- } : {},
833
- ...void 0 !== border.space ? {
834
- space: border.space
835
- } : {},
836
- ...void 0 !== border.shadow ? {
837
- shadow: border.shadow
838
- } : {},
839
- ...void 0 !== border.frame ? {
840
- frame: border.frame
841
- } : {}
842
- };
600
+ function normalizeDocumentThemeFont(value) {
601
+ return THEME_FONTS.has(value) ? value : null;
843
602
  }
844
- function serializedBorderColor(color) {
845
- const theme = serializeDocxThemeReference(color.theme ?? null);
603
+ function documentScriptFontSlotFromHint(hint) {
604
+ if ('eastAsia' === hint) return 'eastAsia';
605
+ if ('cs' === hint) return 'complexScript';
606
+ return 'ascii';
607
+ }
608
+ function documentFontNameFromCssFamily(value) {
609
+ if ('string' != typeof value) return null;
610
+ const source = value.trim();
611
+ if (!source) return null;
612
+ let family = '';
613
+ const quote = source[0];
614
+ if ('"' === quote || "'" === quote) {
615
+ let closed = false;
616
+ for(let index = 1; index < source.length; index += 1){
617
+ const character = source[index];
618
+ if (character === quote) {
619
+ closed = true;
620
+ break;
621
+ }
622
+ if ('\\' !== character) {
623
+ family += character;
624
+ continue;
625
+ }
626
+ const decoded = decodeCssEscape(source, index + 1);
627
+ if (!decoded) return null;
628
+ family += decoded.value;
629
+ index = decoded.end - 1;
630
+ }
631
+ if (!closed) return null;
632
+ } else family = source.split(',')[0] ?? '';
633
+ return normalizeDocumentFontName(family);
634
+ }
635
+ function cssDocumentFontFamily(value) {
636
+ const family = normalizeDocumentFontName(value);
637
+ return family ? cssFontFamily(family) : null;
638
+ }
639
+ function normalizeDocumentFontName(value) {
640
+ if ('string' != typeof value) return null;
641
+ const normalized = value.trim();
642
+ return normalized && normalized.length <= MAX_FONT_NAME_LENGTH && !/[\p{Cc}\p{Cs}]/u.test(normalized) ? normalized : null;
643
+ }
644
+ const scriptFontSlots = [
645
+ 'ascii',
646
+ 'highAnsi',
647
+ 'eastAsia',
648
+ 'complexScript'
649
+ ];
650
+ function normalizeDocumentScriptFontFace(source) {
651
+ if (!isRecordWithKeys(source, SCRIPT_FONT_FACE_KEYS)) return null;
652
+ const name = void 0 === source.name ? void 0 : normalizeDocumentFontName(source.name);
653
+ const resolved = void 0 === source.resolved ? void 0 : normalizeDocumentFontName(source.resolved);
654
+ const theme = void 0 === source.theme ? void 0 : normalizeDocumentThemeFont(source.theme);
655
+ if (void 0 !== source.name && !name || void 0 !== source.resolved && !resolved || null === theme || !name && !theme && !resolved) return null;
846
656
  return {
847
- value: color.value,
657
+ ...name ? {
658
+ name
659
+ } : {},
848
660
  ...theme ? {
849
- theme: JSON.parse(theme)
661
+ theme
662
+ } : {},
663
+ ...resolved ? {
664
+ resolved
850
665
  } : {}
851
666
  };
852
667
  }
853
- function optionalInteger(value) {
854
- if (void 0 === value) return;
855
- return 'number' == typeof value && Number.isSafeInteger(value) ? value : null;
668
+ function directFontFace(value) {
669
+ if (null === value) return null;
670
+ const name = documentFontNameFromCssFamily(value) ?? normalizeDocumentFontName(value);
671
+ return name ? {
672
+ name,
673
+ resolved: name
674
+ } : null;
856
675
  }
857
- function optionalBoolean(value) {
858
- if (void 0 === value) return;
859
- return 'boolean' == typeof value ? value : null;
676
+ function documentScriptFontFaceFamily(face) {
677
+ return face?.resolved ?? face?.name;
860
678
  }
861
- function validBorderSize(style, size) {
862
- if ('nil' === style || 'none' === style) return size >= 0 && size <= 96;
863
- return isDocumentParagraphArtBorderStyle(style) ? size >= 1 && size <= 31 : size >= 2 && size <= 96;
679
+ function strongDocumentScriptFontSlot(character) {
680
+ if (NEUTRAL_SCRIPT_CHARACTER.test(character)) return null;
681
+ const codePoint = character.codePointAt(0);
682
+ if (void 0 === codePoint) return null;
683
+ if (isComplexScriptCodePoint(codePoint)) return 'complexScript';
684
+ if (isEastAsianCodePoint(codePoint)) return 'eastAsia';
685
+ return codePoint <= 0x7f ? 'ascii' : 'highAnsi';
864
686
  }
865
- function documentBorderPresentation(border) {
866
- if ('nil' === border.style || 'none' === border.style || !border.size) return {
867
- color: 'transparent',
868
- style: 'none',
869
- width: 0
870
- };
871
- const width = Math.min(16, isDocumentParagraphArtBorderStyle(border.style) ? border.size * POINTS_TO_PIXELS : border.size / 6);
872
- return {
873
- color: border.color && 'auto' !== border.color.value ? border.color.value : '#000000',
874
- style: borderStyleToCssStyle(border.style),
875
- width
876
- };
687
+ function isComplexScriptCodePoint(codePoint) {
688
+ return codePoint >= 0x0590 && codePoint <= 0x08ff || codePoint >= 0x0900 && codePoint <= 0x109f || codePoint >= 0x1780 && codePoint <= 0x18af || codePoint >= 0x1900 && codePoint <= 0x1cff || codePoint >= 0xa800 && codePoint <= 0xa8ff || codePoint >= 0xa980 && codePoint <= 0xa9df || codePoint >= 0xaa00 && codePoint <= 0xaa7f || codePoint >= 0xabc0 && codePoint <= 0xabff || codePoint >= 0xfb1d && codePoint <= 0xfdff || codePoint >= 0xfe70 && codePoint <= 0xfeff || codePoint >= 0x10a00 && codePoint <= 0x10fff || codePoint >= 0x11000 && codePoint <= 0x11fff || codePoint >= 0x1e900 && codePoint <= 0x1edff || codePoint >= 0x1ee00 && codePoint <= 0x1eeff;
877
689
  }
878
- function borderStyleToCssStyle(style) {
879
- if ('nil' === style || 'none' === style) return 'none';
880
- if ('dotted' === style) return 'dotted';
881
- if (DASHED_BORDER_STYLES.has(style)) return 'dashed';
882
- if (DOUBLE_BORDER_STYLES.has(style)) return 'double';
883
- if ('inset' === style || 'threeDEngrave' === style) return 'inset';
884
- if ('outset' === style || 'threeDEmboss' === style) return 'outset';
885
- return 'solid';
690
+ function isEastAsianCodePoint(codePoint) {
691
+ return codePoint >= 0x1100 && codePoint <= 0x11ff || codePoint >= 0x2e80 && codePoint <= 0xa4cf || codePoint >= 0xac00 && codePoint <= 0xd7af || codePoint >= 0xf900 && codePoint <= 0xfaff || codePoint >= 0xfe10 && codePoint <= 0xfe6f || codePoint >= 0xff00 && codePoint <= 0xffef || codePoint >= 0x20000 && codePoint <= 0x323af;
886
692
  }
887
- function paragraphBordersFromCss(element) {
888
- const borders = {};
889
- for (const edge of [
890
- 'top',
891
- 'left',
892
- 'bottom',
893
- 'right'
894
- ]){
895
- const css = cssBorder(element, edge);
896
- if (css && 'none' !== css.style) borders[edge] = {
897
- style: cssStyleToBorderStyle(css.style),
898
- color: {
899
- value: css.color
900
- },
901
- size: cssWidthToEighthPoints(css.width)
902
- };
693
+ function cssFontFamily(value) {
694
+ return /^(?:-?[\p{L}_])[\p{L}\p{N}_-]*$/u.test(value) ? value : `"${Array.from(value, cssStringCharacter).join('')}"`;
695
+ }
696
+ function cssStringCharacter(character) {
697
+ return /[\\":;{}<>]/u.test(character) ? `\\${character.codePointAt(0)?.toString(16)} ` : character;
698
+ }
699
+ function safeCssFontFamilyList(value) {
700
+ if ('string' != typeof value) return null;
701
+ const source = value.trim();
702
+ if (!source || source.length > 1024 || /[;{}]/u.test(source)) return null;
703
+ const tokens = [];
704
+ let start = 0;
705
+ let quote = '';
706
+ for(let index = 0; index < source.length; index += 1){
707
+ const character = source[index] ?? '';
708
+ if (quote) {
709
+ if ('\\' === character) {
710
+ index += 1;
711
+ if (index >= source.length) return null;
712
+ } else if (character === quote) quote = '';
713
+ continue;
714
+ }
715
+ if ('"' === character || "'" === character) {
716
+ quote = character;
717
+ continue;
718
+ }
719
+ if (',' === character) {
720
+ tokens.push(source.slice(start, index).trim());
721
+ start = index + 1;
722
+ }
903
723
  }
904
- return normalizeDocumentParagraphBorders(borders);
724
+ if (quote) return null;
725
+ tokens.push(source.slice(start).trim());
726
+ return tokens.length && tokens.every(validCssFontFamilyToken) ? source : null;
905
727
  }
906
- function cssBorder(element, edge) {
907
- const style = element.style.getPropertyValue(`border-${edge}-style`).trim();
908
- const width = Number.parseFloat(element.style.getPropertyValue(`border-${edge}-width`));
909
- const color = normalizeCssColor(element.style.getPropertyValue(`border-${edge}-color`));
910
- if (!style) return null;
911
- if ('none' === style || 'hidden' === style) return {
912
- color: '#000000',
913
- style: 'none',
914
- width: 0
915
- };
916
- return color && 'transparent' !== color && Number.isFinite(width) && width > 0 ? {
917
- color,
918
- style,
919
- width
920
- } : null;
728
+ function validCssFontFamilyToken(value) {
729
+ if (!value) return false;
730
+ if (value.startsWith('"')) return /^"(?:[^"\\\r\n\f]|\\(?:[\da-f]{1,6}\s?|[^\r\n\f]))*"$/iu.test(value) && null !== documentFontNameFromCssFamily(value);
731
+ if (value.startsWith("'")) return /^'(?:[^'\\\r\n\f]|\\(?:[\da-f]{1,6}\s?|[^\r\n\f]))*'$/iu.test(value) && null !== documentFontNameFromCssFamily(value);
732
+ return /[\p{L}_]/u.test(value) && /^[\p{L}_-][\p{L}\p{N}_ -]*$/u.test(value) && !/^(?:inherit|initial|revert(?:-layer)?|unset)$/iu.test(value);
921
733
  }
922
- function sameBorderPresentation(border, css) {
923
- const expected = documentBorderPresentation(border);
924
- if ('none' === expected.style) return 'none' === css.style && 0 === css.width;
925
- return expected.color === css.color && expected.style === css.style && Math.abs(expected.width - css.width) < 0.01;
734
+ function decodeCssEscape(source, start) {
735
+ if (start >= source.length) return null;
736
+ const hexadecimal = /^[\da-f]{1,6}/i.exec(source.slice(start))?.[0];
737
+ if (hexadecimal) {
738
+ const codePoint = Number.parseInt(hexadecimal, 16);
739
+ if (0 === codePoint || codePoint > 0x10ffff || codePoint >= 0xd800 && codePoint <= 0xdfff) return null;
740
+ let end = start + hexadecimal.length;
741
+ if (/\s/u.test(source[end] ?? '')) end += 1;
742
+ return {
743
+ value: String.fromCodePoint(codePoint),
744
+ end
745
+ };
746
+ }
747
+ const value = source[start];
748
+ return !value || /[\r\n\f]/u.test(value) ? null : {
749
+ value,
750
+ end: start + 1
751
+ };
926
752
  }
927
- function cssStyleToBorderStyle(style) {
928
- if ('double' === style) return 'double';
929
- if ('dashed' === style) return 'dashed';
930
- if ('dotted' === style) return 'dotted';
931
- if ('inset' === style || 'groove' === style) return 'inset';
932
- if ('outset' === style || 'ridge' === style) return 'outset';
933
- return 'single';
753
+ function isRecordWithKeys(value, allowed) {
754
+ return 'object' == typeof value && null !== value && !Array.isArray(value) && Object.keys(value).every((key)=>allowed.has(key));
934
755
  }
935
- function cssWidthToEighthPoints(width) {
936
- return Math.max(2, Math.min(96, Math.round(6 * width)));
756
+ const DOCUMENT_PROOFING_LANGUAGES_ATTRIBUTE = 'data-office-proofing-languages';
757
+ const DOCUMENT_NO_PROOF_ATTRIBUTE = 'data-office-no-proof';
758
+ const PROOFING_LANGUAGE_KEYS = new Set([
759
+ 'latin',
760
+ 'eastAsia',
761
+ 'bidi'
762
+ ]);
763
+ const PROOFING_LANGUAGE_ORDER = [
764
+ 'latin',
765
+ 'eastAsia',
766
+ 'bidi'
767
+ ];
768
+ const MAX_LANGUAGE_TAG_LENGTH = 85;
769
+ const MAX_SERIALIZED_PROOFING_LANGUAGES_BYTES = 384;
770
+ function normalizeDocumentLanguageTag(source) {
771
+ if ('string' != typeof source) return null;
772
+ if (!source || source !== source.trim() || source.length > MAX_LANGUAGE_TAG_LENGTH || /[\p{Cc}\p{Cs}]/u.test(source)) return null;
773
+ return /^(?:x-none|[a-z0-9]{1,8}(?:-[a-z0-9]{1,8})*)$/iu.test(source) ? source : null;
937
774
  }
938
- function formatPixels(value) {
939
- return Number(value.toFixed(3)).toString();
775
+ function normalizeDocumentProofingLanguages(source) {
776
+ if (!isRecord(source)) return null;
777
+ const keys = Object.keys(source);
778
+ if (!keys.length || keys.some((key)=>!PROOFING_LANGUAGE_KEYS.has(key))) return null;
779
+ const normalized = {};
780
+ for (const key of PROOFING_LANGUAGE_ORDER){
781
+ if (void 0 === source[key]) continue;
782
+ const language = normalizeDocumentLanguageTag(source[key]);
783
+ if (!language) return null;
784
+ normalized[key] = language;
785
+ }
786
+ return Object.keys(normalized).length ? normalized : null;
940
787
  }
941
- const DOCUMENT_RUN_BORDER_ATTRIBUTE = 'data-office-run-border';
942
- const DOCUMENT_RUN_BORDER_STYLES = DOCUMENT_PARAGRAPH_BORDER_STYLES.slice(0, 27);
943
- const MAX_SERIALIZED_RUN_BORDER_BYTES = 4096;
944
- const work_document_run_border_POINTS_TO_PIXELS = 96 / 72;
945
- function normalizeDocumentRunBorder(source) {
946
- const border = normalizeDocumentParagraphBorder(source);
947
- return border && !isDocumentParagraphArtBorderStyle(border.style) ? border : null;
788
+ function serializeDocumentProofingLanguages(source) {
789
+ const normalized = normalizeDocumentProofingLanguages(source);
790
+ if (!normalized) return;
791
+ const serialized = JSON.stringify(normalized);
792
+ return serialized.length <= MAX_SERIALIZED_PROOFING_LANGUAGES_BYTES ? serialized : void 0;
948
793
  }
949
- function parseDocumentRunBorder(source) {
950
- if ('string' != typeof source) return normalizeDocumentRunBorder(source);
951
- if (!source.trim() || source.length > MAX_SERIALIZED_RUN_BORDER_BYTES) return null;
794
+ function parseDocumentProofingLanguages(source) {
795
+ if ('string' != typeof source) return normalizeDocumentProofingLanguages(source);
796
+ if (!source || source.length > MAX_SERIALIZED_PROOFING_LANGUAGES_BYTES) return null;
952
797
  try {
953
- return normalizeDocumentRunBorder(JSON.parse(source));
798
+ const normalized = normalizeDocumentProofingLanguages(JSON.parse(source));
799
+ return normalized && JSON.stringify(normalized) === source ? normalized : null;
954
800
  } catch {
955
801
  return null;
956
802
  }
957
803
  }
958
- function serializeDocumentRunBorder(source) {
959
- const border = normalizeDocumentRunBorder(source);
960
- if (!border) return;
961
- const theme = serializeDocxThemeReference(border.color?.theme ?? null);
962
- return JSON.stringify({
963
- style: border.style,
964
- ...border.color ? {
965
- color: {
966
- value: border.color.value,
967
- ...theme ? {
968
- theme: JSON.parse(theme)
969
- } : {}
970
- }
971
- } : {},
972
- ...void 0 !== border.size ? {
973
- size: border.size
804
+ function normalizeDocumentNoProof(source) {
805
+ if (true === source || 'true' === source || '1' === source) return true;
806
+ if (false === source || 'false' === source || '0' === source) return false;
807
+ return null;
808
+ }
809
+ function documentProofingLanguagesFromElement(element) {
810
+ return parseDocumentProofingLanguages(element.getAttribute(DOCUMENT_PROOFING_LANGUAGES_ATTRIBUTE));
811
+ }
812
+ function documentNoProofFromElement(element) {
813
+ return normalizeDocumentNoProof(element.getAttribute(DOCUMENT_NO_PROOF_ATTRIBUTE));
814
+ }
815
+ function documentProofingLanguageForScript(source, slot) {
816
+ const languages = normalizeDocumentProofingLanguages(source);
817
+ if (!languages) return;
818
+ if ('eastAsia' === slot) return languages.eastAsia ?? languages.latin;
819
+ if ('complexScript' === slot) return languages.bidi ?? languages.latin;
820
+ return languages.latin ?? languages.eastAsia ?? languages.bidi;
821
+ }
822
+ function documentProofingDomAttributes(languagesSource, noProofSource, slot) {
823
+ const languages = normalizeDocumentProofingLanguages(languagesSource);
824
+ const serialized = serializeDocumentProofingLanguages(languages);
825
+ const noProof = normalizeDocumentNoProof(noProofSource);
826
+ const language = documentProofingLanguageForScript(languages, slot);
827
+ return {
828
+ ...serialized ? {
829
+ [DOCUMENT_PROOFING_LANGUAGES_ATTRIBUTE]: serialized
974
830
  } : {},
975
- ...void 0 !== border.space ? {
976
- space: border.space
831
+ ...null === noProof ? {} : {
832
+ [DOCUMENT_NO_PROOF_ATTRIBUTE]: String(noProof)
833
+ },
834
+ ...slot ? {
835
+ [DOCUMENT_SCRIPT_FONT_SLOT_ATTRIBUTE]: slot
977
836
  } : {},
978
- ...void 0 !== border.shadow ? {
979
- shadow: border.shadow
837
+ ...language && 'x-none' !== language ? {
838
+ lang: language
980
839
  } : {},
981
- ...void 0 !== border.frame ? {
982
- frame: border.frame
840
+ ...true === noProof ? {
841
+ spellcheck: 'false'
983
842
  } : {}
984
- });
985
- }
986
- function parseDocumentRunBorderElement(element) {
987
- const semantic = parseDocumentRunBorder(element.getAttribute(DOCUMENT_RUN_BORDER_ATTRIBUTE));
988
- return semantic ?? documentRunBorderFromCss(element);
843
+ };
989
844
  }
990
- function documentRunBorderDomAttributes(source) {
991
- const border = normalizeDocumentRunBorder(source);
992
- const serialized = serializeDocumentRunBorder(border);
993
- if (!border || !serialized) return {};
994
- const presentation = documentBorderPresentation(border);
995
- const declarations = [
996
- `border: ${work_document_run_border_formatPixels(presentation.width)}px ${presentation.style} ${presentation.color}`,
997
- `padding: ${work_document_run_border_formatPixels((border.space ?? 0) * work_document_run_border_POINTS_TO_PIXELS)}px`,
998
- 'box-decoration-break: clone',
999
- '-webkit-box-decoration-break: clone'
1000
- ];
1001
- if (border.shadow && presentation.width > 0) declarations.push(`box-shadow: 2px 2px 0 ${presentation.color}`);
1002
- return {
1003
- [DOCUMENT_RUN_BORDER_ATTRIBUTE]: serialized,
1004
- style: declarations.join('; ')
1005
- };
1006
- }
1007
- function documentRunBorderIsVisible(source) {
1008
- const border = normalizeDocumentRunBorder(source);
1009
- return Boolean(border && documentBorderPresentation(border).width > 0);
1010
- }
1011
- function documentRunBorderFromCss(element) {
1012
- const style = element.style.borderStyle.trim();
1013
- if (!style) return null;
1014
- if ('none' === style || 'hidden' === style) return {
1015
- style: 'none'
845
+ function patchDocumentProofingLanguages(source, patch) {
846
+ const current = normalizeDocumentProofingLanguages(source) ?? {};
847
+ const next = {
848
+ ...current
1016
849
  };
1017
- const width = Number.parseFloat(element.style.borderWidth);
1018
- const color = normalizeCssColor(element.style.borderColor);
1019
- if (!Number.isFinite(width) || width <= 0 || !color || 'transparent' === color) return null;
1020
- const padding = Number.parseFloat(element.style.padding);
1021
- return normalizeDocumentRunBorder({
1022
- style: cssBorderStyle(style),
1023
- color: {
1024
- value: color
1025
- },
1026
- size: Math.max(2, Math.min(96, Math.round(6 * width))),
1027
- ...Number.isFinite(padding) && padding >= 0 ? {
1028
- space: Math.max(0, Math.min(31, Math.round(padding / work_document_run_border_POINTS_TO_PIXELS)))
1029
- } : {}
1030
- });
1031
- }
1032
- function cssBorderStyle(style) {
1033
- if ('double' === style) return 'double';
1034
- if ('dashed' === style) return 'dashed';
1035
- if ('dotted' === style) return 'dotted';
1036
- if ('inset' === style || 'groove' === style) return 'inset';
1037
- if ('outset' === style || 'ridge' === style) return 'outset';
1038
- return 'single';
850
+ for (const slot of PROOFING_LANGUAGE_ORDER){
851
+ const value = patch[slot];
852
+ if (void 0 === value) continue;
853
+ if (null === value) {
854
+ delete next[slot];
855
+ continue;
856
+ }
857
+ const language = normalizeDocumentLanguageTag(value);
858
+ if (!language) return null;
859
+ next[slot] = language;
860
+ }
861
+ return Object.keys(next).length ? next : null;
1039
862
  }
1040
- function work_document_run_border_formatPixels(value) {
1041
- return Number(value.toFixed(3)).toString();
863
+ function isRecord(source) {
864
+ return 'object' == typeof source && null !== source && !Array.isArray(source);
1042
865
  }
1043
- const work_document_paragraph_shading_DOCUMENT_PARAGRAPH_SHADING_PATTERNS = new Set([
1044
- 'nil',
1045
- 'clear',
1046
- 'solid',
1047
- 'horzStripe',
1048
- 'vertStripe',
1049
- 'reverseDiagStripe',
1050
- 'diagStripe',
1051
- 'horzCross',
1052
- 'diagCross',
1053
- 'thinHorzStripe',
1054
- 'thinVertStripe',
1055
- 'thinReverseDiagStripe',
1056
- 'thinDiagStripe',
1057
- 'thinHorzCross',
1058
- 'thinDiagCross',
1059
- 'pct5',
1060
- 'pct10',
1061
- 'pct12',
1062
- 'pct15',
1063
- 'pct20',
1064
- 'pct25',
1065
- 'pct30',
1066
- 'pct35',
1067
- 'pct37',
1068
- 'pct40',
1069
- 'pct45',
1070
- 'pct50',
1071
- 'pct55',
1072
- 'pct60',
1073
- 'pct62',
1074
- 'pct65',
1075
- 'pct70',
1076
- 'pct75',
1077
- 'pct80',
1078
- 'pct85',
1079
- 'pct87',
1080
- 'pct90',
1081
- 'pct95'
1082
- ]);
1083
- function normalizeDocumentParagraphShading(source) {
1084
- if (!source || 'object' != typeof source) return null;
1085
- const value = source;
1086
- const pattern = value.pattern;
1087
- if ('string' != typeof pattern || !work_document_paragraph_shading_DOCUMENT_PARAGRAPH_SHADING_PATTERNS.has(pattern)) return null;
1088
- const color = normalizeShadingColor(value.color);
1089
- const fill = normalizeShadingColor(value.fill);
1090
- if (void 0 !== value.color && !color || void 0 !== value.fill && !fill) return null;
1091
- return {
1092
- pattern: pattern,
1093
- ...color ? {
1094
- color
1095
- } : {},
1096
- ...fill ? {
1097
- fill
1098
- } : {}
1099
- };
866
+ function normalizeCssColor(source) {
867
+ const value = source?.trim().toLowerCase();
868
+ if (!value) return null;
869
+ if ('transparent' === value) return 'transparent';
870
+ const shortHex = /^#([0-9a-f]{3})$/i.exec(value);
871
+ if (shortHex?.[1]) return `#${Array.from(shortHex[1]).map((channel)=>`${channel}${channel}`).join('')}`;
872
+ if (/^#[0-9a-f]{6}$/i.test(value)) return value;
873
+ const rgb = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*([\d.]+))?\s*\)$/i.exec(value);
874
+ if (!rgb) return null;
875
+ const channels = rgb.slice(1, 4).map(Number);
876
+ if (channels.some((channel)=>channel < 0 || channel > 255)) return null;
877
+ if (void 0 !== rgb[4] && 0 === Number(rgb[4])) return 'transparent';
878
+ if (void 0 !== rgb[4] && 1 !== Number(rgb[4])) return null;
879
+ return `#${channels.map((channel)=>channel.toString(16).padStart(2, '0')).join('')}`;
1100
880
  }
1101
- function parseDocumentParagraphShading(source) {
1102
- if ('string' != typeof source) return normalizeDocumentParagraphShading(source);
1103
- if (!source.trim()) return null;
881
+ function decodeXmlBytes(bytes, label) {
882
+ let encoding = 'utf-8';
883
+ let offset = 0;
884
+ if (0xef === bytes[0] && 0xbb === bytes[1] && 0xbf === bytes[2]) offset = 3;
885
+ else if (0xff === bytes[0] && 0xfe === bytes[1]) {
886
+ encoding = 'utf-16le';
887
+ offset = 2;
888
+ } else if (0xfe === bytes[0] && 0xff === bytes[1]) {
889
+ encoding = 'utf-16be';
890
+ offset = 2;
891
+ } else if (0x3c === bytes[0] && 0 === bytes[1] && 0x3f === bytes[2] && 0 === bytes[3]) encoding = 'utf-16le';
892
+ else if (0 === bytes[0] && 0x3c === bytes[1] && 0 === bytes[2] && 0x3f === bytes[3]) encoding = 'utf-16be';
1104
893
  try {
1105
- return normalizeDocumentParagraphShading(JSON.parse(source));
894
+ return new TextDecoder(encoding, {
895
+ fatal: true
896
+ }).decode(bytes.subarray(offset));
1106
897
  } catch {
1107
- return null;
898
+ throw new Error(`${label} uses an invalid ${encoding} XML encoding.`);
1108
899
  }
1109
900
  }
1110
- function serializeDocumentParagraphShading(source) {
1111
- const shading = normalizeDocumentParagraphShading(source);
1112
- if (!shading) return;
1113
- return JSON.stringify({
1114
- pattern: shading.pattern,
1115
- ...shading.color ? {
1116
- color: serializedShadingColor(shading.color)
1117
- } : {},
1118
- ...shading.fill ? {
1119
- fill: serializedShadingColor(shading.fill)
1120
- } : {}
1121
- });
901
+ function serializeUtf8Xml(document) {
902
+ const serialized = new XMLSerializer().serializeToString(document);
903
+ const body = serialized.replace(/^\s*<\?xml[^?]*\?>\s*/i, '');
904
+ return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>${body}`;
1122
905
  }
1123
- function parseDocumentParagraphShadingElement(element) {
1124
- const semantic = parseDocumentParagraphShading(element.dataset.officeParagraphShading);
1125
- const background = normalizeCssColor(element.style.backgroundColor);
1126
- if (semantic) {
1127
- const presentation = documentParagraphShadingPresentation(semantic);
1128
- const expected = normalizeCssColor(presentation.backgroundColor);
1129
- if (background && expected && background !== expected) {
1130
- if ('transparent' === background) return {
1131
- pattern: 'nil'
1132
- };
1133
- if (paragraphShadingBackgroundUsesForeground(semantic)) return {
1134
- ...semantic,
1135
- color: {
1136
- value: background
1137
- }
1138
- };
1139
- return {
1140
- ...semantic,
1141
- fill: {
1142
- value: background
1143
- }
1144
- };
1145
- }
1146
- return semantic;
906
+ class OoxmlPackage {
907
+ zip;
908
+ textCache = new Map();
909
+ constructor(zip){
910
+ this.zip = zip;
1147
911
  }
1148
- return background && 'transparent' !== background ? {
1149
- pattern: 'clear',
1150
- fill: {
1151
- value: background
912
+ static async load(buffer) {
913
+ return new OoxmlPackage(await jszip.loadAsync(buffer));
914
+ }
915
+ has(partPath) {
916
+ return Boolean(this.zip.file(partPath));
917
+ }
918
+ paths(prefix) {
919
+ return Object.keys(this.zip.files).filter((path)=>path.startsWith(prefix) && !this.zip.files[path]?.dir);
920
+ }
921
+ async text(partPath) {
922
+ const cached = this.textCache.get(partPath);
923
+ if (cached) return cached;
924
+ const entry = this.zip.file(partPath);
925
+ if (!entry) throw new Error(`Office package part is missing: ${partPath}`);
926
+ const pending = entry.async('uint8array').then((bytes)=>decodeXmlBytes(bytes, partPath));
927
+ this.textCache.set(partPath, pending);
928
+ try {
929
+ return await pending;
930
+ } catch (error) {
931
+ this.textCache.delete(partPath);
932
+ throw error;
1152
933
  }
1153
- } : null;
934
+ }
935
+ async xml(partPath) {
936
+ return parseXml(await this.text(partPath), partPath);
937
+ }
938
+ async bytes(partPath) {
939
+ const entry = this.zip.file(partPath);
940
+ if (!entry) throw new Error(`Office package part is missing: ${partPath}`);
941
+ return entry.async('uint8array');
942
+ }
943
+ async relationships(sourcePart) {
944
+ const partPath = relationshipsPartPath(sourcePart);
945
+ if (!this.has(partPath)) return new Map();
946
+ const document = await this.xml(partPath);
947
+ return new Map(descendants(document, 'Relationship').map((element)=>{
948
+ const relationship = {
949
+ id: attribute(element, 'Id') ?? '',
950
+ target: resolvePartTarget(sourcePart, attribute(element, 'Target') ?? ''),
951
+ type: attribute(element, 'Type') ?? '',
952
+ targetMode: attribute(element, 'TargetMode') ?? void 0
953
+ };
954
+ return [
955
+ relationship.id,
956
+ relationship
957
+ ];
958
+ }));
959
+ }
1154
960
  }
1155
- function documentParagraphShadingDomAttributes(source) {
1156
- const shading = normalizeDocumentParagraphShading(source);
1157
- const serialized = serializeDocumentParagraphShading(shading);
1158
- if (!shading || !serialized) return {};
1159
- const presentation = documentParagraphShadingPresentation(shading);
1160
- const styles = [
1161
- `background-color: ${presentation.backgroundColor}`,
1162
- presentation.backgroundImage ? `background-image: ${presentation.backgroundImage}` : '',
1163
- presentation.backgroundSize ? `background-size: ${presentation.backgroundSize}` : ''
1164
- ].filter(Boolean);
1165
- return {
1166
- 'data-office-paragraph-shading': serialized,
1167
- style: styles.join('; ')
1168
- };
961
+ function parseXml(source, label = 'Office XML') {
962
+ const document = new DOMParser().parseFromString(source, 'application/xml');
963
+ const error = descendants(document, 'parsererror')[0];
964
+ if (error) throw new Error(`${label} is not valid XML: ${error.textContent?.trim() || 'parse error'}`);
965
+ return document;
1169
966
  }
1170
- function normalizeShadingColor(source) {
1171
- if (!source || 'object' != typeof source) return null;
1172
- const value = source;
1173
- const theme = parseDocxThemeReference('string' == typeof value.theme ? value.theme : value.theme ? JSON.stringify(value.theme) : void 0);
1174
- const direct = 'auto' === value.value ? 'auto' : 'string' == typeof value.value ? normalizeCssColor(value.value) : null;
1175
- const resolved = direct ?? theme?.resolved ?? null;
1176
- if (!resolved || 'transparent' === resolved) return null;
1177
- if (theme && resolved !== theme.resolved) return null;
1178
- return {
1179
- value: resolved,
1180
- ...theme ? {
1181
- theme
1182
- } : {}
1183
- };
967
+ const xmlElementPatterns = new Map();
968
+ function xmlContainsAnyElement(source, localNames) {
969
+ if (!source || !localNames.length) return false;
970
+ const key = localNames.join('\u0000');
971
+ let pattern = xmlElementPatterns.get(key);
972
+ if (!pattern) {
973
+ const alternatives = localNames.map((name)=>name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
974
+ pattern = new RegExp(`<(?:[A-Za-z_][\\w.-]*:)?(?:${alternatives})(?=[\\s/>])`);
975
+ xmlElementPatterns.set(key, pattern);
976
+ }
977
+ return pattern.test(source);
1184
978
  }
1185
- function serializedShadingColor(color) {
1186
- const theme = serializeDocxThemeReference(color.theme ?? null);
1187
- return {
1188
- value: color.value,
1189
- ...theme ? {
1190
- theme: JSON.parse(theme)
1191
- } : {}
1192
- };
979
+ function attribute(element, name) {
980
+ const direct = element.getAttribute(name);
981
+ if (null !== direct) return direct;
982
+ const localName = name.includes(':') ? name.slice(name.indexOf(':') + 1) : name;
983
+ return Array.from(element.attributes).find((item)=>{
984
+ const itemLocalName = item.localName.includes(':') ? item.localName.slice(item.localName.indexOf(':') + 1) : item.localName;
985
+ return itemLocalName === localName && (!name.includes(':') || item.name === name);
986
+ })?.value ?? null;
1193
987
  }
1194
- function documentParagraphShadingPresentation(shading) {
1195
- if ('nil' === shading.pattern) return {
1196
- backgroundColor: 'transparent'
1197
- };
1198
- const foreground = shadingColor(shading.color, '#000000');
1199
- const background = shadingColor(shading.fill, 'transparent');
1200
- if ('clear' === shading.pattern) return {
1201
- backgroundColor: background
1202
- };
1203
- if ('solid' === shading.pattern) return {
1204
- backgroundColor: foreground
1205
- };
1206
- const percentage = shadingPercentage(shading.pattern);
1207
- if (null !== percentage) {
1208
- const inverted = percentage > 50;
1209
- const dotColor = inverted ? background : foreground;
1210
- const baseColor = inverted ? foreground : background;
1211
- const density = Math.min(50, inverted ? 100 - percentage : percentage);
1212
- const spacing = Math.max(2, Math.round(9 - density / 7));
1213
- return {
1214
- backgroundColor: baseColor,
1215
- backgroundImage: `radial-gradient(circle, ${dotColor} 0 1px, transparent 1.2px)`,
1216
- backgroundSize: `${spacing}px ${spacing}px`
1217
- };
988
+ function xmlNamespacePrefix(element, namespace) {
989
+ if (!namespace) return element.prefix;
990
+ if ('function' == typeof element.lookupPrefix) {
991
+ const prefix = element.lookupPrefix(namespace);
992
+ if (prefix) return prefix;
1218
993
  }
1219
- const thin = shading.pattern.startsWith('thin');
1220
- const width = thin ? 1 : 2;
1221
- const period = thin ? 7 : 6;
1222
- const stripe = (angle)=>`repeating-linear-gradient(${angle}deg, ${foreground} 0 ${width}px, transparent ${width}px ${period}px)`;
1223
- const angles = shadingPatternAngles(shading.pattern);
1224
- return {
1225
- backgroundColor: background,
1226
- backgroundImage: angles.map(stripe).join(', ')
1227
- };
1228
- }
1229
- function shadingColor(color, fallback) {
1230
- return color && 'auto' !== color.value ? color.value : fallback;
994
+ let current = element;
995
+ while(current){
996
+ if (current.namespaceURI === namespace && current.prefix) return current.prefix;
997
+ const declaration = Array.from(current.attributes).find((item)=>item.value === namespace && ('xmlns' === item.name || item.name.startsWith('xmlns:')));
998
+ if (declaration?.name.startsWith('xmlns:')) return declaration.name.slice(6);
999
+ current = current.parentElement;
1000
+ }
1001
+ return null;
1231
1002
  }
1232
- function shadingPercentage(pattern) {
1233
- const match = /^pct(\d+)$/.exec(pattern);
1234
- return match?.[1] ? Number(match[1]) : null;
1003
+ function directChildren(parent, localName) {
1004
+ return Array.from(parent.children).filter((element)=>!localName || element.localName === localName);
1235
1005
  }
1236
- function paragraphShadingBackgroundUsesForeground(shading) {
1237
- if ('solid' === shading.pattern) return true;
1238
- const percentage = shadingPercentage(shading.pattern);
1239
- return null !== percentage && percentage > 50;
1006
+ function directChild(parent, localName) {
1007
+ return directChildren(parent, localName)[0];
1240
1008
  }
1241
- function shadingPatternAngles(pattern) {
1242
- if (pattern.includes('HorzCross')) return [
1243
- 0,
1244
- 90
1245
- ];
1246
- if (pattern.includes('DiagCross')) return [
1247
- 45,
1248
- -45
1249
- ];
1250
- if (pattern.includes('VertStripe')) return [
1251
- 90
1252
- ];
1253
- if (pattern.includes('ReverseDiagStripe')) return [
1254
- -45
1255
- ];
1256
- if (pattern.includes('DiagStripe')) return [
1257
- 45
1258
- ];
1259
- return [
1260
- 0
1261
- ];
1009
+ function descendants(parent, localName) {
1010
+ return Array.from(parent.querySelectorAll('*')).filter((element)=>element.localName === localName);
1262
1011
  }
1263
- const DOCUMENT_HIGHLIGHT_ATTRIBUTE = 'data-office-highlight';
1264
- const DOCUMENT_HIGHLIGHT_VALUES = new Set([
1265
- 'black',
1266
- 'blue',
1267
- 'cyan',
1268
- 'darkBlue',
1269
- 'darkCyan',
1270
- 'darkGray',
1271
- 'darkGreen',
1272
- 'darkMagenta',
1273
- 'darkRed',
1274
- 'darkYellow',
1275
- 'green',
1276
- 'lightGray',
1277
- 'magenta',
1278
- 'none',
1279
- 'red',
1280
- 'white',
1281
- 'yellow'
1282
- ]);
1283
- const HIGHLIGHT_COLORS = {
1284
- black: '#000000',
1285
- blue: '#0000ff',
1286
- cyan: '#00ffff',
1287
- darkBlue: '#000080',
1288
- darkCyan: '#008080',
1289
- darkGray: '#808080',
1290
- darkGreen: '#008000',
1291
- darkMagenta: '#800080',
1292
- darkRed: '#800000',
1293
- darkYellow: '#808000',
1294
- green: '#00ff00',
1295
- lightGray: '#c0c0c0',
1296
- magenta: '#ff00ff',
1297
- none: 'transparent',
1298
- red: '#ff0000',
1299
- white: '#ffffff',
1300
- yellow: '#ffff00'
1301
- };
1302
- function normalizeDocumentHighlight(source) {
1303
- return 'string' == typeof source && DOCUMENT_HIGHLIGHT_VALUES.has(source) ? source : null;
1012
+ function firstDescendant(parent, localName) {
1013
+ if (!parent) return;
1014
+ return descendants(parent, localName)[0];
1304
1015
  }
1305
- function documentHighlightFromDocxValue(source) {
1306
- if ('string' != typeof source) return null;
1307
- const normalized = source.trim().toLowerCase();
1308
- for (const value of DOCUMENT_HIGHLIGHT_VALUES)if (value.toLowerCase() === normalized) return value;
1309
- return null;
1016
+ function childPath(parent, ...localNames) {
1017
+ let current = parent;
1018
+ for (const name of localNames){
1019
+ if (!current) return;
1020
+ current = directChild(current, name);
1021
+ }
1022
+ return current instanceof Element ? current : void 0;
1310
1023
  }
1311
- function documentHighlightCssColor(source) {
1312
- const value = normalizeDocumentHighlight(source);
1313
- return value ? HIGHLIGHT_COLORS[value] : null;
1024
+ function resolvePartTarget(sourcePart, target) {
1025
+ if (/^[a-z][a-z0-9+.-]*:/i.test(target)) return target;
1026
+ const segments = target.startsWith('/') ? [] : sourcePart.split('/').slice(0, -1);
1027
+ for (const segment of target.replace(/^\/+/, '').split('/'))if (segment && '.' !== segment) if ('..' === segment) segments.pop();
1028
+ else segments.push(segment);
1029
+ return segments.join('/');
1314
1030
  }
1315
- function documentHighlightForCssColor(source) {
1316
- const color = normalizeCssColor('string' == typeof source ? source : null);
1317
- if (!color) return null;
1318
- for (const [value, candidate] of Object.entries(HIGHLIGHT_COLORS))if (candidate === color) return value;
1319
- return null;
1031
+ function contentTypeForPart(partPath) {
1032
+ const extension = partPath.split('.').pop()?.toLowerCase();
1033
+ const types = {
1034
+ apng: 'image/apng',
1035
+ bmp: 'image/bmp',
1036
+ emf: 'image/emf',
1037
+ gif: 'image/gif',
1038
+ jpeg: 'image/jpeg',
1039
+ jpg: 'image/jpeg',
1040
+ png: 'image/png',
1041
+ svg: 'image/svg+xml',
1042
+ tif: 'image/tiff',
1043
+ tiff: 'image/tiff',
1044
+ webp: 'image/webp',
1045
+ wmf: 'image/wmf'
1046
+ };
1047
+ return types[extension ?? ''] ?? 'application/octet-stream';
1320
1048
  }
1321
- function documentHighlightFromElement(element) {
1322
- return normalizeDocumentHighlight(element.getAttribute(DOCUMENT_HIGHLIGHT_ATTRIBUTE)) ?? documentHighlightForCssColor(element.style.backgroundColor);
1049
+ function bytesToDataUrl(bytes, contentType) {
1050
+ let binary = '';
1051
+ const chunkSize = 32768;
1052
+ for(let offset = 0; offset < bytes.length; offset += chunkSize)binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
1053
+ return `data:${contentType};base64,${btoa(binary)}`;
1323
1054
  }
1324
- function documentHighlightDomAttributes(source) {
1325
- const value = normalizeDocumentHighlight(source);
1326
- const color = documentHighlightCssColor(value);
1327
- return value && color ? {
1328
- [DOCUMENT_HIGHLIGHT_ATTRIBUTE]: value,
1329
- style: `background-color: ${color}`
1330
- } : {};
1055
+ function relationshipsPartPath(sourcePart) {
1056
+ const separator = sourcePart.lastIndexOf('/');
1057
+ const directory = separator >= 0 ? sourcePart.slice(0, separator + 1) : '';
1058
+ const fileName = separator >= 0 ? sourcePart.slice(separator + 1) : sourcePart;
1059
+ return `${directory}_rels/${fileName}.rels`;
1331
1060
  }
1332
- const DOCUMENT_RUN_SHADING_ATTRIBUTE = 'data-office-run-shading';
1333
- const MAX_SERIALIZED_RUN_SHADING_BYTES = 4096;
1334
- const RUN_SHADING_KEYS = new Set([
1335
- 'pattern',
1336
- 'color',
1337
- 'fill'
1338
- ]);
1339
- const RUN_SHADING_COLOR_KEYS = new Set([
1340
- 'value',
1341
- 'theme'
1342
- ]);
1343
- const THEME_REFERENCE_KEYS = new Set([
1344
- 'theme',
1345
- 'resolved',
1346
- 'tint',
1347
- 'shade'
1348
- ]);
1349
- function normalizeDocumentRunShading(source) {
1350
- if (!isRecordWithKeys(source, RUN_SHADING_KEYS)) return null;
1351
- for (const name of [
1352
- 'color',
1353
- 'fill'
1354
- ]){
1355
- const color = source[name];
1356
- if (void 0 !== color) {
1357
- if (!isRecordWithKeys(color, RUN_SHADING_COLOR_KEYS)) return null;
1358
- if (void 0 !== color.theme && !isRecordWithKeys(color.theme, THEME_REFERENCE_KEYS)) return null;
1359
- }
1061
+ const WORD_NAMESPACE = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
1062
+ class DocxThemePatchCollector {
1063
+ patches = [];
1064
+ nextMarker = 1;
1065
+ usedColors;
1066
+ constructor(sourceHtml){
1067
+ this.usedColors = sourceColors(sourceHtml);
1068
+ }
1069
+ marker(kind, reference, currentColor) {
1070
+ if (!reference || normalizeColor(currentColor) !== reference.resolved) return null;
1071
+ let marker = '';
1072
+ do {
1073
+ marker = (0xf00000 + this.nextMarker).toString(16).padStart(6, '0').toUpperCase();
1074
+ this.nextMarker += 1;
1075
+ }while (this.usedColors.has(marker))
1076
+ this.usedColors.add(marker);
1077
+ this.patches.push({
1078
+ kind,
1079
+ marker,
1080
+ reference
1081
+ });
1082
+ return marker;
1360
1083
  }
1361
- return normalizeDocumentParagraphShading(source);
1362
1084
  }
1363
- function parseDocumentRunShading(source) {
1364
- if ('string' != typeof source) return normalizeDocumentRunShading(source);
1365
- if (!source.trim() || source.length > MAX_SERIALIZED_RUN_SHADING_BYTES) return null;
1085
+ function serializeDocxThemeReference(reference) {
1086
+ return reference ? JSON.stringify(reference) : void 0;
1087
+ }
1088
+ function parseDocxThemeReference(value) {
1089
+ if (!value) return null;
1366
1090
  try {
1367
- return normalizeDocumentRunShading(JSON.parse(source));
1091
+ const parsed = JSON.parse(value);
1092
+ const theme = 'string' == typeof parsed.theme ? parsed.theme.trim() : '';
1093
+ const resolved = normalizeColor('string' == typeof parsed.resolved ? parsed.resolved : null);
1094
+ const tint = byteHex(parsed.tint);
1095
+ const shade = byteHex(parsed.shade);
1096
+ if (!theme || !resolved) return null;
1097
+ return {
1098
+ theme,
1099
+ resolved,
1100
+ ...tint ? {
1101
+ tint
1102
+ } : {},
1103
+ ...shade ? {
1104
+ shade
1105
+ } : {}
1106
+ };
1368
1107
  } catch {
1369
1108
  return null;
1370
1109
  }
1371
1110
  }
1372
- function serializeDocumentRunShading(source) {
1373
- const shading = normalizeDocumentRunShading(source);
1374
- const serialized = shading ? serializeDocumentParagraphShading(shading) : void 0;
1375
- return serialized && serialized.length <= MAX_SERIALIZED_RUN_SHADING_BYTES ? serialized : void 0;
1111
+ async function patchDocxThemeReferences(buffer, patches) {
1112
+ if (!patches.length) return buffer;
1113
+ const archive = await jszip.loadAsync(buffer);
1114
+ const byMarker = new Map(patches.map((patch)=>[
1115
+ patch.marker,
1116
+ patch
1117
+ ]));
1118
+ const entries = Object.values(archive.files).filter((entry)=>!entry.dir && /^word\/(?:document|header\d+|footer\d+|footnotes|endnotes|comments)\.xml$/.test(entry.name));
1119
+ for (const entry of entries){
1120
+ const document = parseXml(await entry.async('text'), entry.name);
1121
+ let changed = false;
1122
+ for (const element of Array.from(document.getElementsByTagName('*')))for (const target of themePatchTargets(element.localName)){
1123
+ const marker = wordAttribute(element, target.directAttribute)?.toUpperCase();
1124
+ const patch = marker ? byMarker.get(marker) : void 0;
1125
+ if (patch && patch.kind === target.kind) {
1126
+ setWordAttribute(document, element, target.directAttribute, patch.reference.resolved.slice(1).toUpperCase());
1127
+ setWordAttribute(document, element, target.themeAttribute, patch.reference.theme);
1128
+ setOptionalWordAttribute(document, element, target.tintAttribute, patch.reference.tint);
1129
+ setOptionalWordAttribute(document, element, target.shadeAttribute, patch.reference.shade);
1130
+ changed = true;
1131
+ }
1132
+ }
1133
+ if (changed) archive.file(entry.name, new XMLSerializer().serializeToString(document));
1134
+ }
1135
+ return archive.generateAsync({
1136
+ type: 'arraybuffer'
1137
+ });
1376
1138
  }
1377
- function parseDocumentRunShadingElement(element) {
1378
- const semantic = parseDocumentRunShading(element.getAttribute(DOCUMENT_RUN_SHADING_ATTRIBUTE));
1379
- if (!semantic) return null;
1380
- if (element.hasAttribute(DOCUMENT_HIGHLIGHT_ATTRIBUTE)) return semantic;
1381
- const background = normalizeCssColor(element.style.backgroundColor);
1382
- const expected = normalizeCssColor(documentParagraphShadingPresentation(semantic).backgroundColor);
1383
- if (!background || !expected || background === expected) return semantic;
1384
- if ('transparent' === background) return {
1385
- pattern: 'nil'
1386
- };
1387
- return paragraphShadingBackgroundUsesForeground(semantic) ? {
1388
- ...semantic,
1389
- color: {
1390
- value: background
1139
+ function themePatchTargets(localName) {
1140
+ if ('color' === localName) return [
1141
+ {
1142
+ kind: 'color',
1143
+ directAttribute: 'val',
1144
+ themeAttribute: 'themeColor',
1145
+ tintAttribute: 'themeTint',
1146
+ shadeAttribute: 'themeShade'
1391
1147
  }
1392
- } : {
1393
- ...semantic,
1394
- fill: {
1395
- value: background
1148
+ ];
1149
+ if ('u' === localName) return [
1150
+ {
1151
+ kind: 'underline',
1152
+ directAttribute: 'color',
1153
+ themeAttribute: 'themeColor',
1154
+ tintAttribute: 'themeTint',
1155
+ shadeAttribute: 'themeShade'
1396
1156
  }
1397
- };
1398
- }
1399
- function documentRunShadingDomAttributes(source) {
1400
- const shading = normalizeDocumentRunShading(source);
1401
- const serialized = serializeDocumentRunShading(shading);
1402
- if (!shading || !serialized) return {};
1403
- const paragraphAttributes = documentParagraphShadingDomAttributes(shading);
1404
- const style = [
1405
- paragraphAttributes.style,
1406
- 'box-decoration-break: clone',
1407
- '-webkit-box-decoration-break: clone'
1408
- ].filter(Boolean).join('; ');
1409
- return {
1410
- [DOCUMENT_RUN_SHADING_ATTRIBUTE]: serialized,
1411
- style
1412
- };
1157
+ ];
1158
+ if ('shd' === localName) return [
1159
+ {
1160
+ kind: 'fill',
1161
+ directAttribute: 'fill',
1162
+ themeAttribute: 'themeFill',
1163
+ tintAttribute: 'themeFillTint',
1164
+ shadeAttribute: 'themeFillShade'
1165
+ },
1166
+ {
1167
+ kind: 'shadingColor',
1168
+ directAttribute: 'color',
1169
+ themeAttribute: 'themeColor',
1170
+ tintAttribute: 'themeTint',
1171
+ shadeAttribute: 'themeShade'
1172
+ }
1173
+ ];
1174
+ return [
1175
+ 'top',
1176
+ 'right',
1177
+ 'bottom',
1178
+ 'left',
1179
+ 'start',
1180
+ 'end'
1181
+ ].includes(localName) ? [
1182
+ {
1183
+ kind: 'border',
1184
+ directAttribute: 'color',
1185
+ themeAttribute: 'themeColor',
1186
+ tintAttribute: 'themeTint',
1187
+ shadeAttribute: 'themeShade'
1188
+ }
1189
+ ] : [];
1413
1190
  }
1414
- function isRecordWithKeys(source, allowed) {
1415
- return 'object' == typeof source && null !== source && !Array.isArray(source) && Object.keys(source).every((key)=>allowed.has(key));
1191
+ function normalizeColor(value) {
1192
+ const normalized = value?.trim().toLowerCase();
1193
+ if (!normalized || !/^#[0-9a-f]{6}$/.test(normalized)) return null;
1194
+ return normalized;
1416
1195
  }
1417
- const DOCUMENT_SCRIPT_FONTS_ATTRIBUTE = "data-office-script-fonts";
1418
- const DOCUMENT_SCRIPT_FONT_SLOT_ATTRIBUTE = "data-office-script-font-slot";
1419
- const MAX_FONT_NAME_LENGTH = 127;
1420
- const MAX_SERIALIZED_SCRIPT_FONTS_LENGTH = 4096;
1421
- const SCRIPT_FONT_KEYS = new Set([
1422
- 'ascii',
1423
- 'highAnsi',
1424
- 'eastAsia',
1425
- 'complexScript',
1426
- 'hint'
1427
- ]);
1428
- const SCRIPT_FONT_FACE_KEYS = new Set([
1429
- 'name',
1430
- 'theme',
1431
- 'resolved'
1432
- ]);
1433
- const SCRIPT_FONT_HINTS = new Set([
1434
- 'default',
1435
- 'eastAsia',
1436
- 'cs'
1437
- ]);
1438
- const THEME_FONTS = new Set([
1439
- 'majorEastAsia',
1440
- 'majorBidi',
1441
- 'majorAscii',
1442
- 'majorHAnsi',
1443
- 'minorEastAsia',
1444
- 'minorBidi',
1445
- 'minorAscii',
1446
- 'minorHAnsi'
1447
- ]);
1448
- const SLOT_FALLBACK_ORDER = {
1449
- ascii: [
1450
- 'ascii',
1451
- 'highAnsi',
1452
- 'eastAsia',
1453
- 'complexScript'
1454
- ],
1455
- highAnsi: [
1456
- 'highAnsi',
1457
- 'ascii',
1458
- 'eastAsia',
1459
- 'complexScript'
1460
- ],
1461
- eastAsia: [
1462
- 'eastAsia',
1463
- 'highAnsi',
1464
- 'ascii',
1465
- 'complexScript'
1466
- ],
1467
- complexScript: [
1468
- 'complexScript',
1469
- 'highAnsi',
1470
- 'ascii',
1471
- 'eastAsia'
1472
- ]
1473
- };
1474
- const NEUTRAL_SCRIPT_CHARACTER = /^[\p{Cc}\p{Cf}\p{M}\p{N}\p{P}\p{S}\p{Z}]$/u;
1475
- function normalizeDocumentScriptFonts(source) {
1476
- if (!work_document_script_fonts_isRecordWithKeys(source, SCRIPT_FONT_KEYS)) return null;
1477
- const normalized = {};
1478
- for (const slot of scriptFontSlots){
1479
- if (void 0 === source[slot]) continue;
1480
- const face = normalizeDocumentScriptFontFace(source[slot]);
1481
- if (!face) return null;
1482
- normalized[slot] = face;
1483
- }
1484
- if (void 0 !== source.hint) {
1485
- const hint = normalizeDocumentScriptFontHint(source.hint);
1486
- if (!hint) return null;
1487
- normalized.hint = hint;
1196
+ function sourceColors(source) {
1197
+ const colors = new Set();
1198
+ for (const match of source.matchAll(/#([0-9a-f]{6})\b/gi))if (match[1]) colors.add(match[1].toUpperCase());
1199
+ for (const match of source.matchAll(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/gi)){
1200
+ const channels = match.slice(1, 4).map(Number);
1201
+ if (!channels.some((channel)=>channel < 0 || channel > 255)) colors.add(channels.map((channel)=>channel.toString(16).padStart(2, '0')).join('').toUpperCase());
1488
1202
  }
1489
- return Object.keys(normalized).length ? normalized : null;
1203
+ return colors;
1490
1204
  }
1491
- function serializeDocumentScriptFonts(source) {
1492
- const fonts = normalizeDocumentScriptFonts(source);
1493
- return fonts ? JSON.stringify(fonts) : null;
1205
+ function byteHex(value) {
1206
+ if ('string' != typeof value) return;
1207
+ const normalized = value.trim().toUpperCase();
1208
+ return /^[0-9A-F]{2}$/.test(normalized) ? normalized : void 0;
1494
1209
  }
1495
- function parseDocumentScriptFonts(source) {
1496
- if (!source || source.length > MAX_SERIALIZED_SCRIPT_FONTS_LENGTH) return null;
1497
- try {
1498
- return normalizeDocumentScriptFonts(JSON.parse(source));
1499
- } catch {
1500
- return null;
1210
+ function wordAttribute(element, name) {
1211
+ return element.getAttributeNS(WORD_NAMESPACE, name) ?? element.getAttribute(`w:${name}`);
1212
+ }
1213
+ function setWordAttribute(document, element, name, value) {
1214
+ const prefix = xmlNamespacePrefix(document.documentElement, WORD_NAMESPACE) ?? 'w';
1215
+ element.setAttributeNS(WORD_NAMESPACE, `${prefix}:${name}`, value);
1216
+ }
1217
+ function setOptionalWordAttribute(document, element, name, value) {
1218
+ if (value) setWordAttribute(document, element, name, value);
1219
+ else element.removeAttributeNS(WORD_NAMESPACE, name);
1220
+ }
1221
+ const DOCUMENT_PARAGRAPH_BORDERS_ATTRIBUTE = 'data-office-paragraph-borders';
1222
+ const DOCUMENT_PARAGRAPH_BORDER_EDGES = [
1223
+ 'top',
1224
+ 'left',
1225
+ 'bottom',
1226
+ 'right',
1227
+ 'between',
1228
+ 'bar'
1229
+ ];
1230
+ const DOCUMENT_PARAGRAPH_BORDER_STYLES = [
1231
+ 'nil',
1232
+ 'none',
1233
+ 'single',
1234
+ 'thick',
1235
+ 'double',
1236
+ 'dotted',
1237
+ 'dashed',
1238
+ 'dotDash',
1239
+ 'dotDotDash',
1240
+ 'triple',
1241
+ 'thinThickSmallGap',
1242
+ 'thickThinSmallGap',
1243
+ 'thinThickThinSmallGap',
1244
+ 'thinThickMediumGap',
1245
+ 'thickThinMediumGap',
1246
+ 'thinThickThinMediumGap',
1247
+ 'thinThickLargeGap',
1248
+ 'thickThinLargeGap',
1249
+ 'thinThickThinLargeGap',
1250
+ 'wave',
1251
+ 'doubleWave',
1252
+ 'dashSmallGap',
1253
+ 'dashDotStroked',
1254
+ 'threeDEmboss',
1255
+ 'threeDEngrave',
1256
+ 'outset',
1257
+ 'inset',
1258
+ 'apples',
1259
+ 'archedScallops',
1260
+ 'babyPacifier',
1261
+ 'babyRattle',
1262
+ 'balloons3Colors',
1263
+ 'balloonsHotAir',
1264
+ 'basicBlackDashes',
1265
+ 'basicBlackDots',
1266
+ 'basicBlackSquares',
1267
+ 'basicThinLines',
1268
+ 'basicWhiteDashes',
1269
+ 'basicWhiteDots',
1270
+ 'basicWhiteSquares',
1271
+ 'basicWideInline',
1272
+ 'basicWideMidline',
1273
+ 'basicWideOutline',
1274
+ 'bats',
1275
+ 'birds',
1276
+ 'birdsFlight',
1277
+ 'cabins',
1278
+ 'cakeSlice',
1279
+ 'candyCorn',
1280
+ 'celticKnotwork',
1281
+ 'certificateBanner',
1282
+ 'chainLink',
1283
+ 'champagneBottle',
1284
+ 'checkedBarBlack',
1285
+ 'checkedBarColor',
1286
+ 'checkered',
1287
+ 'christmasTree',
1288
+ 'circlesLines',
1289
+ 'circlesRectangles',
1290
+ 'classicalWave',
1291
+ 'clocks',
1292
+ 'compass',
1293
+ 'confetti',
1294
+ 'confettiGrays',
1295
+ 'confettiOutline',
1296
+ 'confettiStreamers',
1297
+ 'confettiWhite',
1298
+ 'cornerTriangles',
1299
+ 'couponCutoutDashes',
1300
+ 'couponCutoutDots',
1301
+ 'crazyMaze',
1302
+ 'creaturesButterfly',
1303
+ 'creaturesFish',
1304
+ 'creaturesInsects',
1305
+ 'creaturesLadyBug',
1306
+ 'crossStitch',
1307
+ 'cup',
1308
+ 'decoArch',
1309
+ 'decoArchColor',
1310
+ 'decoBlocks',
1311
+ 'diamondsGray',
1312
+ 'doubleD',
1313
+ 'doubleDiamonds',
1314
+ 'earth1',
1315
+ 'earth2',
1316
+ 'eclipsingSquares1',
1317
+ 'eclipsingSquares2',
1318
+ 'eggsBlack',
1319
+ 'fans',
1320
+ 'film',
1321
+ 'firecrackers',
1322
+ 'flowersBlockPrint',
1323
+ 'flowersDaisies',
1324
+ 'flowersModern1',
1325
+ 'flowersModern2',
1326
+ 'flowersPansy',
1327
+ 'flowersRedRose',
1328
+ 'flowersRoses',
1329
+ 'flowersTeacup',
1330
+ 'flowersTiny',
1331
+ 'gems',
1332
+ 'gingerbreadMan',
1333
+ 'gradient',
1334
+ 'handmade1',
1335
+ 'handmade2',
1336
+ 'heartBalloon',
1337
+ 'heartGray',
1338
+ 'hearts',
1339
+ 'heebieJeebies',
1340
+ 'holly',
1341
+ 'houseFunky',
1342
+ 'hypnotic',
1343
+ 'iceCreamCones',
1344
+ 'lightBulb',
1345
+ 'lightning1',
1346
+ 'lightning2',
1347
+ 'mapPins',
1348
+ 'mapleLeaf',
1349
+ 'mapleMuffins',
1350
+ 'marquee',
1351
+ 'marqueeToothed',
1352
+ 'moons',
1353
+ 'mosaic',
1354
+ 'musicNotes',
1355
+ 'northwest',
1356
+ 'ovals',
1357
+ 'packages',
1358
+ 'palmsBlack',
1359
+ 'palmsColor',
1360
+ 'paperClips',
1361
+ 'papyrus',
1362
+ 'partyFavor',
1363
+ 'partyGlass',
1364
+ 'pencils',
1365
+ 'people',
1366
+ 'peopleWaving',
1367
+ 'peopleHats',
1368
+ 'poinsettias',
1369
+ 'postageStamp',
1370
+ 'pumpkin1',
1371
+ 'pushPinNote2',
1372
+ 'pushPinNote1',
1373
+ 'pyramids',
1374
+ 'pyramidsAbove',
1375
+ 'quadrants',
1376
+ 'rings',
1377
+ 'safari',
1378
+ 'sawtooth',
1379
+ 'sawtoothGray',
1380
+ 'scaredCat',
1381
+ 'seattle',
1382
+ 'shadowedSquares',
1383
+ 'sharksTeeth',
1384
+ 'shorebirdTracks',
1385
+ 'skyrocket',
1386
+ 'snowflakeFancy',
1387
+ 'snowflakes',
1388
+ 'sombrero',
1389
+ 'southwest',
1390
+ 'stars',
1391
+ 'starsTop',
1392
+ 'stars3d',
1393
+ 'starsBlack',
1394
+ 'starsShadowed',
1395
+ 'sun',
1396
+ 'swirligig',
1397
+ 'tornPaper',
1398
+ 'tornPaperBlack',
1399
+ 'trees',
1400
+ 'triangleParty',
1401
+ 'triangles',
1402
+ 'tribal1',
1403
+ 'tribal2',
1404
+ 'tribal3',
1405
+ 'tribal4',
1406
+ 'tribal5',
1407
+ 'tribal6',
1408
+ 'triangle1',
1409
+ 'triangle2',
1410
+ 'triangleCircle1',
1411
+ 'triangleCircle2',
1412
+ 'shapes1',
1413
+ 'shapes2',
1414
+ 'twistedLines1',
1415
+ 'twistedLines2',
1416
+ 'vine',
1417
+ 'waveline',
1418
+ 'weavingAngles',
1419
+ 'weavingBraid',
1420
+ 'weavingRibbon',
1421
+ 'weavingStrips',
1422
+ 'whiteFlowers',
1423
+ 'woodwork',
1424
+ 'xIllusions',
1425
+ 'zanyTriangles',
1426
+ 'zigZag',
1427
+ 'zigZagStitch'
1428
+ ];
1429
+ const BORDER_STYLE_SET = new Set(DOCUMENT_PARAGRAPH_BORDER_STYLES);
1430
+ const BORDER_EDGE_SET = new Set(DOCUMENT_PARAGRAPH_BORDER_EDGES);
1431
+ const BORDER_PROPERTY_SET = new Set([
1432
+ 'style',
1433
+ 'color',
1434
+ 'size',
1435
+ 'space',
1436
+ 'shadow',
1437
+ 'frame'
1438
+ ]);
1439
+ const LINE_BORDER_STYLES = new Set(DOCUMENT_PARAGRAPH_BORDER_STYLES.slice(0, 27));
1440
+ const DASHED_BORDER_STYLES = new Set([
1441
+ 'dashed',
1442
+ 'dashSmallGap',
1443
+ 'dashDotStroked',
1444
+ 'dotDash',
1445
+ 'dotDotDash'
1446
+ ]);
1447
+ const DOUBLE_BORDER_STYLES = new Set([
1448
+ 'double',
1449
+ 'triple',
1450
+ 'thinThickSmallGap',
1451
+ 'thickThinSmallGap',
1452
+ 'thinThickThinSmallGap',
1453
+ 'thinThickMediumGap',
1454
+ 'thickThinMediumGap',
1455
+ 'thinThickThinMediumGap',
1456
+ 'thinThickLargeGap',
1457
+ 'thickThinLargeGap',
1458
+ 'thinThickThinLargeGap',
1459
+ 'doubleWave'
1460
+ ]);
1461
+ const MAX_SERIALIZED_PARAGRAPH_BORDERS = 32768;
1462
+ const POINTS_TO_PIXELS = 96 / 72;
1463
+ function normalizeDocumentParagraphBorders(source) {
1464
+ if (!source || 'object' != typeof source || Array.isArray(source)) return null;
1465
+ const record = source;
1466
+ if (Object.keys(record).some((key)=>!BORDER_EDGE_SET.has(key))) return null;
1467
+ const borders = {};
1468
+ for (const edge of DOCUMENT_PARAGRAPH_BORDER_EDGES){
1469
+ if (void 0 === record[edge]) continue;
1470
+ const border = normalizeDocumentParagraphBorder(record[edge]);
1471
+ if (!border) return null;
1472
+ borders[edge] = border;
1501
1473
  }
1474
+ return Object.keys(borders).length ? borders : null;
1502
1475
  }
1503
- function documentScriptFontsFromElement(element) {
1504
- return parseDocumentScriptFonts(element.getAttribute(DOCUMENT_SCRIPT_FONTS_ATTRIBUTE));
1505
- }
1506
- function documentScriptFontSlotFromElement(element) {
1507
- return normalizeDocumentScriptFontSlot(element.getAttribute(DOCUMENT_SCRIPT_FONT_SLOT_ATTRIBUTE));
1508
- }
1509
- function documentScriptFontsDomAttributes(source, slot) {
1510
- const fonts = normalizeDocumentScriptFonts(source);
1511
- const normalizedSlot = normalizeDocumentScriptFontSlot(slot);
1512
- if (!fonts) return {};
1513
- const serialized = serializeDocumentScriptFonts(fonts);
1514
- if (!serialized) return {};
1515
- const family = documentScriptFontFamily(fonts, normalizedSlot ?? documentScriptFontSlotFromHint(fonts.hint));
1476
+ function normalizeDocumentParagraphBorder(source) {
1477
+ if (!source || 'object' != typeof source || Array.isArray(source)) return null;
1478
+ const record = source;
1479
+ if (Object.keys(record).some((key)=>!BORDER_PROPERTY_SET.has(key))) return null;
1480
+ const style = record.style;
1481
+ if ('string' != typeof style || !BORDER_STYLE_SET.has(style)) return null;
1482
+ const normalizedStyle = style;
1483
+ const color = normalizeBorderColor(record.color);
1484
+ if (void 0 !== record.color && !color) return null;
1485
+ const size = optionalInteger(record.size);
1486
+ if (null === size || void 0 !== size && !validBorderSize(normalizedStyle, size)) return null;
1487
+ const space = optionalInteger(record.space);
1488
+ if (null === space || void 0 !== space && (space < 0 || space > 31)) return null;
1489
+ const shadow = optionalBoolean(record.shadow);
1490
+ const frame = optionalBoolean(record.frame);
1491
+ if (null === shadow || null === frame) return null;
1516
1492
  return {
1517
- [DOCUMENT_SCRIPT_FONTS_ATTRIBUTE]: serialized,
1518
- ...normalizedSlot ? {
1519
- [DOCUMENT_SCRIPT_FONT_SLOT_ATTRIBUTE]: normalizedSlot
1493
+ style: normalizedStyle,
1494
+ ...color ? {
1495
+ color
1520
1496
  } : {},
1521
- ...family ? {
1522
- style: `font-family: ${family}`
1497
+ ...void 0 !== size ? {
1498
+ size
1499
+ } : {},
1500
+ ...void 0 !== space ? {
1501
+ space
1502
+ } : {},
1503
+ ...void 0 !== shadow ? {
1504
+ shadow
1505
+ } : {},
1506
+ ...void 0 !== frame ? {
1507
+ frame
1523
1508
  } : {}
1524
1509
  };
1525
1510
  }
1526
- function documentScriptFontFamily(source, slot) {
1527
- const fonts = normalizeDocumentScriptFonts(source);
1528
- if (!fonts) return;
1529
- const families = [];
1530
- const seen = new Set();
1531
- for (const candidate of documentScriptFontFallbackSlots(slot)){
1532
- const family = documentScriptFontFaceFamily(fonts[candidate]);
1533
- const key = family?.toLocaleLowerCase();
1534
- if (!(!family || !key || seen.has(key))) {
1535
- seen.add(key);
1536
- families.push(cssFontFamily(family));
1537
- }
1511
+ function parseDocumentParagraphBorders(source) {
1512
+ if ('string' != typeof source) return normalizeDocumentParagraphBorders(source);
1513
+ if (!source.trim() || source.length > MAX_SERIALIZED_PARAGRAPH_BORDERS) return null;
1514
+ try {
1515
+ return normalizeDocumentParagraphBorders(JSON.parse(source));
1516
+ } catch {
1517
+ return null;
1538
1518
  }
1539
- return families.length ? families.join(', ') : void 0;
1540
- }
1541
- function documentScriptFontFallbackSlots(slot) {
1542
- return SLOT_FALLBACK_ORDER[slot];
1543
- }
1544
- function documentScriptFontFamilyForRendering(source, slot, currentFontFamily) {
1545
- const projected = documentScriptFontFamily(source, slot);
1546
- const safeCurrent = safeCssFontFamilyList(currentFontFamily);
1547
- if (!safeCurrent || !projected) return projected;
1548
- const currentPrimary = documentFontNameFromCssFamily(safeCurrent);
1549
- const projectedPrimary = documentFontNameFromCssFamily(projected);
1550
- return currentPrimary && projectedPrimary && currentPrimary.toLocaleLowerCase() === projectedPrimary.toLocaleLowerCase() ? safeCurrent : projected;
1551
- }
1552
- function documentScriptFontDirectFamily(source, slot) {
1553
- const fonts = normalizeDocumentScriptFonts(source);
1554
- return fonts ? documentScriptFontFaceFamily(fonts[slot]) ?? null : null;
1555
1519
  }
1556
- function documentScriptFontsForAllText(fontFamily) {
1557
- const name = documentFontNameFromCssFamily(fontFamily);
1558
- if (!name) return null;
1559
- const face = {
1560
- name,
1561
- resolved: name
1562
- };
1563
- return {
1564
- ascii: face,
1565
- highAnsi: face,
1566
- eastAsia: face,
1567
- complexScript: face,
1568
- hint: 'default'
1569
- };
1520
+ function serializeDocumentParagraphBorders(source) {
1521
+ const borders = normalizeDocumentParagraphBorders(source);
1522
+ if (!borders) return;
1523
+ return JSON.stringify(Object.fromEntries(DOCUMENT_PARAGRAPH_BORDER_EDGES.flatMap((edge)=>{
1524
+ const border = borders[edge];
1525
+ return border ? [
1526
+ [
1527
+ edge,
1528
+ serializedBorder(border)
1529
+ ]
1530
+ ] : [];
1531
+ })));
1570
1532
  }
1571
- function patchDocumentScriptFonts(source, patch, fallbackFontFamily) {
1572
- const current = normalizeDocumentScriptFonts(source) ?? documentScriptFontsForAllText(fallbackFontFamily) ?? {};
1573
- const next = {
1574
- ...current
1533
+ function parseDocumentParagraphBordersElement(element) {
1534
+ const semantic = parseDocumentParagraphBorders(element.getAttribute(DOCUMENT_PARAGRAPH_BORDERS_ATTRIBUTE));
1535
+ if (!semantic) return paragraphBordersFromCss(element);
1536
+ const edited = {
1537
+ ...semantic
1575
1538
  };
1576
- if (void 0 !== patch.latin) {
1577
- const face = directFontFace(patch.latin);
1578
- if (face) {
1579
- next.ascii = face;
1580
- next.highAnsi = face;
1581
- } else {
1582
- delete next.ascii;
1583
- delete next.highAnsi;
1584
- }
1585
- }
1586
- if (void 0 !== patch.eastAsia) {
1587
- const face = directFontFace(patch.eastAsia);
1588
- if (face) next.eastAsia = face;
1589
- else delete next.eastAsia;
1590
- }
1591
- if (void 0 !== patch.complexScript) {
1592
- const face = directFontFace(patch.complexScript);
1593
- if (face) next.complexScript = face;
1594
- else delete next.complexScript;
1595
- }
1596
- return normalizeDocumentScriptFonts(next);
1597
- }
1598
- function documentScriptFontSegments(text, hint = 'default', forceComplexScript = false) {
1599
- if (!text) return [];
1600
- if (forceComplexScript) return [
1601
- {
1602
- from: 0,
1603
- to: text.length,
1604
- slot: 'complexScript'
1605
- }
1606
- ];
1607
- const characters = [];
1608
- let offset = 0;
1609
- for (const character of text){
1610
- const from = offset;
1611
- offset += character.length;
1612
- characters.push({
1613
- from,
1614
- to: offset,
1615
- slot: strongDocumentScriptFontSlot(character)
1616
- });
1617
- }
1618
- const fallback = documentScriptFontSlotFromHint(hint);
1619
- let previous = null;
1620
- for(let index = 0; index < characters.length; index += 1){
1621
- const entry = characters[index];
1622
- if (!entry) continue;
1623
- if (entry.slot) {
1624
- previous = entry.slot;
1625
- continue;
1626
- }
1627
- let next = previous;
1628
- if (!next) for(let cursor = index + 1; cursor < characters.length; cursor += 1){
1629
- const candidate = characters[cursor]?.slot;
1630
- if (candidate) {
1631
- next = candidate;
1632
- break;
1633
- }
1634
- }
1635
- entry.slot = next ?? fallback;
1636
- }
1637
- const segments = [];
1638
- for (const entry of characters){
1639
- const slot = entry.slot ?? fallback;
1640
- const prior = segments[segments.length - 1];
1641
- if (prior?.slot === slot && prior.to === entry.from) prior.to = entry.to;
1642
- else segments.push({
1643
- from: entry.from,
1644
- to: entry.to,
1645
- slot
1646
- });
1647
- }
1648
- return segments;
1649
- }
1650
- function normalizeDocumentScriptFontSlot(value) {
1651
- return scriptFontSlots.includes(value) ? value : null;
1652
- }
1653
- function normalizeDocumentScriptFontHint(value) {
1654
- return SCRIPT_FONT_HINTS.has(value) ? value : null;
1655
- }
1656
- function normalizeDocumentThemeFont(value) {
1657
- return THEME_FONTS.has(value) ? value : null;
1658
- }
1659
- function documentScriptFontSlotFromHint(hint) {
1660
- if ('eastAsia' === hint) return 'eastAsia';
1661
- if ('cs' === hint) return 'complexScript';
1662
- return 'ascii';
1663
- }
1664
- function documentFontNameFromCssFamily(value) {
1665
- if ('string' != typeof value) return null;
1666
- const source = value.trim();
1667
- if (!source) return null;
1668
- let family = '';
1669
- const quote = source[0];
1670
- if ('"' === quote || "'" === quote) {
1671
- let closed = false;
1672
- for(let index = 1; index < source.length; index += 1){
1673
- const character = source[index];
1674
- if (character === quote) {
1675
- closed = true;
1676
- break;
1677
- }
1678
- if ('\\' !== character) {
1679
- family += character;
1539
+ for (const edge of [
1540
+ 'top',
1541
+ 'left',
1542
+ 'bottom',
1543
+ 'right'
1544
+ ]){
1545
+ const border = semantic[edge];
1546
+ const css = cssBorder(element, edge);
1547
+ if (!(!css || border && sameBorderPresentation(border, css))) {
1548
+ if ('none' === css.style) {
1549
+ edited[edge] = {
1550
+ style: 'nil'
1551
+ };
1680
1552
  continue;
1681
1553
  }
1682
- const decoded = decodeCssEscape(source, index + 1);
1683
- if (!decoded) return null;
1684
- family += decoded.value;
1685
- index = decoded.end - 1;
1554
+ edited[edge] = {
1555
+ ...border ?? {
1556
+ style: 'single'
1557
+ },
1558
+ style: cssStyleToBorderStyle(css.style),
1559
+ color: {
1560
+ value: css.color
1561
+ },
1562
+ size: cssWidthToEighthPoints(css.width)
1563
+ };
1686
1564
  }
1687
- if (!closed) return null;
1688
- } else family = source.split(',')[0] ?? '';
1689
- return normalizeDocumentFontName(family);
1565
+ }
1566
+ return normalizeDocumentParagraphBorders(edited);
1690
1567
  }
1691
- function cssDocumentFontFamily(value) {
1692
- const family = normalizeDocumentFontName(value);
1693
- return family ? cssFontFamily(family) : null;
1568
+ function documentParagraphBordersDomAttributes(source) {
1569
+ const borders = normalizeDocumentParagraphBorders(source);
1570
+ const serialized = serializeDocumentParagraphBorders(borders);
1571
+ if (!borders || !serialized) return {};
1572
+ const styles = [];
1573
+ const shadows = [];
1574
+ for (const edge of [
1575
+ 'top',
1576
+ 'left',
1577
+ 'bottom',
1578
+ 'right'
1579
+ ]){
1580
+ const border = borders[edge];
1581
+ if (!border) continue;
1582
+ const presentation = documentBorderPresentation(border);
1583
+ styles.push(`border-${edge}: ${formatPixels(presentation.width)}px ${presentation.style} ${presentation.color}`);
1584
+ if (border.space) styles.push(`padding-${edge}: ${formatPixels(border.space * POINTS_TO_PIXELS)}px`);
1585
+ if (border.shadow && presentation.width > 0) shadows.push(`2px 2px 0 ${presentation.color}`);
1586
+ }
1587
+ const between = borders.between ? documentBorderPresentation(borders.between) : null;
1588
+ if (between && between.width > 0) shadows.push(`inset 0 -${formatPixels(between.width)}px 0 ${between.color}`);
1589
+ const bar = borders.bar ? documentBorderPresentation(borders.bar) : null;
1590
+ if (bar && bar.width > 0) shadows.push(`inset ${formatPixels(bar.width)}px 0 0 ${bar.color}`);
1591
+ if (shadows.length) styles.push(`box-shadow: ${shadows.join(', ')}`);
1592
+ return {
1593
+ [DOCUMENT_PARAGRAPH_BORDERS_ATTRIBUTE]: serialized,
1594
+ ...styles.length ? {
1595
+ style: styles.join('; ')
1596
+ } : {}
1597
+ };
1694
1598
  }
1695
- function normalizeDocumentFontName(value) {
1696
- if ('string' != typeof value) return null;
1697
- const normalized = value.trim();
1698
- return normalized && normalized.length <= MAX_FONT_NAME_LENGTH && !/[\p{Cc}\p{Cs}]/u.test(normalized) ? normalized : null;
1599
+ function isDocumentParagraphArtBorderStyle(style) {
1600
+ return !LINE_BORDER_STYLES.has(style);
1699
1601
  }
1700
- const scriptFontSlots = [
1701
- 'ascii',
1702
- 'highAnsi',
1703
- 'eastAsia',
1704
- 'complexScript'
1705
- ];
1706
- function normalizeDocumentScriptFontFace(source) {
1707
- if (!work_document_script_fonts_isRecordWithKeys(source, SCRIPT_FONT_FACE_KEYS)) return null;
1708
- const name = void 0 === source.name ? void 0 : normalizeDocumentFontName(source.name);
1709
- const resolved = void 0 === source.resolved ? void 0 : normalizeDocumentFontName(source.resolved);
1710
- const theme = void 0 === source.theme ? void 0 : normalizeDocumentThemeFont(source.theme);
1711
- if (void 0 !== source.name && !name || void 0 !== source.resolved && !resolved || null === theme || !name && !theme && !resolved) return null;
1602
+ function normalizeBorderColor(source) {
1603
+ if (!source || 'object' != typeof source || Array.isArray(source)) return null;
1604
+ const record = source;
1605
+ if (Object.keys(record).some((key)=>'value' !== key && 'theme' !== key)) return null;
1606
+ const theme = parseDocxThemeReference('string' == typeof record.theme ? record.theme : record.theme ? JSON.stringify(record.theme) : void 0);
1607
+ const direct = 'auto' === record.value ? 'auto' : 'string' == typeof record.value ? normalizeCssColor(record.value) : null;
1608
+ const resolved = direct ?? theme?.resolved ?? null;
1609
+ if (!resolved || 'transparent' === resolved) return null;
1610
+ if (theme && resolved !== theme.resolved && !('auto' === resolved && 'none' === theme.theme && '#000000' === theme.resolved)) return null;
1712
1611
  return {
1713
- ...name ? {
1714
- name
1715
- } : {},
1612
+ value: resolved,
1716
1613
  ...theme ? {
1717
1614
  theme
1718
- } : {},
1719
- ...resolved ? {
1720
- resolved
1721
1615
  } : {}
1722
1616
  };
1723
1617
  }
1724
- function directFontFace(value) {
1725
- if (null === value) return null;
1726
- const name = documentFontNameFromCssFamily(value) ?? normalizeDocumentFontName(value);
1727
- return name ? {
1728
- name,
1729
- resolved: name
1730
- } : null;
1731
- }
1732
- function documentScriptFontFaceFamily(face) {
1733
- return face?.resolved ?? face?.name;
1734
- }
1735
- function strongDocumentScriptFontSlot(character) {
1736
- if (NEUTRAL_SCRIPT_CHARACTER.test(character)) return null;
1737
- const codePoint = character.codePointAt(0);
1738
- if (void 0 === codePoint) return null;
1739
- if (isComplexScriptCodePoint(codePoint)) return 'complexScript';
1740
- if (isEastAsianCodePoint(codePoint)) return 'eastAsia';
1741
- return codePoint <= 0x7f ? 'ascii' : 'highAnsi';
1618
+ function serializedBorder(border) {
1619
+ const color = border.color ? serializedBorderColor(border.color) : void 0;
1620
+ return {
1621
+ style: border.style,
1622
+ ...color ? {
1623
+ color
1624
+ } : {},
1625
+ ...void 0 !== border.size ? {
1626
+ size: border.size
1627
+ } : {},
1628
+ ...void 0 !== border.space ? {
1629
+ space: border.space
1630
+ } : {},
1631
+ ...void 0 !== border.shadow ? {
1632
+ shadow: border.shadow
1633
+ } : {},
1634
+ ...void 0 !== border.frame ? {
1635
+ frame: border.frame
1636
+ } : {}
1637
+ };
1742
1638
  }
1743
- function isComplexScriptCodePoint(codePoint) {
1744
- return codePoint >= 0x0590 && codePoint <= 0x08ff || codePoint >= 0x0900 && codePoint <= 0x109f || codePoint >= 0x1780 && codePoint <= 0x18af || codePoint >= 0x1900 && codePoint <= 0x1cff || codePoint >= 0xa800 && codePoint <= 0xa8ff || codePoint >= 0xa980 && codePoint <= 0xa9df || codePoint >= 0xaa00 && codePoint <= 0xaa7f || codePoint >= 0xabc0 && codePoint <= 0xabff || codePoint >= 0xfb1d && codePoint <= 0xfdff || codePoint >= 0xfe70 && codePoint <= 0xfeff || codePoint >= 0x10a00 && codePoint <= 0x10fff || codePoint >= 0x11000 && codePoint <= 0x11fff || codePoint >= 0x1e900 && codePoint <= 0x1edff || codePoint >= 0x1ee00 && codePoint <= 0x1eeff;
1639
+ function serializedBorderColor(color) {
1640
+ const theme = serializeDocxThemeReference(color.theme ?? null);
1641
+ return {
1642
+ value: color.value,
1643
+ ...theme ? {
1644
+ theme: JSON.parse(theme)
1645
+ } : {}
1646
+ };
1745
1647
  }
1746
- function isEastAsianCodePoint(codePoint) {
1747
- return codePoint >= 0x1100 && codePoint <= 0x11ff || codePoint >= 0x2e80 && codePoint <= 0xa4cf || codePoint >= 0xac00 && codePoint <= 0xd7af || codePoint >= 0xf900 && codePoint <= 0xfaff || codePoint >= 0xfe10 && codePoint <= 0xfe6f || codePoint >= 0xff00 && codePoint <= 0xffef || codePoint >= 0x20000 && codePoint <= 0x323af;
1648
+ function optionalInteger(value) {
1649
+ if (void 0 === value) return;
1650
+ return 'number' == typeof value && Number.isSafeInteger(value) ? value : null;
1748
1651
  }
1749
- function cssFontFamily(value) {
1750
- return /^(?:-?[\p{L}_])[\p{L}\p{N}_-]*$/u.test(value) ? value : `"${Array.from(value, cssStringCharacter).join('')}"`;
1652
+ function optionalBoolean(value) {
1653
+ if (void 0 === value) return;
1654
+ return 'boolean' == typeof value ? value : null;
1751
1655
  }
1752
- function cssStringCharacter(character) {
1753
- return /[\\":;{}<>]/u.test(character) ? `\\${character.codePointAt(0)?.toString(16)} ` : character;
1656
+ function validBorderSize(style, size) {
1657
+ if ('nil' === style || 'none' === style) return size >= 0 && size <= 96;
1658
+ return isDocumentParagraphArtBorderStyle(style) ? size >= 1 && size <= 31 : size >= 2 && size <= 96;
1754
1659
  }
1755
- function safeCssFontFamilyList(value) {
1756
- if ('string' != typeof value) return null;
1757
- const source = value.trim();
1758
- if (!source || source.length > 1024 || /[;{}]/u.test(source)) return null;
1759
- const tokens = [];
1760
- let start = 0;
1761
- let quote = '';
1762
- for(let index = 0; index < source.length; index += 1){
1763
- const character = source[index] ?? '';
1764
- if (quote) {
1765
- if ('\\' === character) {
1766
- index += 1;
1767
- if (index >= source.length) return null;
1768
- } else if (character === quote) quote = '';
1769
- continue;
1770
- }
1771
- if ('"' === character || "'" === character) {
1772
- quote = character;
1773
- continue;
1774
- }
1775
- if (',' === character) {
1776
- tokens.push(source.slice(start, index).trim());
1777
- start = index + 1;
1778
- }
1779
- }
1780
- if (quote) return null;
1781
- tokens.push(source.slice(start).trim());
1782
- return tokens.length && tokens.every(validCssFontFamilyToken) ? source : null;
1660
+ function documentBorderPresentation(border) {
1661
+ if ('nil' === border.style || 'none' === border.style || !border.size) return {
1662
+ color: 'transparent',
1663
+ style: 'none',
1664
+ width: 0
1665
+ };
1666
+ const width = Math.min(16, isDocumentParagraphArtBorderStyle(border.style) ? border.size * POINTS_TO_PIXELS : border.size / 6);
1667
+ return {
1668
+ color: border.color && 'auto' !== border.color.value ? border.color.value : '#000000',
1669
+ style: borderStyleToCssStyle(border.style),
1670
+ width
1671
+ };
1783
1672
  }
1784
- function validCssFontFamilyToken(value) {
1785
- if (!value) return false;
1786
- if (value.startsWith('"')) return /^"(?:[^"\\\r\n\f]|\\(?:[\da-f]{1,6}\s?|[^\r\n\f]))*"$/iu.test(value) && null !== documentFontNameFromCssFamily(value);
1787
- if (value.startsWith("'")) return /^'(?:[^'\\\r\n\f]|\\(?:[\da-f]{1,6}\s?|[^\r\n\f]))*'$/iu.test(value) && null !== documentFontNameFromCssFamily(value);
1788
- return /[\p{L}_]/u.test(value) && /^[\p{L}_-][\p{L}\p{N}_ -]*$/u.test(value) && !/^(?:inherit|initial|revert(?:-layer)?|unset)$/iu.test(value);
1673
+ function borderStyleToCssStyle(style) {
1674
+ if ('nil' === style || 'none' === style) return 'none';
1675
+ if ('dotted' === style) return 'dotted';
1676
+ if (DASHED_BORDER_STYLES.has(style)) return 'dashed';
1677
+ if (DOUBLE_BORDER_STYLES.has(style)) return 'double';
1678
+ if ('inset' === style || 'threeDEngrave' === style) return 'inset';
1679
+ if ('outset' === style || 'threeDEmboss' === style) return 'outset';
1680
+ return 'solid';
1789
1681
  }
1790
- function decodeCssEscape(source, start) {
1791
- if (start >= source.length) return null;
1792
- const hexadecimal = /^[\da-f]{1,6}/i.exec(source.slice(start))?.[0];
1793
- if (hexadecimal) {
1794
- const codePoint = Number.parseInt(hexadecimal, 16);
1795
- if (0 === codePoint || codePoint > 0x10ffff || codePoint >= 0xd800 && codePoint <= 0xdfff) return null;
1796
- let end = start + hexadecimal.length;
1797
- if (/\s/u.test(source[end] ?? '')) end += 1;
1798
- return {
1799
- value: String.fromCodePoint(codePoint),
1800
- end
1682
+ function paragraphBordersFromCss(element) {
1683
+ const borders = {};
1684
+ for (const edge of [
1685
+ 'top',
1686
+ 'left',
1687
+ 'bottom',
1688
+ 'right'
1689
+ ]){
1690
+ const css = cssBorder(element, edge);
1691
+ if (css && 'none' !== css.style) borders[edge] = {
1692
+ style: cssStyleToBorderStyle(css.style),
1693
+ color: {
1694
+ value: css.color
1695
+ },
1696
+ size: cssWidthToEighthPoints(css.width)
1801
1697
  };
1802
1698
  }
1803
- const value = source[start];
1804
- return !value || /[\r\n\f]/u.test(value) ? null : {
1805
- value,
1806
- end: start + 1
1807
- };
1808
- }
1809
- function work_document_script_fonts_isRecordWithKeys(value, allowed) {
1810
- return 'object' == typeof value && null !== value && !Array.isArray(value) && Object.keys(value).every((key)=>allowed.has(key));
1699
+ return normalizeDocumentParagraphBorders(borders);
1811
1700
  }
1812
- const DOCUMENT_PROOFING_LANGUAGES_ATTRIBUTE = 'data-office-proofing-languages';
1813
- const DOCUMENT_NO_PROOF_ATTRIBUTE = 'data-office-no-proof';
1814
- const PROOFING_LANGUAGE_KEYS = new Set([
1815
- 'latin',
1816
- 'eastAsia',
1817
- 'bidi'
1818
- ]);
1819
- const PROOFING_LANGUAGE_ORDER = [
1820
- 'latin',
1821
- 'eastAsia',
1822
- 'bidi'
1823
- ];
1824
- const MAX_LANGUAGE_TAG_LENGTH = 85;
1825
- const MAX_SERIALIZED_PROOFING_LANGUAGES_BYTES = 384;
1826
- function normalizeDocumentLanguageTag(source) {
1827
- if ('string' != typeof source) return null;
1828
- if (!source || source !== source.trim() || source.length > MAX_LANGUAGE_TAG_LENGTH || /[\p{Cc}\p{Cs}]/u.test(source)) return null;
1829
- return /^(?:x-none|[a-z0-9]{1,8}(?:-[a-z0-9]{1,8})*)$/iu.test(source) ? source : null;
1701
+ function cssBorder(element, edge) {
1702
+ const style = element.style.getPropertyValue(`border-${edge}-style`).trim();
1703
+ const width = Number.parseFloat(element.style.getPropertyValue(`border-${edge}-width`));
1704
+ const color = normalizeCssColor(element.style.getPropertyValue(`border-${edge}-color`));
1705
+ if (!style) return null;
1706
+ if ('none' === style || 'hidden' === style) return {
1707
+ color: '#000000',
1708
+ style: 'none',
1709
+ width: 0
1710
+ };
1711
+ return color && 'transparent' !== color && Number.isFinite(width) && width > 0 ? {
1712
+ color,
1713
+ style,
1714
+ width
1715
+ } : null;
1830
1716
  }
1831
- function normalizeDocumentProofingLanguages(source) {
1832
- if (!isRecord(source)) return null;
1833
- const keys = Object.keys(source);
1834
- if (!keys.length || keys.some((key)=>!PROOFING_LANGUAGE_KEYS.has(key))) return null;
1835
- const normalized = {};
1836
- for (const key of PROOFING_LANGUAGE_ORDER){
1837
- if (void 0 === source[key]) continue;
1838
- const language = normalizeDocumentLanguageTag(source[key]);
1839
- if (!language) return null;
1840
- normalized[key] = language;
1841
- }
1842
- return Object.keys(normalized).length ? normalized : null;
1717
+ function sameBorderPresentation(border, css) {
1718
+ const expected = documentBorderPresentation(border);
1719
+ if ('none' === expected.style) return 'none' === css.style && 0 === css.width;
1720
+ return expected.color === css.color && expected.style === css.style && Math.abs(expected.width - css.width) < 0.01;
1843
1721
  }
1844
- function serializeDocumentProofingLanguages(source) {
1845
- const normalized = normalizeDocumentProofingLanguages(source);
1846
- if (!normalized) return;
1847
- const serialized = JSON.stringify(normalized);
1848
- return serialized.length <= MAX_SERIALIZED_PROOFING_LANGUAGES_BYTES ? serialized : void 0;
1722
+ function cssStyleToBorderStyle(style) {
1723
+ if ('double' === style) return 'double';
1724
+ if ('dashed' === style) return 'dashed';
1725
+ if ('dotted' === style) return 'dotted';
1726
+ if ('inset' === style || 'groove' === style) return 'inset';
1727
+ if ('outset' === style || 'ridge' === style) return 'outset';
1728
+ return 'single';
1849
1729
  }
1850
- function parseDocumentProofingLanguages(source) {
1851
- if ('string' != typeof source) return normalizeDocumentProofingLanguages(source);
1852
- if (!source || source.length > MAX_SERIALIZED_PROOFING_LANGUAGES_BYTES) return null;
1730
+ function cssWidthToEighthPoints(width) {
1731
+ return Math.max(2, Math.min(96, Math.round(6 * width)));
1732
+ }
1733
+ function formatPixels(value) {
1734
+ return Number(value.toFixed(3)).toString();
1735
+ }
1736
+ const DOCUMENT_RUN_BORDER_ATTRIBUTE = 'data-office-run-border';
1737
+ const DOCUMENT_RUN_BORDER_STYLES = DOCUMENT_PARAGRAPH_BORDER_STYLES.slice(0, 27);
1738
+ const MAX_SERIALIZED_RUN_BORDER_BYTES = 4096;
1739
+ const work_document_run_border_POINTS_TO_PIXELS = 96 / 72;
1740
+ function normalizeDocumentRunBorder(source) {
1741
+ const border = normalizeDocumentParagraphBorder(source);
1742
+ return border && !isDocumentParagraphArtBorderStyle(border.style) ? border : null;
1743
+ }
1744
+ function parseDocumentRunBorder(source) {
1745
+ if ('string' != typeof source) return normalizeDocumentRunBorder(source);
1746
+ if (!source.trim() || source.length > MAX_SERIALIZED_RUN_BORDER_BYTES) return null;
1853
1747
  try {
1854
- const normalized = normalizeDocumentProofingLanguages(JSON.parse(source));
1855
- return normalized && JSON.stringify(normalized) === source ? normalized : null;
1748
+ return normalizeDocumentRunBorder(JSON.parse(source));
1856
1749
  } catch {
1857
1750
  return null;
1858
1751
  }
1859
1752
  }
1860
- function normalizeDocumentNoProof(source) {
1861
- if (true === source || 'true' === source || '1' === source) return true;
1862
- if (false === source || 'false' === source || '0' === source) return false;
1863
- return null;
1753
+ function serializeDocumentRunBorder(source) {
1754
+ const border = normalizeDocumentRunBorder(source);
1755
+ if (!border) return;
1756
+ const theme = serializeDocxThemeReference(border.color?.theme ?? null);
1757
+ return JSON.stringify({
1758
+ style: border.style,
1759
+ ...border.color ? {
1760
+ color: {
1761
+ value: border.color.value,
1762
+ ...theme ? {
1763
+ theme: JSON.parse(theme)
1764
+ } : {}
1765
+ }
1766
+ } : {},
1767
+ ...void 0 !== border.size ? {
1768
+ size: border.size
1769
+ } : {},
1770
+ ...void 0 !== border.space ? {
1771
+ space: border.space
1772
+ } : {},
1773
+ ...void 0 !== border.shadow ? {
1774
+ shadow: border.shadow
1775
+ } : {},
1776
+ ...void 0 !== border.frame ? {
1777
+ frame: border.frame
1778
+ } : {}
1779
+ });
1864
1780
  }
1865
- function documentProofingLanguagesFromElement(element) {
1866
- return parseDocumentProofingLanguages(element.getAttribute(DOCUMENT_PROOFING_LANGUAGES_ATTRIBUTE));
1781
+ function parseDocumentRunBorderElement(element) {
1782
+ const semantic = parseDocumentRunBorder(element.getAttribute(DOCUMENT_RUN_BORDER_ATTRIBUTE));
1783
+ return semantic ?? documentRunBorderFromCss(element);
1867
1784
  }
1868
- function documentNoProofFromElement(element) {
1869
- return normalizeDocumentNoProof(element.getAttribute(DOCUMENT_NO_PROOF_ATTRIBUTE));
1785
+ function documentRunBorderDomAttributes(source) {
1786
+ const border = normalizeDocumentRunBorder(source);
1787
+ const serialized = serializeDocumentRunBorder(border);
1788
+ if (!border || !serialized) return {};
1789
+ const presentation = documentBorderPresentation(border);
1790
+ const declarations = [
1791
+ `border: ${work_document_run_border_formatPixels(presentation.width)}px ${presentation.style} ${presentation.color}`,
1792
+ `padding: ${work_document_run_border_formatPixels((border.space ?? 0) * work_document_run_border_POINTS_TO_PIXELS)}px`,
1793
+ 'box-decoration-break: clone',
1794
+ '-webkit-box-decoration-break: clone'
1795
+ ];
1796
+ if (border.shadow && presentation.width > 0) declarations.push(`box-shadow: 2px 2px 0 ${presentation.color}`);
1797
+ return {
1798
+ [DOCUMENT_RUN_BORDER_ATTRIBUTE]: serialized,
1799
+ style: declarations.join('; ')
1800
+ };
1870
1801
  }
1871
- function documentProofingLanguageForScript(source, slot) {
1872
- const languages = normalizeDocumentProofingLanguages(source);
1873
- if (!languages) return;
1874
- if ('eastAsia' === slot) return languages.eastAsia ?? languages.latin;
1875
- if ('complexScript' === slot) return languages.bidi ?? languages.latin;
1876
- return languages.latin ?? languages.eastAsia ?? languages.bidi;
1802
+ function documentRunBorderIsVisible(source) {
1803
+ const border = normalizeDocumentRunBorder(source);
1804
+ return Boolean(border && documentBorderPresentation(border).width > 0);
1877
1805
  }
1878
- function documentProofingDomAttributes(languagesSource, noProofSource, slot) {
1879
- const languages = normalizeDocumentProofingLanguages(languagesSource);
1880
- const serialized = serializeDocumentProofingLanguages(languages);
1881
- const noProof = normalizeDocumentNoProof(noProofSource);
1882
- const language = documentProofingLanguageForScript(languages, slot);
1883
- return {
1884
- ...serialized ? {
1885
- [DOCUMENT_PROOFING_LANGUAGES_ATTRIBUTE]: serialized
1886
- } : {},
1887
- ...null === noProof ? {} : {
1888
- [DOCUMENT_NO_PROOF_ATTRIBUTE]: String(noProof)
1806
+ function documentRunBorderFromCss(element) {
1807
+ const style = element.style.borderStyle.trim();
1808
+ if (!style) return null;
1809
+ if ('none' === style || 'hidden' === style) return {
1810
+ style: 'none'
1811
+ };
1812
+ const width = Number.parseFloat(element.style.borderWidth);
1813
+ const color = normalizeCssColor(element.style.borderColor);
1814
+ if (!Number.isFinite(width) || width <= 0 || !color || 'transparent' === color) return null;
1815
+ const padding = Number.parseFloat(element.style.padding);
1816
+ return normalizeDocumentRunBorder({
1817
+ style: cssBorderStyle(style),
1818
+ color: {
1819
+ value: color
1889
1820
  },
1890
- ...slot ? {
1891
- [DOCUMENT_SCRIPT_FONT_SLOT_ATTRIBUTE]: slot
1892
- } : {},
1893
- ...language && 'x-none' !== language ? {
1894
- lang: language
1821
+ size: Math.max(2, Math.min(96, Math.round(6 * width))),
1822
+ ...Number.isFinite(padding) && padding >= 0 ? {
1823
+ space: Math.max(0, Math.min(31, Math.round(padding / work_document_run_border_POINTS_TO_PIXELS)))
1824
+ } : {}
1825
+ });
1826
+ }
1827
+ function cssBorderStyle(style) {
1828
+ if ('double' === style) return 'double';
1829
+ if ('dashed' === style) return 'dashed';
1830
+ if ('dotted' === style) return 'dotted';
1831
+ if ('inset' === style || 'groove' === style) return 'inset';
1832
+ if ('outset' === style || 'ridge' === style) return 'outset';
1833
+ return 'single';
1834
+ }
1835
+ function work_document_run_border_formatPixels(value) {
1836
+ return Number(value.toFixed(3)).toString();
1837
+ }
1838
+ const work_document_paragraph_shading_DOCUMENT_PARAGRAPH_SHADING_PATTERNS = new Set([
1839
+ 'nil',
1840
+ 'clear',
1841
+ 'solid',
1842
+ 'horzStripe',
1843
+ 'vertStripe',
1844
+ 'reverseDiagStripe',
1845
+ 'diagStripe',
1846
+ 'horzCross',
1847
+ 'diagCross',
1848
+ 'thinHorzStripe',
1849
+ 'thinVertStripe',
1850
+ 'thinReverseDiagStripe',
1851
+ 'thinDiagStripe',
1852
+ 'thinHorzCross',
1853
+ 'thinDiagCross',
1854
+ 'pct5',
1855
+ 'pct10',
1856
+ 'pct12',
1857
+ 'pct15',
1858
+ 'pct20',
1859
+ 'pct25',
1860
+ 'pct30',
1861
+ 'pct35',
1862
+ 'pct37',
1863
+ 'pct40',
1864
+ 'pct45',
1865
+ 'pct50',
1866
+ 'pct55',
1867
+ 'pct60',
1868
+ 'pct62',
1869
+ 'pct65',
1870
+ 'pct70',
1871
+ 'pct75',
1872
+ 'pct80',
1873
+ 'pct85',
1874
+ 'pct87',
1875
+ 'pct90',
1876
+ 'pct95'
1877
+ ]);
1878
+ function normalizeDocumentParagraphShading(source) {
1879
+ if (!source || 'object' != typeof source) return null;
1880
+ const value = source;
1881
+ const pattern = value.pattern;
1882
+ if ('string' != typeof pattern || !work_document_paragraph_shading_DOCUMENT_PARAGRAPH_SHADING_PATTERNS.has(pattern)) return null;
1883
+ const color = normalizeShadingColor(value.color);
1884
+ const fill = normalizeShadingColor(value.fill);
1885
+ if (void 0 !== value.color && !color || void 0 !== value.fill && !fill) return null;
1886
+ return {
1887
+ pattern: pattern,
1888
+ ...color ? {
1889
+ color
1895
1890
  } : {},
1896
- ...true === noProof ? {
1897
- spellcheck: 'false'
1891
+ ...fill ? {
1892
+ fill
1898
1893
  } : {}
1899
1894
  };
1900
1895
  }
1901
- function patchDocumentProofingLanguages(source, patch) {
1902
- const current = normalizeDocumentProofingLanguages(source) ?? {};
1903
- const next = {
1904
- ...current
1905
- };
1906
- for (const slot of PROOFING_LANGUAGE_ORDER){
1907
- const value = patch[slot];
1908
- if (void 0 === value) continue;
1909
- if (null === value) {
1910
- delete next[slot];
1911
- continue;
1896
+ function parseDocumentParagraphShading(source) {
1897
+ if ('string' != typeof source) return normalizeDocumentParagraphShading(source);
1898
+ if (!source.trim()) return null;
1899
+ try {
1900
+ return normalizeDocumentParagraphShading(JSON.parse(source));
1901
+ } catch {
1902
+ return null;
1903
+ }
1904
+ }
1905
+ function serializeDocumentParagraphShading(source) {
1906
+ const shading = normalizeDocumentParagraphShading(source);
1907
+ if (!shading) return;
1908
+ return JSON.stringify({
1909
+ pattern: shading.pattern,
1910
+ ...shading.color ? {
1911
+ color: serializedShadingColor(shading.color)
1912
+ } : {},
1913
+ ...shading.fill ? {
1914
+ fill: serializedShadingColor(shading.fill)
1915
+ } : {}
1916
+ });
1917
+ }
1918
+ function parseDocumentParagraphShadingElement(element) {
1919
+ const semantic = parseDocumentParagraphShading(element.dataset.officeParagraphShading);
1920
+ const background = normalizeCssColor(element.style.backgroundColor);
1921
+ if (semantic) {
1922
+ const presentation = documentParagraphShadingPresentation(semantic);
1923
+ const expected = normalizeCssColor(presentation.backgroundColor);
1924
+ if (background && expected && background !== expected) {
1925
+ if ('transparent' === background) return {
1926
+ pattern: 'nil'
1927
+ };
1928
+ if (paragraphShadingBackgroundUsesForeground(semantic)) return {
1929
+ ...semantic,
1930
+ color: {
1931
+ value: background
1932
+ }
1933
+ };
1934
+ return {
1935
+ ...semantic,
1936
+ fill: {
1937
+ value: background
1938
+ }
1939
+ };
1912
1940
  }
1913
- const language = normalizeDocumentLanguageTag(value);
1914
- if (!language) return null;
1915
- next[slot] = language;
1941
+ return semantic;
1916
1942
  }
1917
- return Object.keys(next).length ? next : null;
1918
- }
1919
- function isRecord(source) {
1920
- return 'object' == typeof source && null !== source && !Array.isArray(source);
1943
+ return background && 'transparent' !== background ? {
1944
+ pattern: 'clear',
1945
+ fill: {
1946
+ value: background
1947
+ }
1948
+ } : null;
1921
1949
  }
1922
- const DEFAULT_DOCUMENT_INDEX_OPTIONS = {
1923
- columns: 1,
1924
- format: 'indented',
1925
- rightAlignPageNumbers: true,
1926
- leader: 'dot'
1927
- };
1928
- const MAX_DOCUMENT_INDEX_ENTRIES = 512;
1929
- const MAX_DOCUMENT_INDEX_MARKERS = 2048;
1930
- const MAX_DOCUMENT_INDEX_TERM_LENGTH = 240;
1931
- const INDEX_ENTRY_SELECTOR = '[data-document-index-entry]';
1932
- const INDEX_SELECTOR = '[data-document-index]';
1933
- const INDEX_ID_PATTERN = /^[a-z0-9][a-z0-9._:-]{0,127}$/i;
1934
- function normalizeDocumentIndexEntryDraft(source) {
1935
- const mainEntry = normalizedIndexTerm(source?.mainEntry);
1936
- if (!mainEntry) return null;
1937
- const subEntry = normalizedIndexTerm(source?.subEntry);
1938
- const crossReference = normalizedIndexTerm(source?.crossReference);
1950
+ function documentParagraphShadingDomAttributes(source) {
1951
+ const shading = normalizeDocumentParagraphShading(source);
1952
+ const serialized = serializeDocumentParagraphShading(shading);
1953
+ if (!shading || !serialized) return {};
1954
+ const presentation = documentParagraphShadingPresentation(shading);
1955
+ const styles = [
1956
+ `background-color: ${presentation.backgroundColor}`,
1957
+ presentation.backgroundImage ? `background-image: ${presentation.backgroundImage}` : '',
1958
+ presentation.backgroundSize ? `background-size: ${presentation.backgroundSize}` : ''
1959
+ ].filter(Boolean);
1939
1960
  return {
1940
- mainEntry,
1941
- subEntry,
1942
- crossReference,
1943
- pageBold: !crossReference && Boolean(source?.pageBold),
1944
- pageItalic: !crossReference && Boolean(source?.pageItalic)
1961
+ 'data-office-paragraph-shading': serialized,
1962
+ style: styles.join('; ')
1945
1963
  };
1946
1964
  }
1947
- function normalizeDocumentIndexEntry(source, fallbackId = 'index-entry') {
1948
- const value = normalizeDocumentIndexEntryDraft(source);
1949
- if (!value) return null;
1965
+ function normalizeShadingColor(source) {
1966
+ if (!source || 'object' != typeof source) return null;
1967
+ const value = source;
1968
+ const theme = parseDocxThemeReference('string' == typeof value.theme ? value.theme : value.theme ? JSON.stringify(value.theme) : void 0);
1969
+ const direct = 'auto' === value.value ? 'auto' : 'string' == typeof value.value ? normalizeCssColor(value.value) : null;
1970
+ const resolved = direct ?? theme?.resolved ?? null;
1971
+ if (!resolved || 'transparent' === resolved) return null;
1972
+ if (theme && resolved !== theme.resolved) return null;
1950
1973
  return {
1951
- id: validIndexId(source?.id) ?? fallbackId,
1952
- ...value
1974
+ value: resolved,
1975
+ ...theme ? {
1976
+ theme
1977
+ } : {}
1953
1978
  };
1954
1979
  }
1955
- function normalizeDocumentIndexOptions(source) {
1980
+ function serializedShadingColor(color) {
1981
+ const theme = serializeDocxThemeReference(color.theme ?? null);
1956
1982
  return {
1957
- columns: boundedColumns(source?.columns),
1958
- format: source?.format === 'run-in' ? 'run-in' : 'indented',
1959
- rightAlignPageNumbers: source?.rightAlignPageNumbers !== false,
1960
- leader: indexLeader(source?.leader) ?? 'dot'
1983
+ value: color.value,
1984
+ ...theme ? {
1985
+ theme: JSON.parse(theme)
1986
+ } : {}
1961
1987
  };
1962
1988
  }
1963
- function normalizeDocumentIndexValue(source) {
1964
- return {
1965
- id: validIndexId(source.id) ?? 'document-index',
1966
- options: normalizeDocumentIndexOptions(source.options),
1967
- entries: normalizeGeneratedEntries(source.entries),
1968
- truncated: Boolean(source.truncated)
1989
+ function documentParagraphShadingPresentation(shading) {
1990
+ if ('nil' === shading.pattern) return {
1991
+ backgroundColor: 'transparent'
1969
1992
  };
1970
- }
1971
- function buildDocumentIndexEntries(document, options = {}) {
1972
- const grouped = new Map();
1973
- let markerCount = 0;
1974
- document.descendants((node, position)=>{
1975
- if ('documentIndexEntry' !== node.type.name) return;
1976
- markerCount += 1;
1977
- if (markerCount > MAX_DOCUMENT_INDEX_MARKERS) return;
1978
- const marker = normalizeDocumentIndexEntry(node.attrs, `index-entry-${markerCount}`);
1979
- if (!marker) return;
1980
- const key = indexEntryKey(marker);
1981
- let group = grouped.get(key);
1982
- if (!group) {
1983
- group = {
1984
- entry: {
1985
- mainEntry: marker.mainEntry,
1986
- subEntry: marker.subEntry,
1987
- crossReference: marker.crossReference,
1988
- pages: []
1989
- },
1990
- pages: new Map()
1991
- };
1992
- grouped.set(key, group);
1993
- }
1994
- if (marker.crossReference) return;
1995
- const pageNumber = positiveInteger(options.resolveContext?.(position)?.pageNumber) ?? fallbackDocumentPageNumber(document, position);
1996
- const existing = group.pages.get(pageNumber);
1997
- if (existing) {
1998
- existing.pageBold ||= marker.pageBold;
1999
- existing.pageItalic ||= marker.pageItalic;
2000
- if (!existing.targetIds.includes(marker.id)) existing.targetIds.push(marker.id);
2001
- return;
2002
- }
2003
- group.pages.set(pageNumber, {
2004
- pageNumber,
2005
- pageBold: marker.pageBold,
2006
- pageItalic: marker.pageItalic,
2007
- targetIds: [
2008
- marker.id
2009
- ]
2010
- });
2011
- });
2012
- const collator = new Intl.Collator(void 0, {
2013
- numeric: true,
2014
- sensitivity: 'base',
2015
- usage: 'sort'
2016
- });
2017
- const allEntries = Array.from(grouped.values()).map(({ entry, pages })=>({
2018
- ...entry,
2019
- pages: Array.from(pages.values()).sort((left, right)=>left.pageNumber - right.pageNumber)
2020
- })).sort((left, right)=>collator.compare(left.mainEntry, right.mainEntry) || collator.compare(left.subEntry, right.subEntry) || collator.compare(left.crossReference, right.crossReference));
1993
+ const foreground = shadingColor(shading.color, '#000000');
1994
+ const background = shadingColor(shading.fill, 'transparent');
1995
+ if ('clear' === shading.pattern) return {
1996
+ backgroundColor: background
1997
+ };
1998
+ if ('solid' === shading.pattern) return {
1999
+ backgroundColor: foreground
2000
+ };
2001
+ const percentage = shadingPercentage(shading.pattern);
2002
+ if (null !== percentage) {
2003
+ const inverted = percentage > 50;
2004
+ const dotColor = inverted ? background : foreground;
2005
+ const baseColor = inverted ? foreground : background;
2006
+ const density = Math.min(50, inverted ? 100 - percentage : percentage);
2007
+ const spacing = Math.max(2, Math.round(9 - density / 7));
2008
+ return {
2009
+ backgroundColor: baseColor,
2010
+ backgroundImage: `radial-gradient(circle, ${dotColor} 0 1px, transparent 1.2px)`,
2011
+ backgroundSize: `${spacing}px ${spacing}px`
2012
+ };
2013
+ }
2014
+ const thin = shading.pattern.startsWith('thin');
2015
+ const width = thin ? 1 : 2;
2016
+ const period = thin ? 7 : 6;
2017
+ const stripe = (angle)=>`repeating-linear-gradient(${angle}deg, ${foreground} 0 ${width}px, transparent ${width}px ${period}px)`;
2018
+ const angles = shadingPatternAngles(shading.pattern);
2021
2019
  return {
2022
- entries: allEntries.slice(0, MAX_DOCUMENT_INDEX_ENTRIES),
2023
- truncated: markerCount > MAX_DOCUMENT_INDEX_MARKERS || allEntries.length > MAX_DOCUMENT_INDEX_ENTRIES
2020
+ backgroundColor: background,
2021
+ backgroundImage: angles.map(stripe).join(', ')
2024
2022
  };
2025
2023
  }
2026
- function documentIndexEntryHtml(source) {
2027
- const value = normalizeDocumentIndexEntry(source);
2028
- if (!value) return '';
2029
- const detail = indexEntryDisplay(value);
2030
- return [
2031
- `<span data-document-index-entry="true" data-index-entry-id="${escapeHtmlAttribute(value.id)}" data-index-main-entry="${escapeHtmlAttribute(value.mainEntry)}" data-index-sub-entry="${escapeHtmlAttribute(value.subEntry)}" data-index-cross-reference="${escapeHtmlAttribute(value.crossReference)}" data-index-page-bold="${String(value.pageBold)}" data-index-page-italic="${String(value.pageItalic)}" class="work-document-index-entry" contenteditable="false" aria-label="索引项:${escapeHtmlAttribute(detail)}">`,
2032
- '<span aria-hidden="true">索引项</span>',
2033
- `<strong>${escapeHtml(detail)}</strong>`,
2034
- '</span>'
2035
- ].join('');
2024
+ function shadingColor(color, fallback) {
2025
+ return color && 'auto' !== color.value ? color.value : fallback;
2036
2026
  }
2037
- function documentIndexHtml(source) {
2038
- const value = normalizeDocumentIndexValue(source);
2039
- const rows = value.entries.length ? value.entries.map(documentIndexRowHtml) : [
2040
- '<li class="work-document-index-empty">没有已标记的索引项</li>'
2027
+ function shadingPercentage(pattern) {
2028
+ const match = /^pct(\d+)$/.exec(pattern);
2029
+ return match?.[1] ? Number(match[1]) : null;
2030
+ }
2031
+ function paragraphShadingBackgroundUsesForeground(shading) {
2032
+ if ('solid' === shading.pattern) return true;
2033
+ const percentage = shadingPercentage(shading.pattern);
2034
+ return null !== percentage && percentage > 50;
2035
+ }
2036
+ function shadingPatternAngles(pattern) {
2037
+ if (pattern.includes('HorzCross')) return [
2038
+ 0,
2039
+ 90
2040
+ ];
2041
+ if (pattern.includes('DiagCross')) return [
2042
+ 45,
2043
+ -45
2044
+ ];
2045
+ if (pattern.includes('VertStripe')) return [
2046
+ 90
2047
+ ];
2048
+ if (pattern.includes('ReverseDiagStripe')) return [
2049
+ -45
2050
+ ];
2051
+ if (pattern.includes('DiagStripe')) return [
2052
+ 45
2041
2053
  ];
2042
- const status = value.truncated ? `仅显示前 ${MAX_DOCUMENT_INDEX_ENTRIES} 项` : `${value.entries.length} 项`;
2043
2054
  return [
2044
- `<div data-document-index="true" data-index-id="${escapeHtmlAttribute(value.id)}" data-index-columns="${value.options.columns}" data-index-format="${value.options.format}" data-index-right-align-page-numbers="${String(value.options.rightAlignPageNumbers)}" data-index-leader="${value.options.leader}" data-index-entries="${escapeHtmlAttribute(JSON.stringify(value.entries))}" data-index-truncated="${String(value.truncated)}" class="work-document-index" contenteditable="false" aria-label="索引">`,
2045
- `<div class="work-document-index-header"><strong>索引</strong><span>${status}</span></div>`,
2046
- '<ol class="work-document-index-list">',
2047
- ...rows,
2048
- '</ol></div>'
2049
- ].join('');
2055
+ 0
2056
+ ];
2050
2057
  }
2051
- function documentIndexEntryFromElement(element, index = 1) {
2052
- return normalizeDocumentIndexEntry({
2053
- id: element.dataset.indexEntryId,
2054
- mainEntry: element.dataset.indexMainEntry,
2055
- subEntry: element.dataset.indexSubEntry,
2056
- crossReference: element.dataset.indexCrossReference,
2057
- pageBold: 'true' === element.dataset.indexPageBold,
2058
- pageItalic: 'true' === element.dataset.indexPageItalic
2059
- }, `index-entry-${index}`);
2058
+ const DOCUMENT_HIGHLIGHT_ATTRIBUTE = 'data-office-highlight';
2059
+ const DOCUMENT_HIGHLIGHT_VALUES = new Set([
2060
+ 'black',
2061
+ 'blue',
2062
+ 'cyan',
2063
+ 'darkBlue',
2064
+ 'darkCyan',
2065
+ 'darkGray',
2066
+ 'darkGreen',
2067
+ 'darkMagenta',
2068
+ 'darkRed',
2069
+ 'darkYellow',
2070
+ 'green',
2071
+ 'lightGray',
2072
+ 'magenta',
2073
+ 'none',
2074
+ 'red',
2075
+ 'white',
2076
+ 'yellow'
2077
+ ]);
2078
+ const HIGHLIGHT_COLORS = {
2079
+ black: '#000000',
2080
+ blue: '#0000ff',
2081
+ cyan: '#00ffff',
2082
+ darkBlue: '#000080',
2083
+ darkCyan: '#008080',
2084
+ darkGray: '#808080',
2085
+ darkGreen: '#008000',
2086
+ darkMagenta: '#800080',
2087
+ darkRed: '#800000',
2088
+ darkYellow: '#808000',
2089
+ green: '#00ff00',
2090
+ lightGray: '#c0c0c0',
2091
+ magenta: '#ff00ff',
2092
+ none: 'transparent',
2093
+ red: '#ff0000',
2094
+ white: '#ffffff',
2095
+ yellow: '#ffff00'
2096
+ };
2097
+ function normalizeDocumentHighlight(source) {
2098
+ return 'string' == typeof source && DOCUMENT_HIGHLIGHT_VALUES.has(source) ? source : null;
2060
2099
  }
2061
- function documentIndexValueFromElement(element, index = 1) {
2062
- return normalizeDocumentIndexValue({
2063
- id: validIndexId(element.dataset.indexId) ?? `document-index-${index}`,
2064
- options: {
2065
- columns: Number(element.dataset.indexColumns),
2066
- format: 'run-in' === element.dataset.indexFormat ? 'run-in' : 'indented',
2067
- rightAlignPageNumbers: 'false' !== element.dataset.indexRightAlignPageNumbers,
2068
- leader: indexLeader(element.dataset.indexLeader) ?? 'dot'
2069
- },
2070
- entries: parseGeneratedEntries(element.dataset.indexEntries),
2071
- truncated: 'true' === element.dataset.indexTruncated
2072
- });
2100
+ function documentHighlightFromDocxValue(source) {
2101
+ if ('string' != typeof source) return null;
2102
+ const normalized = source.trim().toLowerCase();
2103
+ for (const value of DOCUMENT_HIGHLIGHT_VALUES)if (value.toLowerCase() === normalized) return value;
2104
+ return null;
2073
2105
  }
2074
- function normalizeDocumentIndexesHtml(source) {
2075
- const document = new DOMParser().parseFromString(source, 'text/html');
2076
- const usedEntryIds = new Set();
2077
- for (const [index, element] of Array.from(document.body.querySelectorAll(INDEX_ENTRY_SELECTOR)).entries()){
2078
- const value = documentIndexEntryFromElement(element, index + 1);
2079
- if (!value) {
2080
- element.remove();
2081
- continue;
2082
- }
2083
- value.id = uniqueIndexId(value.id, 'index-entry', index + 1, usedEntryIds);
2084
- element.replaceWith(document.createRange().createContextualFragment(documentIndexEntryHtml(value)));
2085
- }
2086
- const usedIndexIds = new Set();
2087
- for (const [index, element] of Array.from(document.body.querySelectorAll(INDEX_SELECTOR)).entries()){
2088
- const value = documentIndexValueFromElement(element, index + 1);
2089
- value.id = uniqueIndexId(value.id, 'document-index', index + 1, usedIndexIds);
2090
- element.replaceWith(document.createRange().createContextualFragment(documentIndexHtml(value)));
2091
- }
2092
- return document.body.innerHTML;
2106
+ function documentHighlightCssColor(source) {
2107
+ const value = normalizeDocumentHighlight(source);
2108
+ return value ? HIGHLIGHT_COLORS[value] : null;
2093
2109
  }
2094
- function indexLeader(value) {
2095
- return 'dot' === value || 'dash' === value || 'underline' === value || 'none' === value ? value : null;
2110
+ function documentHighlightForCssColor(source) {
2111
+ const color = normalizeCssColor('string' == typeof source ? source : null);
2112
+ if (!color) return null;
2113
+ for (const [value, candidate] of Object.entries(HIGHLIGHT_COLORS))if (candidate === color) return value;
2114
+ return null;
2096
2115
  }
2097
- function documentIndexRowHtml(entry) {
2098
- const term = entry.subEntry ? `<span class="work-document-index-main">${escapeHtml(entry.mainEntry)}</span><span class="work-document-index-sub">${escapeHtml(entry.subEntry)}</span>` : `<span class="work-document-index-main">${escapeHtml(entry.mainEntry)}</span>`;
2099
- const pages = entry.crossReference ? `<span class="work-document-index-cross-reference">参见 ${escapeHtml(entry.crossReference)}</span>` : entry.pages.map((page)=>{
2100
- const target = page.targetIds[0] ?? '';
2101
- const classes = [
2102
- page.pageBold ? 'bold' : '',
2103
- page.pageItalic ? 'italic' : ''
2104
- ].filter(Boolean).join(' ');
2105
- return `<a href="#${escapeHtmlAttribute(target)}" data-index-target="${escapeHtmlAttribute(target)}" tabindex="-1"${classes ? ` class="${classes}"` : ''}>${page.pageNumber}</a>`;
2106
- }).join('<span aria-hidden="true">, </span>');
2107
- return `<li data-index-main-entry="${escapeHtmlAttribute(entry.mainEntry)}" data-index-sub-entry="${escapeHtmlAttribute(entry.subEntry)}">${term}<i aria-hidden="true"></i><span class="work-document-index-pages">${pages}</span></li>`;
2116
+ function documentHighlightFromElement(element) {
2117
+ return normalizeDocumentHighlight(element.getAttribute(DOCUMENT_HIGHLIGHT_ATTRIBUTE)) ?? documentHighlightForCssColor(element.style.backgroundColor);
2108
2118
  }
2109
- function normalizeGeneratedEntries(source) {
2110
- const entries = [];
2111
- for (const item of source.slice(0, MAX_DOCUMENT_INDEX_ENTRIES)){
2112
- if (!item || 'object' != typeof item) continue;
2113
- const candidate = item;
2114
- const draft = normalizeDocumentIndexEntryDraft({
2115
- mainEntry: normalizedIndexTerm(candidate.mainEntry),
2116
- subEntry: normalizedIndexTerm(candidate.subEntry),
2117
- crossReference: normalizedIndexTerm(candidate.crossReference)
2118
- });
2119
- if (draft) entries.push({
2120
- mainEntry: draft.mainEntry,
2121
- subEntry: draft.subEntry,
2122
- crossReference: draft.crossReference,
2123
- pages: draft.crossReference ? [] : normalizeIndexPages(candidate.pages)
2124
- });
2125
- }
2126
- return entries;
2119
+ function documentHighlightDomAttributes(source) {
2120
+ const value = normalizeDocumentHighlight(source);
2121
+ const color = documentHighlightCssColor(value);
2122
+ return value && color ? {
2123
+ [DOCUMENT_HIGHLIGHT_ATTRIBUTE]: value,
2124
+ style: `background-color: ${color}`
2125
+ } : {};
2127
2126
  }
2128
- function normalizeIndexPages(source) {
2129
- if (!Array.isArray(source)) return [];
2130
- const pages = [];
2131
- const seen = new Set();
2132
- for (const item of source.slice(0, MAX_DOCUMENT_INDEX_MARKERS)){
2133
- if (!item || 'object' != typeof item) continue;
2134
- const candidate = item;
2135
- const pageNumber = positiveInteger(candidate.pageNumber);
2136
- if (!pageNumber || seen.has(pageNumber)) continue;
2137
- const targetIds = Array.isArray(candidate.targetIds) ? candidate.targetIds.flatMap((value)=>validIndexId(value) ? [
2138
- String(value)
2139
- ] : []).slice(0, 64) : [];
2140
- if (targetIds.length) {
2141
- seen.add(pageNumber);
2142
- pages.push({
2143
- pageNumber,
2144
- pageBold: Boolean(candidate.pageBold),
2145
- pageItalic: Boolean(candidate.pageItalic),
2146
- targetIds
2147
- });
2127
+ const DOCUMENT_RUN_SHADING_ATTRIBUTE = 'data-office-run-shading';
2128
+ const MAX_SERIALIZED_RUN_SHADING_BYTES = 4096;
2129
+ const RUN_SHADING_KEYS = new Set([
2130
+ 'pattern',
2131
+ 'color',
2132
+ 'fill'
2133
+ ]);
2134
+ const RUN_SHADING_COLOR_KEYS = new Set([
2135
+ 'value',
2136
+ 'theme'
2137
+ ]);
2138
+ const THEME_REFERENCE_KEYS = new Set([
2139
+ 'theme',
2140
+ 'resolved',
2141
+ 'tint',
2142
+ 'shade'
2143
+ ]);
2144
+ function normalizeDocumentRunShading(source) {
2145
+ if (!work_document_run_shading_isRecordWithKeys(source, RUN_SHADING_KEYS)) return null;
2146
+ for (const name of [
2147
+ 'color',
2148
+ 'fill'
2149
+ ]){
2150
+ const color = source[name];
2151
+ if (void 0 !== color) {
2152
+ if (!work_document_run_shading_isRecordWithKeys(color, RUN_SHADING_COLOR_KEYS)) return null;
2153
+ if (void 0 !== color.theme && !work_document_run_shading_isRecordWithKeys(color.theme, THEME_REFERENCE_KEYS)) return null;
2148
2154
  }
2149
2155
  }
2150
- return pages.sort((left, right)=>left.pageNumber - right.pageNumber);
2156
+ return normalizeDocumentParagraphShading(source);
2151
2157
  }
2152
- function parseGeneratedEntries(source) {
2153
- if (!source) return [];
2158
+ function parseDocumentRunShading(source) {
2159
+ if ('string' != typeof source) return normalizeDocumentRunShading(source);
2160
+ if (!source.trim() || source.length > MAX_SERIALIZED_RUN_SHADING_BYTES) return null;
2154
2161
  try {
2155
- const parsed = JSON.parse(source);
2156
- return Array.isArray(parsed) ? normalizeGeneratedEntries(parsed) : [];
2162
+ return normalizeDocumentRunShading(JSON.parse(source));
2157
2163
  } catch {
2158
- return [];
2164
+ return null;
2159
2165
  }
2160
2166
  }
2161
- function fallbackDocumentPageNumber(document, position) {
2162
- let pageNumber = 1;
2163
- document.descendants((node, offset)=>{
2164
- if (offset >= position) return false;
2165
- if ('pageBreak' === node.type.name) pageNumber += 1;
2166
- return true;
2167
- });
2168
- return pageNumber;
2169
- }
2170
- function indexEntryKey(entry) {
2171
- return [
2172
- entry.mainEntry,
2173
- entry.subEntry,
2174
- entry.crossReference
2175
- ].map((value)=>value.normalize('NFKC').toLocaleLowerCase()).join('\u0000');
2176
- }
2177
- function indexEntryDisplay(entry) {
2178
- const term = entry.subEntry ? `${entry.mainEntry} › ${entry.subEntry}` : entry.mainEntry;
2179
- return entry.crossReference ? `${term} · 参见 ${entry.crossReference}` : term;
2180
- }
2181
- function normalizedIndexTerm(value) {
2182
- return 'string' == typeof value ? value.replace(/\s+/g, ' ').trim().slice(0, MAX_DOCUMENT_INDEX_TERM_LENGTH) : '';
2183
- }
2184
- function boundedColumns(value) {
2185
- const number = Number(value);
2186
- return Number.isInteger(number) && number >= 1 && number <= 4 ? number : 1;
2187
- }
2188
- function positiveInteger(value) {
2189
- const number = Number(value);
2190
- return Number.isSafeInteger(number) && number > 0 ? Math.min(999999, number) : null;
2191
- }
2192
- function validIndexId(value) {
2193
- return 'string' == typeof value && INDEX_ID_PATTERN.test(value) ? value : null;
2167
+ function serializeDocumentRunShading(source) {
2168
+ const shading = normalizeDocumentRunShading(source);
2169
+ const serialized = shading ? serializeDocumentParagraphShading(shading) : void 0;
2170
+ return serialized && serialized.length <= MAX_SERIALIZED_RUN_SHADING_BYTES ? serialized : void 0;
2194
2171
  }
2195
- function uniqueIndexId(source, prefix, index, used) {
2196
- if (!used.has(source)) {
2197
- used.add(source);
2198
- return source;
2199
- }
2200
- let suffix = index;
2201
- while(used.has(`${prefix}-${suffix}`))suffix += 1;
2202
- const id = `${prefix}-${suffix}`;
2203
- used.add(id);
2204
- return id;
2172
+ function parseDocumentRunShadingElement(element) {
2173
+ const semantic = parseDocumentRunShading(element.getAttribute(DOCUMENT_RUN_SHADING_ATTRIBUTE));
2174
+ if (!semantic) return null;
2175
+ if (element.hasAttribute(DOCUMENT_HIGHLIGHT_ATTRIBUTE)) return semantic;
2176
+ const background = normalizeCssColor(element.style.backgroundColor);
2177
+ const expected = normalizeCssColor(documentParagraphShadingPresentation(semantic).backgroundColor);
2178
+ if (!background || !expected || background === expected) return semantic;
2179
+ if ('transparent' === background) return {
2180
+ pattern: 'nil'
2181
+ };
2182
+ return paragraphShadingBackgroundUsesForeground(semantic) ? {
2183
+ ...semantic,
2184
+ color: {
2185
+ value: background
2186
+ }
2187
+ } : {
2188
+ ...semantic,
2189
+ fill: {
2190
+ value: background
2191
+ }
2192
+ };
2205
2193
  }
2206
- function escapeHtml(value) {
2207
- return value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
2194
+ function documentRunShadingDomAttributes(source) {
2195
+ const shading = normalizeDocumentRunShading(source);
2196
+ const serialized = serializeDocumentRunShading(shading);
2197
+ if (!shading || !serialized) return {};
2198
+ const paragraphAttributes = documentParagraphShadingDomAttributes(shading);
2199
+ const style = [
2200
+ paragraphAttributes.style,
2201
+ 'box-decoration-break: clone',
2202
+ '-webkit-box-decoration-break: clone'
2203
+ ].filter(Boolean).join('; ');
2204
+ return {
2205
+ [DOCUMENT_RUN_SHADING_ATTRIBUTE]: serialized,
2206
+ style
2207
+ };
2208
2208
  }
2209
- function escapeHtmlAttribute(value) {
2210
- return escapeHtml(value).replaceAll('"', '&quot;').replaceAll("'", '&#39;');
2209
+ function work_document_run_shading_isRecordWithKeys(source, allowed) {
2210
+ return 'object' == typeof source && null !== source && !Array.isArray(source) && Object.keys(source).every((key)=>allowed.has(key));
2211
2211
  }
2212
2212
  const DOCUMENT_PARAGRAPH_ID_ATTRIBUTE = 'data-office-paragraph-id';
2213
2213
  const DOCUMENT_PARAGRAPH_TEXT_ID_ATTRIBUTE = 'data-office-paragraph-text-id';
@@ -2909,7 +2909,7 @@ const WORK_TEMPLATES = [
2909
2909
  id: 'structured-references',
2910
2910
  kind: 'spreadsheet',
2911
2911
  name: '结构化引用',
2912
- description: '表名、当前行公式与插入行自动填充',
2912
+ description: '表名、当前行公式、插入行自动填充与汇总行',
2913
2913
  accent: '#0f7b61'
2914
2914
  },
2915
2915
  {
@@ -3685,7 +3685,7 @@ function structuredReferenceTemplateSheets() {
3685
3685
  fc: '#215446',
3686
3686
  bg: '#dff3ec'
3687
3687
  });
3688
- sales[7][1] = styledCell('=SUM(Sales[Units])', {
3688
+ sales[7][1] = styledCell('=SUBTOTAL(109,Sales[Units])', {
3689
3689
  bl: 1,
3690
3690
  fc: '#215446',
3691
3691
  bg: '#dff3ec',
@@ -3694,7 +3694,7 @@ function structuredReferenceTemplateSheets() {
3694
3694
  t: 'n'
3695
3695
  }
3696
3696
  });
3697
- sales[7][3] = styledCell('=SUM(Sales[Revenue])', {
3697
+ sales[7][3] = styledCell('=SUBTOTAL(109,Sales[Revenue])', {
3698
3698
  bl: 1,
3699
3699
  fc: '#215446',
3700
3700
  bg: '#dff3ec',
@@ -3751,17 +3751,20 @@ function structuredReferenceTemplateSheets() {
3751
3751
  },
3752
3752
  columns: [
3753
3753
  {
3754
- name: 'Item'
3754
+ name: 'Item',
3755
+ totalsLabel: 'Total'
3755
3756
  },
3756
3757
  {
3757
- name: 'Units'
3758
+ name: 'Units',
3759
+ totalsFunction: 'sum'
3758
3760
  },
3759
3761
  {
3760
3762
  name: 'Unit price'
3761
3763
  },
3762
3764
  {
3763
3765
  name: 'Revenue',
3764
- calculatedFormula: '=[@Units]*[@[Unit price]]'
3766
+ calculatedFormula: '=[@Units]*[@[Unit price]]',
3767
+ totalsFunction: 'sum'
3765
3768
  }
3766
3769
  ],
3767
3770
  filters: [],