Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/documents/src/engine/diagram-data-model.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { UnsupportedDocumentError } from './errors.js';
import { elementChildren } from './docx-xml.js';

const DGM = 'http://schemas.openxmlformats.org/drawingml/2006/diagram';
const pointTypes = new Set(['doc', 'node', 'asst', 'parTrans', 'sibTrans', 'pres']);
const connectionTypes = new Set(['parOf', 'presOf', 'presParOf', 'unknownRelationship']);
const fail = reason => { throw new UnsupportedDocumentError(`diagram data with ${reason}`); };
const children = node => [...(node?.children || [])];
const children = node => node?.children ? elementChildren(node) : [];
function list(root, name, optional = false) {
const matches = children(root).filter(n => n.namespaceURI === DGM && n.localName === name);
if (optional && !matches.length) return [];
Expand Down
3 changes: 2 additions & 1 deletion src/documents/src/engine/docx-backgrounds.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { checkedHighlight } from './backgrounds.js';
import { UnsupportedDocumentError } from './errors.js';
import { childElements } from './docx-xml.js';
const W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
const fail = why => { throw new UnsupportedDocumentError(why); };
function single(parent, tag) {
const nodes = [...(parent?.children || [])].filter(n => n.namespaceURI === W && n.localName === tag);
const nodes = childElements(parent, W, tag);
if (nodes.length > 1) fail(`duplicate run ${tag}`);
if (nodes[0]?.children.length) fail(`nested run ${tag}`);
return nodes[0];
Expand Down
3 changes: 2 additions & 1 deletion src/documents/src/engine/docx-cell-margins.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { UnsupportedDocumentError } from './errors.js';
import { checkedCellMargins } from './cell-margins.js';
import { elementChildren } from './docx-xml.js';
const W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
const ORDER = ['top', 'start', 'left', 'bottom', 'end', 'right'];
const children = node => [...(node?.children || [])];
const children = node => node?.children ? elementChildren(node) : [];
const fail = reason => { throw new UnsupportedDocumentError('cell margins with ' + reason); };

export function readCellMargins(parent, table = false) {
Expand Down
3 changes: 2 additions & 1 deletion src/documents/src/engine/docx-chart-text.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { UnsupportedDocumentError } from './errors.js';
import { drawingColor } from './docx-drawing-style.js';
import { checkedFontName } from './font-families.js';
import { elementChildren } from './docx-xml.js';

const A = 'http://schemas.openxmlformats.org/drawingml/2006/main';
const children = n => [...(n?.children || [])];
const children = n => n?.children ? elementChildren(n) : [];
const child = (n, name) => children(n).find(c => c.namespaceURI === A && c.localName === name);
const fail = why => { throw new UnsupportedDocumentError(`chart text with ${why}`); };
function check(n, names) {
Expand Down
15 changes: 8 additions & 7 deletions src/documents/src/engine/docx-colors.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { parseXml } from './docx-xml.js';
import { childElements, elementChildren, parseXml } from './docx-xml.js';
import { relationshipEntries } from './docx-relationships.js';
import { UnsupportedDocumentError } from './errors.js';
import { normalizeColor, themeColorTransform } from './colors.js';

const W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
const A = 'http://schemas.openxmlformats.org/drawingml/2006/main';
const REL = 'http://schemas.openxmlformats.org/package/2006/relationships';
const R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/';
const children = n => [...(n?.children || [])];
const children = n => n?.children ? elementChildren(n) : [];
const fail = reason => { throw new UnsupportedDocumentError(`text colors with ${reason}`); };
const names = { dark1: 'dk1', light1: 'lt1', dark2: 'dk2', light2: 'lt2', hyperlink: 'hlink', followedHyperlink: 'folHlink', ...Object.fromEntries(Array.from({ length: 6 }, (_, i) => [`accent${i + 1}`, `accent${i + 1}`])) };
const aliases = { background1: ['bg1', 'light1'], background2: ['bg2', 'light2'], text1: ['t1', 'dark1'], text2: ['t2', 'dark2'] };
function only(parent, ns, name) {
const nodes = children(parent).filter(n => n.namespaceURI === ns && n.localName === name);
const nodes = childElements(parent, ns, name);
if (nodes.length > 1) fail(`duplicate ${name}`);
return nodes[0];
}
Expand All @@ -25,16 +26,16 @@ export function colorReader(parts, settings) {
let theme, mapping, themeLinks;
const themeRelationships = () => {
if (!themeLinks) {
const rels = parts['word/_rels/document.xml.rels'] && parseXml(parts['word/_rels/document.xml.rels']);
themeLinks = children(rels?.documentElement).filter(n => n.namespaceURI === REL && n.getAttribute('Type') === R + 'theme');
const rels = parts['word/_rels/document.xml.rels'] && relationshipEntries(parts['word/_rels/document.xml.rels']);
themeLinks = (rels || []).filter(n => n.namespaceURI === REL && n.attribute('Type') === R + 'theme');
}
return themeLinks;
};
const themeBase = name => {
if (!theme) {
const links = themeRelationships();
if (links.length !== 1 || ![null, 'Internal'].includes(links[0].getAttribute('TargetMode'))) fail('a missing, external or duplicate theme relationship');
const target = links[0].getAttribute('Target') || '';
if (links.length !== 1 || ![null, 'Internal'].includes(links[0].attribute('TargetMode'))) fail('a missing, external or duplicate theme relationship');
const target = links[0].attribute('Target') || '';
if (/[\\?#%:]/.test(target)) fail('an unsupported theme target');
const segments = (target.startsWith('/') ? target.slice(1) : 'word/' + target).split('/'), normalized = [];
for (const segment of segments) {
Expand Down
47 changes: 42 additions & 5 deletions src/documents/src/engine/docx-compatibility.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,52 @@ export function activeDescendants(node) {
}
// Prefixes used in attribute VALUES (Requires/Ignorable) are invisible to the
// XML serializer. Carry in-scope namespace declarations on detached fragments.
// Reading a document serializes every paragraph this way, so the walks below index the
// attribute maps and follow siblings rather than copying live collections into arrays.
export function serializeFragment(node) {
const copy = node.cloneNode(true), bound = new Set();
for (let at = node; at?.nodeType === 1; at = at.parentElement) for (const a of at.attributes) if (a.namespaceURI === XMLNS && !bound.has(a.name)) {
bound.add(a.name); copy.setAttributeNS(XMLNS, a.name, a.value);
return serializeCopy(node.cloneNode(true), inScopeDeclarations(node));
}
/**
* serializeFragment(node) as it is now, serialized only when first asked for. The node and
* the declarations it inherits are copied now, so later changes to the tree cannot alter the
* text. Opening keeps each body paragraph's source this way: most are never asked for (an
* unedited save writes the original part), and serializing them all was a large share of
* the time to open a long document.
*/
export function deferredFragment(node) {
let copy = node.cloneNode(true), declarations = inScopeDeclarations(node), text;
return () => {
if (text === undefined) { text = serializeCopy(copy, declarations); copy = declarations = null; }
return text;
};
}
// The namespace declarations on `node` and its ancestors, the nearest of each prefix first.
function inScopeDeclarations(node) {
const declarations = [], bound = new Set();
for (let at = node; at?.nodeType === 1; at = at.parentElement) {
const attributes = at.attributes;
for (let i = 0; i < attributes.length; i++) {
const a = attributes[i];
if (a.namespaceURI === XMLNS && !bound.has(a.name)) { bound.add(a.name); declarations.push([a.name, a.value]); }
}
}
return declarations;
}
function serializeCopy(copy, declarations) {
for (const [name, value] of declarations) copy.setAttributeNS(XMLNS, name, value);
// Detached shells have no ancestors. Bind namespaced attributes explicitly so
// DOM serializers cannot invent a prefix and reuse it on an unbound sibling.
for (const element of [copy, ...copy.getElementsByTagName('*')]) for (const a of [...element.attributes]) {
if (a.prefix && a.namespaceURI && a.namespaceURI !== XMLNS && a.prefix !== 'xml' && element.lookupNamespaceURI(a.prefix) !== a.namespaceURI) element.setAttributeNS(XMLNS, `xmlns:${a.prefix}`, a.namespaceURI);
// Each element and its descendants in document order; a declaration added here is
// appended to the element's attributes, so the count read first covers its own.
for (let element = copy; element;) {
const attributes = element.attributes;
for (let i = 0, count = attributes.length; i < count; i++) {
const a = attributes[i];
if (a.prefix && a.namespaceURI && a.namespaceURI !== XMLNS && a.prefix !== 'xml' && element.lookupNamespaceURI(a.prefix) !== a.namespaceURI) element.setAttributeNS(XMLNS, `xmlns:${a.prefix}`, a.namespaceURI);
}
let next = element.firstElementChild;
while (!next && element !== copy) { next = element.nextElementSibling; if (!next) element = element.parentElement; }
element = next;
}
return new XMLSerializer().serializeToString(copy);
}
Expand Down
4 changes: 2 additions & 2 deletions src/documents/src/engine/docx-content-controls.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { UnsupportedDocumentError } from './errors.js';
import { serializeFragment } from './docx-compatibility.js';
import { relatedPartPath } from './docx-relationships.js';
import { parseXml, wordText } from './docx-xml.js';
import { elementChildren, parseXml, wordText } from './docx-xml.js';
import { bookmarkReader } from './docx-bookmarks.js';
import { bookmarkEntries } from './bookmarks.js';
import { unresolvedBindingReader } from './docx-data-binding.js';
Expand Down Expand Up @@ -68,7 +68,7 @@ function projectRetainedControls(root) {
}
projectControlContents(root);
}
const children = n => [...(n?.children || [])];
const children = n => n?.children ? elementChildren(n) : [];
const fail = reason => { throw new UnsupportedDocumentError(`content controls with ${reason}`); };
const child = (n, name) => children(n).find(c => c.namespaceURI === W && c.localName === name);
function attributes(node, names = [], ns = W) {
Expand Down
3 changes: 2 additions & 1 deletion src/documents/src/engine/docx-contextual-spacing.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { UnsupportedDocumentError } from './errors.js';
import { childElements } from './docx-xml.js';
const W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';

export function readContextualFlag(parent, tag = 'contextualSpacing') {
const nodes = [...(parent?.children || [])].filter(n => n.namespaceURI === W && n.localName === tag);
const nodes = childElements(parent, W, tag);
if (nodes.length > 1) throw new UnsupportedDocumentError(`duplicate ${tag}`);
const node = nodes[0]; if (!node) return undefined;
if (node.children.length || node.textContent.trim() || [...node.attributes].some(a => a.namespaceURI === W && a.localName !== 'val')) throw new UnsupportedDocumentError(`unsupported ${tag} content`);
Expand Down
3 changes: 2 additions & 1 deletion src/documents/src/engine/docx-drawing-metadata.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { UnsupportedDocumentError } from './errors.js';
import { INK_EXTENSION_URI, readInkExtension } from './docx-ink-extension.js';
import { elementChildren } from './docx-xml.js';

const A = 'http://schemas.openxmlformats.org/drawingml/2006/main';
const DECORATIVE = 'http://schemas.microsoft.com/office/drawing/2017/decorative';
const URI = '{C183D7F6-B498-43B3-948B-1728B52AA6E4}';
const CREATION = 'http://schemas.microsoft.com/office/drawing/2014/main';
const CREATION_URI = '{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}';
const children = node => [...(node?.children || [])];
const children = node => node?.children ? elementChildren(node) : [];
const fail = why => { throw new UnsupportedDocumentError(`drawing metadata with ${why}`); };
function content(node, attrs = [], kind = 'decorative') {
if ([...(node?.childNodes || [])].some(n => [3, 4].includes(n.nodeType) && /[^ \t\r\n]/.test(n.textContent))) fail('unexpected metadata text');
Expand Down
3 changes: 2 additions & 1 deletion src/documents/src/engine/docx-drawing-preview.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { validateImage } from './images.js';
import { elementChildren } from './docx-xml.js';

const WP = 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing';
const A = 'http://schemas.openxmlformats.org/drawingml/2006/main';
const children = node => [...(node?.children || [])];
const children = node => node?.children ? elementChildren(node) : [];

// Use only an unambiguous, unrotated inline frame. A floating placement or
// invalid dimensions cannot safely be approximated as an inline rectangle.
Expand Down
4 changes: 2 additions & 2 deletions src/documents/src/engine/docx-drawing-style.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { parseXml } from './docx-xml.js';
import { elementChildren, parseXml } from './docx-xml.js';
import { relatedPartPath } from './docx-relationships.js';
import { UnsupportedDocumentError } from './errors.js';
import { drawingPresetColors } from './drawing-preset-colors.js';
import { drawingSystemColors } from './drawing-system-colors.js';
import { luminanceTransform } from './colors.js';

const A = 'http://schemas.openxmlformats.org/drawingml/2006/main';
const children = n => [...(n?.children || [])];
const children = n => n?.children ? elementChildren(n) : [];
const fail = why => { throw new UnsupportedDocumentError(`drawing styles with ${why}`); };
const clamp = n => Math.max(0, Math.min(1, n));
// The input and final color resolve through linear RGB percentage units.
Expand Down
3 changes: 2 additions & 1 deletion src/documents/src/engine/docx-field-results.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { MC, alternateBranch } from './docx-compatibility.js';
import { macroButtonLabelStart } from './docx-macro-buttons.js';
import { elementChildren } from './docx-xml.js';

const W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
const is = (node, name) => node.namespaceURI === W && node.localName === name;
const children = node => [...node.children];
const children = node => elementChildren(node);
const metadata = new Set(['pPr', 'rPr', 'sectPr', 'tblPr', 'trPr', 'tcPr', 'sdtPr', 'del', 'moveFrom']);

// Read one story in document order, including fields spanning paragraphs and
Expand Down
6 changes: 3 additions & 3 deletions src/documents/src/engine/docx-fonts.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { parseXml } from './docx-xml.js';
import { childElements, elementChildren, parseXml } from './docx-xml.js';
import { relatedPartPath } from './docx-relationships.js';
import { FONT_SLOTS, checkedFontName } from './font-families.js';
import { UnsupportedDocumentError } from './errors.js';
import { documentEmbeddedFonts } from './embedded-fonts.js';

const W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
const A = 'http://schemas.openxmlformats.org/drawingml/2006/main';
const children = n => [...(n?.children || [])];
const children = n => n?.children ? elementChildren(n) : [];
const fail = why => { throw new UnsupportedDocumentError(`font declarations with ${why}`); };
function only(n, ns, name) {
const nodes = children(n).filter(c => c.namespaceURI === ns && c.localName === name);
const nodes = childElements(n, ns, name);
if (nodes.length > 1) fail(`duplicate ${name}`);
return nodes[0];
}
Expand Down
26 changes: 18 additions & 8 deletions src/documents/src/engine/docx-hyperlinks.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { strToU8 } from 'fflate';
import { parseXml } from './docx-xml.js';
import { elementChildren, parseXml } from './docx-xml.js';
import { UnsupportedDocumentError } from './errors.js';
import { checkedHyperlinkTarget, hyperlinkId, internalHyperlink } from './hyperlinks.js';
import { relationshipPartPath } from './package-part-paths.js';
import { relationshipEntries, relationshipRoot } from './docx-relationships.js';
const W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
const R = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships';
const REL = 'http://schemas.openxmlformats.org/package/2006/relationships';
const serialize = n => new XMLSerializer().serializeToString(n);
const children = n => [...n.children];
const children = n => elementChildren(n);
const relationships = (parts, path) => parts[path] ? parseXml(parts[path]) : parseXml(`<Relationships xmlns="${REL}"/>`);

export function hyperlinkReader(parts, sourcePart = 'word/document.xml') {
const path = relationshipPartPath(sourcePart);
let rels;
let root, byId;
return node => {
if ([...node.childNodes].some(n => [3, 4].includes(n.nodeType) && n.textContent.trim())) throw new UnsupportedDocumentError('text outside hyperlink runs');
for (const attr of [...node.attributes]) {
Expand All @@ -28,14 +29,23 @@ export function hyperlinkReader(parts, sourcePart = 'word/document.xml') {
if (node.hasAttributeNS(W, 'history') && !['true', 'false', '1', '0', 'on', 'off'].includes(history)) throw new UnsupportedDocumentError('invalid hyperlink history');
const common = { id: hyperlinkId(), shell: serialize(node.cloneNode(false)), tooltip: node.getAttributeNS(W, 'tooltip') || '', history: ['true', '1', 'on'].includes(history) };
if (!sourceId) return { ...common, anchor: node.hasAttributeNS(W, 'anchor') ? node.getAttributeNS(W, 'anchor') : null };
rels ||= relationships(parts, path);
if (rels.documentElement.namespaceURI !== REL || rels.documentElement.localName !== 'Relationships') throw new UnsupportedDocumentError('invalid hyperlink relationships part');
const matches = children(rels.documentElement).filter(r => r.getAttribute('Id') === sourceId);
// The relationships part is read once (relationshipEntries) and indexed: a document with
// hundreds of links would otherwise scan every relationship for each link.
if (!byId) {
const bytes = parts[path] || `<Relationships xmlns="${REL}"/>`;
root = relationshipRoot(bytes); byId = new Map();
for (const r of relationshipEntries(bytes)) {
const id = r.attribute('Id');
if (byId.has(id)) byId.get(id).push(r); else byId.set(id, [r]);
}
}
if (root.namespaceURI !== REL || root.localName !== 'Relationships') throw new UnsupportedDocumentError('invalid hyperlink relationships part');
const matches = byId.get(sourceId) || [];
const rel = matches[0];
if (matches.length !== 1 || rel.namespaceURI !== REL || rel.localName !== 'Relationship' || rel.getAttribute('Type') !== `${R}/hyperlink` || rel.getAttribute('TargetMode') !== 'External') throw new UnsupportedDocumentError('missing, duplicate or unsupported hyperlink relationship');
if (matches.length !== 1 || rel.namespaceURI !== REL || rel.localName !== 'Relationship' || rel.attribute('Type') !== `${R}/hyperlink` || rel.attribute('TargetMode') !== 'External') throw new UnsupportedDocumentError('missing, duplicate or unsupported hyperlink relationship');
// Part 2 relationships permit any URI reference. A destination the editor
// cannot open (a relative or file path, unescaped spaces) is kept verbatim.
const target = rel.getAttribute('Target');
const target = rel.attribute('Target');
let retainedTarget = false;
try { checkedHyperlinkTarget(target); } catch { retainedTarget = true; }
return { ...common, target, sourceId, ...(retainedTarget ? { retainedTarget } : {}) };
Expand Down
4 changes: 2 additions & 2 deletions src/documents/src/engine/docx-images.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { drawingStyleReader, drawingEffectColor } from './docx-drawing-style.js'
import { readDrawingMetadata, readDrawingCreationId } from './docx-drawing-metadata.js';
import { readDrawingHyperlink, drawingHyperlinkWriter } from './docx-drawing-hyperlinks.js';
import { readShapeExtensions } from './docx-shape-extensions.js';
import { parseXml, setContentTypeOverride } from './docx-xml.js';
import { elementChildren, parseXml, setContentTypeOverride } from './docx-xml.js';
import { UnsupportedDocumentError } from './errors.js';
import { imageInfo, validateImage, normalizeCrop, normalizedRotation, imageExtents, CROP_SIDES } from './images.js';
import { strToU8 } from 'fflate';
Expand Down Expand Up @@ -39,7 +39,7 @@ export const ASVG = 'http://schemas.microsoft.com/office/drawing/2016/SVG/main';
const SVG_EXTENSION = '{96dac541-7b7a-43d3-8b79-37d633b846f1}';
const LOCAL_DPI = '{28a0092b-c50c-407e-a947-70e740481c1c}';
const serialize = n => new XMLSerializer().serializeToString(n);
const children = n => n ? [...n.children] : [];
const children = n => n ? elementChildren(n) : [];
const child = (n, ns, name) => children(n).find(c => c.namespaceURI === ns && c.localName === name);
const fail = why => { throw new UnsupportedDocumentError(`pictures with ${why}`); };
function check(n, ns, names) {
Expand Down
Loading
Loading