@a3s-lab/office 0.35.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;
1490
- }
1491
- function serializeDocumentScriptFonts(source) {
1492
- const fonts = normalizeDocumentScriptFonts(source);
1493
- return fonts ? JSON.stringify(fonts) : null;
1494
- }
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;
1501
- }
1502
- }
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));
1516
- return {
1517
- [DOCUMENT_SCRIPT_FONTS_ATTRIBUTE]: serialized,
1518
- ...normalizedSlot ? {
1519
- [DOCUMENT_SCRIPT_FONT_SLOT_ATTRIBUTE]: normalizedSlot
1520
- } : {},
1521
- ...family ? {
1522
- style: `font-family: ${family}`
1523
- } : {}
1524
- };
1525
- }
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
- }
1538
- }
1539
- return families.length ? families.join(', ') : void 0;
1540
- }
1541
- function documentScriptFontFallbackSlots(slot) {
1542
- return SLOT_FALLBACK_ORDER[slot];
1203
+ return colors;
1543
1204
  }
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;
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;
1551
1209
  }
1552
- function documentScriptFontDirectFamily(source, slot) {
1553
- const fonts = normalizeDocumentScriptFonts(source);
1554
- return fonts ? documentScriptFontFaceFamily(fonts[slot]) ?? null : null;
1210
+ function wordAttribute(element, name) {
1211
+ return element.getAttributeNS(WORD_NAMESPACE, name) ?? element.getAttribute(`w:${name}`);
1555
1212
  }
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
- };
1213
+ function setWordAttribute(document, element, name, value) {
1214
+ const prefix = xmlNamespacePrefix(document.documentElement, WORD_NAMESPACE) ?? 'w';
1215
+ element.setAttributeNS(WORD_NAMESPACE, `${prefix}:${name}`, value);
1570
1216
  }
1571
- function patchDocumentScriptFonts(source, patch, fallbackFontFamily) {
1572
- const current = normalizeDocumentScriptFonts(source) ?? documentScriptFontsForAllText(fallbackFontFamily) ?? {};
1573
- const next = {
1574
- ...current
1575
- };
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);
1217
+ function setOptionalWordAttribute(document, element, name, value) {
1218
+ if (value) setWordAttribute(document, element, name, value);
1219
+ else element.removeAttributeNS(WORD_NAMESPACE, name);
1597
1220
  }
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
- });
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;
1647
1473
  }
1648
- return segments;
1649
- }
1650
- function normalizeDocumentScriptFontSlot(value) {
1651
- return scriptFontSlots.includes(value) ? value : null;
1474
+ return Object.keys(borders).length ? borders : null;
1652
1475
  }
1653
- function normalizeDocumentScriptFontHint(value) {
1654
- return SCRIPT_FONT_HINTS.has(value) ? value : null;
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;
1492
+ return {
1493
+ style: normalizedStyle,
1494
+ ...color ? {
1495
+ color
1496
+ } : {},
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
1508
+ } : {}
1509
+ };
1655
1510
  }
1656
- function normalizeDocumentThemeFont(value) {
1657
- return THEME_FONTS.has(value) ? value : null;
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;
1518
+ }
1658
1519
  }
1659
- function documentScriptFontSlotFromHint(hint) {
1660
- if ('eastAsia' === hint) return 'eastAsia';
1661
- if ('cs' === hint) return 'complexScript';
1662
- return 'ascii';
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
+ })));
1663
1532
  }
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;
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
1538
+ };
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
1699
+ return normalizeDocumentParagraphBorders(borders);
1700
+ }
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
1807
1710
  };
1711
+ return color && 'transparent' !== color && Number.isFinite(width) && width > 0 ? {
1712
+ color,
1713
+ style,
1714
+ width
1715
+ } : null;
1808
1716
  }
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));
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;
1811
1721
  }
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;
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';
1830
1729
  }
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;
1730
+ function cssWidthToEighthPoints(width) {
1731
+ return Math.max(2, Math.min(96, Math.round(6 * width)));
1843
1732
  }
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;
1733
+ function formatPixels(value) {
1734
+ return Number(value.toFixed(3)).toString();
1849
1735
  }
1850
- function parseDocumentProofingLanguages(source) {
1851
- if ('string' != typeof source) return normalizeDocumentProofingLanguages(source);
1852
- if (!source || source.length > MAX_SERIALIZED_PROOFING_LANGUAGES_BYTES) return null;
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;
1864
- }
1865
- function documentProofingLanguagesFromElement(element) {
1866
- return parseDocumentProofingLanguages(element.getAttribute(DOCUMENT_PROOFING_LANGUAGES_ATTRIBUTE));
1867
- }
1868
- function documentNoProofFromElement(element) {
1869
- return normalizeDocumentNoProof(element.getAttribute(DOCUMENT_NO_PROOF_ATTRIBUTE));
1870
- }
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;
1877
- }
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)
1889
- },
1890
- ...slot ? {
1891
- [DOCUMENT_SCRIPT_FONT_SLOT_ATTRIBUTE]: slot
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
+ }
1892
1766
  } : {},
1893
- ...language && 'x-none' !== language ? {
1894
- lang: language
1767
+ ...void 0 !== border.size ? {
1768
+ size: border.size
1895
1769
  } : {},
1896
- ...true === noProof ? {
1897
- spellcheck: 'false'
1898
- } : {}
1899
- };
1900
- }
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;
1912
- }
1913
- const language = normalizeDocumentLanguageTag(value);
1914
- if (!language) return null;
1915
- next[slot] = language;
1916
- }
1917
- return Object.keys(next).length ? next : null;
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
+ });
1918
1780
  }
1919
- function isRecord(source) {
1920
- return 'object' == typeof source && null !== source && !Array.isArray(source);
1781
+ function parseDocumentRunBorderElement(element) {
1782
+ const semantic = parseDocumentRunBorder(element.getAttribute(DOCUMENT_RUN_BORDER_ATTRIBUTE));
1783
+ return semantic ?? documentRunBorderFromCss(element);
1921
1784
  }
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);
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}`);
1939
1797
  return {
1940
- mainEntry,
1941
- subEntry,
1942
- crossReference,
1943
- pageBold: !crossReference && Boolean(source?.pageBold),
1944
- pageItalic: !crossReference && Boolean(source?.pageItalic)
1798
+ [DOCUMENT_RUN_BORDER_ATTRIBUTE]: serialized,
1799
+ style: declarations.join('; ')
1945
1800
  };
1946
1801
  }
1947
- function normalizeDocumentIndexEntry(source, fallbackId = 'index-entry') {
1948
- const value = normalizeDocumentIndexEntryDraft(source);
1949
- if (!value) return null;
1950
- return {
1951
- id: validIndexId(source?.id) ?? fallbackId,
1952
- ...value
1953
- };
1802
+ function documentRunBorderIsVisible(source) {
1803
+ const border = normalizeDocumentRunBorder(source);
1804
+ return Boolean(border && documentBorderPresentation(border).width > 0);
1954
1805
  }
1955
- function normalizeDocumentIndexOptions(source) {
1956
- 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'
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'
1961
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
1820
+ },
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
+ });
1962
1826
  }
1963
- function normalizeDocumentIndexValue(source) {
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;
1964
1886
  return {
1965
- id: validIndexId(source.id) ?? 'document-index',
1966
- options: normalizeDocumentIndexOptions(source.options),
1967
- entries: normalizeGeneratedEntries(source.entries),
1968
- truncated: Boolean(source.truncated)
1887
+ pattern: pattern,
1888
+ ...color ? {
1889
+ color
1890
+ } : {},
1891
+ ...fill ? {
1892
+ fill
1893
+ } : {}
1969
1894
  };
1970
1895
  }
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()
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
+ }
1991
1939
  };
1992
- grouped.set(key, group);
1993
1940
  }
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;
1941
+ return semantic;
1942
+ }
1943
+ return background && 'transparent' !== background ? {
1944
+ pattern: 'clear',
1945
+ fill: {
1946
+ value: background
2002
1947
  }
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));
1948
+ } : null;
1949
+ }
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);
2021
1960
  return {
2022
- entries: allEntries.slice(0, MAX_DOCUMENT_INDEX_ENTRIES),
2023
- truncated: markerCount > MAX_DOCUMENT_INDEX_MARKERS || allEntries.length > MAX_DOCUMENT_INDEX_ENTRIES
1961
+ 'data-office-paragraph-shading': serialized,
1962
+ style: styles.join('; ')
2024
1963
  };
2025
1964
  }
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('');
2036
- }
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>'
2041
- ];
2042
- const status = value.truncated ? `仅显示前 ${MAX_DOCUMENT_INDEX_ENTRIES} 项` : `${value.entries.length} 项`;
2043
- 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('');
2050
- }
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}`);
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;
1973
+ return {
1974
+ value: resolved,
1975
+ ...theme ? {
1976
+ theme
1977
+ } : {}
1978
+ };
2060
1979
  }
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
- });
1980
+ function serializedShadingColor(color) {
1981
+ const theme = serializeDocxThemeReference(color.theme ?? null);
1982
+ return {
1983
+ value: color.value,
1984
+ ...theme ? {
1985
+ theme: JSON.parse(theme)
1986
+ } : {}
1987
+ };
2073
1988
  }
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)));
1989
+ function documentParagraphShadingPresentation(shading) {
1990
+ if ('nil' === shading.pattern) return {
1991
+ backgroundColor: 'transparent'
1992
+ };
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
+ };
2091
2013
  }
2092
- return document.body.innerHTML;
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);
2019
+ return {
2020
+ backgroundColor: background,
2021
+ backgroundImage: angles.map(stripe).join(', ')
2022
+ };
2093
2023
  }
2094
- function indexLeader(value) {
2095
- return 'dot' === value || 'dash' === value || 'underline' === value || 'none' === value ? value : null;
2024
+ function shadingColor(color, fallback) {
2025
+ return color && 'auto' !== color.value ? color.value : fallback;
2096
2026
  }
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>`;
2027
+ function shadingPercentage(pattern) {
2028
+ const match = /^pct(\d+)$/.exec(pattern);
2029
+ return match?.[1] ? Number(match[1]) : null;
2108
2030
  }
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;
2031
+ function paragraphShadingBackgroundUsesForeground(shading) {
2032
+ if ('solid' === shading.pattern) return true;
2033
+ const percentage = shadingPercentage(shading.pattern);
2034
+ return null !== percentage && percentage > 50;
2127
2035
  }
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
- });
2148
- }
2149
- }
2150
- return pages.sort((left, right)=>left.pageNumber - right.pageNumber);
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
2053
+ ];
2054
+ return [
2055
+ 0
2056
+ ];
2057
+ }
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;
2151
2099
  }
2152
- function parseGeneratedEntries(source) {
2153
- if (!source) return [];
2154
- try {
2155
- const parsed = JSON.parse(source);
2156
- return Array.isArray(parsed) ? normalizeGeneratedEntries(parsed) : [];
2157
- } catch {
2158
- return [];
2159
- }
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;
2160
2105
  }
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;
2106
+ function documentHighlightCssColor(source) {
2107
+ const value = normalizeDocumentHighlight(source);
2108
+ return value ? HIGHLIGHT_COLORS[value] : null;
2169
2109
  }
2170
- function indexEntryKey(entry) {
2171
- return [
2172
- entry.mainEntry,
2173
- entry.subEntry,
2174
- entry.crossReference
2175
- ].map((value)=>value.normalize('NFKC').toLocaleLowerCase()).join('\u0000');
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;
2176
2115
  }
2177
- function indexEntryDisplay(entry) {
2178
- const term = entry.subEntry ? `${entry.mainEntry} › ${entry.subEntry}` : entry.mainEntry;
2179
- return entry.crossReference ? `${term} · 参见 ${entry.crossReference}` : term;
2116
+ function documentHighlightFromElement(element) {
2117
+ return normalizeDocumentHighlight(element.getAttribute(DOCUMENT_HIGHLIGHT_ATTRIBUTE)) ?? documentHighlightForCssColor(element.style.backgroundColor);
2180
2118
  }
2181
- function normalizedIndexTerm(value) {
2182
- return 'string' == typeof value ? value.replace(/\s+/g, ' ').trim().slice(0, MAX_DOCUMENT_INDEX_TERM_LENGTH) : '';
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
+ } : {};
2183
2126
  }
2184
- function boundedColumns(value) {
2185
- const number = Number(value);
2186
- return Number.isInteger(number) && number >= 1 && number <= 4 ? number : 1;
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;
2154
+ }
2155
+ }
2156
+ return normalizeDocumentParagraphShading(source);
2187
2157
  }
2188
- function positiveInteger(value) {
2189
- const number = Number(value);
2190
- return Number.isSafeInteger(number) && number > 0 ? Math.min(999999, number) : null;
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;
2161
+ try {
2162
+ return normalizeDocumentRunShading(JSON.parse(source));
2163
+ } catch {
2164
+ return null;
2165
+ }
2191
2166
  }
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';
@@ -2905,6 +2905,13 @@ const WORK_TEMPLATES = [
2905
2905
  description: '下拉列表、输入提示与错误警告',
2906
2906
  accent: '#13795b'
2907
2907
  },
2908
+ {
2909
+ id: 'structured-references',
2910
+ kind: 'spreadsheet',
2911
+ name: '结构化引用',
2912
+ description: '表名、当前行公式、插入行自动填充与汇总行',
2913
+ accent: '#0f7b61'
2914
+ },
2908
2915
  {
2909
2916
  id: 'blank-presentation',
2910
2917
  kind: 'presentation',
@@ -2958,6 +2965,7 @@ function initialTitle(templateId, kind) {
2958
2965
  'proofing-languages': '校对语言示例',
2959
2966
  'quarterly-plan': '季度执行计划',
2960
2967
  'data-validation': '数据验证示例',
2968
+ 'structured-references': '结构化引用示例',
2961
2969
  'strategy-deck': '业务策略汇报',
2962
2970
  'animated-deck': '入场动画示例'
2963
2971
  };
@@ -3257,6 +3265,10 @@ function contentForTemplate(templateId) {
3257
3265
  type: 'spreadsheet',
3258
3266
  sheets: dataValidationTemplateSheets()
3259
3267
  };
3268
+ if ('structured-references' === templateId) return {
3269
+ type: 'spreadsheet',
3270
+ sheets: structuredReferenceTemplateSheets()
3271
+ };
3260
3272
  if ('strategy-deck' === templateId) return strategyPresentation();
3261
3273
  if ('animated-deck' === templateId) return animatedPresentation();
3262
3274
  if ('blank-spreadsheet' === templateId) return {
@@ -3609,6 +3621,254 @@ function dataValidationTemplateSheets() {
3609
3621
  }
3610
3622
  ];
3611
3623
  }
3624
+ function structuredReferenceTemplateSheets() {
3625
+ const sales = emptyMatrix(16, 10);
3626
+ sales[0][0] = styledCell('Sales · 结构化引用', {
3627
+ bl: 1,
3628
+ fs: 16,
3629
+ fc: '#ffffff',
3630
+ bg: '#0f7b61'
3631
+ });
3632
+ sales[1][0] = styledCell('插入表格正文行会自动补齐 Revenue;已填写的手工值不会覆盖。', {
3633
+ fc: '#49645c',
3634
+ fs: 10
3635
+ });
3636
+ [
3637
+ 'Item',
3638
+ 'Units',
3639
+ 'Unit price',
3640
+ 'Revenue'
3641
+ ].forEach((value, column)=>{
3642
+ sales[2][column] = headerCell(value);
3643
+ });
3644
+ const rows = [
3645
+ [
3646
+ 'Landing page',
3647
+ 12,
3648
+ 48,
3649
+ '=[@Units]*[@[Unit price]]'
3650
+ ],
3651
+ [
3652
+ 'API integration',
3653
+ 8,
3654
+ 120,
3655
+ '=[@Units]*[@[Unit price]]'
3656
+ ],
3657
+ [
3658
+ 'QA review',
3659
+ 16,
3660
+ 36,
3661
+ '=[@Units]*[@[Unit price]]'
3662
+ ],
3663
+ [
3664
+ 'Release support',
3665
+ 5,
3666
+ 80,
3667
+ '=[@Units]*[@[Unit price]]'
3668
+ ]
3669
+ ];
3670
+ rows.forEach((row, rowIndex)=>{
3671
+ row.forEach((value, columnIndex)=>{
3672
+ sales[rowIndex + 3][columnIndex] = styledCell(value, {
3673
+ bg: rowIndex % 2 ? '#f3faf7' : '#ffffff',
3674
+ ...columnIndex >= 1 ? {
3675
+ ct: {
3676
+ fa: 1 === columnIndex ? '0' : '#,##0.00',
3677
+ t: 'n'
3678
+ }
3679
+ } : {}
3680
+ });
3681
+ });
3682
+ });
3683
+ sales[7][0] = styledCell('Total', {
3684
+ bl: 1,
3685
+ fc: '#215446',
3686
+ bg: '#dff3ec'
3687
+ });
3688
+ sales[7][1] = styledCell('=SUBTOTAL(109,Sales[Units])', {
3689
+ bl: 1,
3690
+ fc: '#215446',
3691
+ bg: '#dff3ec',
3692
+ ct: {
3693
+ fa: '0',
3694
+ t: 'n'
3695
+ }
3696
+ });
3697
+ sales[7][3] = styledCell('=SUBTOTAL(109,Sales[Revenue])', {
3698
+ bl: 1,
3699
+ fc: '#215446',
3700
+ bg: '#dff3ec',
3701
+ ct: {
3702
+ fa: '#,##0.00',
3703
+ t: 'n'
3704
+ }
3705
+ });
3706
+ sales[9][0] = styledCell('Reference examples', {
3707
+ bl: 1,
3708
+ fc: '#215446'
3709
+ });
3710
+ sales[10][0] = styledCell('Headers count');
3711
+ sales[10][1] = styledCell('=COUNTA(Sales[#Headers])', {
3712
+ ct: {
3713
+ fa: '0',
3714
+ t: 'n'
3715
+ }
3716
+ });
3717
+ sales[11][0] = styledCell('Data revenue');
3718
+ sales[11][1] = styledCell('=SUM(Sales[Revenue])', {
3719
+ ct: {
3720
+ fa: '#,##0.00',
3721
+ t: 'n'
3722
+ }
3723
+ });
3724
+ sales[12][0] = styledCell('Units + prices');
3725
+ sales[12][1] = styledCell('=SUM(Sales[[Units]:[Unit price]])', {
3726
+ ct: {
3727
+ fa: '#,##0.00',
3728
+ t: 'n'
3729
+ }
3730
+ });
3731
+ sales[13][0] = styledCell('All table cells');
3732
+ sales[13][1] = styledCell('=COUNTA(Sales[#All])', {
3733
+ ct: {
3734
+ fa: '0',
3735
+ t: 'n'
3736
+ }
3737
+ });
3738
+ const table = {
3739
+ id: createWorkId('spreadsheet-table'),
3740
+ name: 'Sales',
3741
+ displayName: 'SalesData',
3742
+ range: {
3743
+ row: [
3744
+ 2,
3745
+ 7
3746
+ ],
3747
+ column: [
3748
+ 0,
3749
+ 3
3750
+ ]
3751
+ },
3752
+ columns: [
3753
+ {
3754
+ name: 'Item',
3755
+ totalsLabel: 'Total'
3756
+ },
3757
+ {
3758
+ name: 'Units',
3759
+ totalsFunction: 'sum'
3760
+ },
3761
+ {
3762
+ name: 'Unit price'
3763
+ },
3764
+ {
3765
+ name: 'Revenue',
3766
+ calculatedFormula: '=[@Units]*[@[Unit price]]',
3767
+ totalsFunction: 'sum'
3768
+ }
3769
+ ],
3770
+ filters: [],
3771
+ headerRow: true,
3772
+ totalsRow: true,
3773
+ style: {
3774
+ family: 'medium',
3775
+ number: 4
3776
+ },
3777
+ showFirstColumn: false,
3778
+ showLastColumn: false,
3779
+ showRowStripes: true,
3780
+ showColumnStripes: false
3781
+ };
3782
+ const salesSheet = {
3783
+ id: createWorkId('sheet'),
3784
+ name: 'Sales',
3785
+ status: 1,
3786
+ order: 0,
3787
+ row: 16,
3788
+ column: 10,
3789
+ data: sales,
3790
+ tables: [
3791
+ table
3792
+ ],
3793
+ config: {
3794
+ columnlen: {
3795
+ 0: 172,
3796
+ 1: 76,
3797
+ 2: 102,
3798
+ 3: 112
3799
+ },
3800
+ rowlen: {
3801
+ 0: 32,
3802
+ 1: 24,
3803
+ 2: 28,
3804
+ 7: 28
3805
+ },
3806
+ merge: {
3807
+ '0_0': {
3808
+ r: 0,
3809
+ c: 0,
3810
+ rs: 1,
3811
+ cs: 4
3812
+ }
3813
+ }
3814
+ }
3815
+ };
3816
+ const summary = emptyMatrix(10, 4);
3817
+ summary[0][0] = styledCell('Summary · qualified references', {
3818
+ bl: 1,
3819
+ fs: 16,
3820
+ fc: '#ffffff',
3821
+ bg: '#215446'
3822
+ });
3823
+ summary[2][0] = styledCell('Revenue from Sales table');
3824
+ summary[2][1] = styledCell('=SUM(Sales!Sales[Revenue])', {
3825
+ ct: {
3826
+ fa: '#,##0.00',
3827
+ t: 'n'
3828
+ }
3829
+ });
3830
+ summary[3][0] = styledCell('Headers from Sales table');
3831
+ summary[3][1] = styledCell('=COUNTA(Sales!Sales[#Headers])', {
3832
+ ct: {
3833
+ fa: '0',
3834
+ t: 'n'
3835
+ }
3836
+ });
3837
+ summary[5][0] = styledCell('Sales!Sales[...] demonstrates a worksheet-qualified table reference.', {
3838
+ fc: '#49645c',
3839
+ fs: 10
3840
+ });
3841
+ return [
3842
+ salesSheet,
3843
+ {
3844
+ id: createWorkId('sheet'),
3845
+ name: 'Summary',
3846
+ status: 0,
3847
+ order: 1,
3848
+ row: 10,
3849
+ column: 4,
3850
+ data: summary,
3851
+ config: {
3852
+ columnlen: {
3853
+ 0: 240,
3854
+ 1: 120
3855
+ },
3856
+ rowlen: {
3857
+ 0: 32,
3858
+ 5: 28
3859
+ },
3860
+ merge: {
3861
+ '0_0': {
3862
+ r: 0,
3863
+ c: 0,
3864
+ rs: 1,
3865
+ cs: 2
3866
+ }
3867
+ }
3868
+ }
3869
+ }
3870
+ ];
3871
+ }
3612
3872
  function dataValidationTemplateItem(overrides) {
3613
3873
  return {
3614
3874
  type: 'dropdown',