@openpresentation/opf-pptx 0.11.9 → 0.12.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/README.md +22 -3
- package/dist/annotation-export.js +83 -0
- package/dist/annotation-provenance.js +246 -0
- package/dist/body-text-import.js +4 -1
- package/dist/chart-options.js +276 -0
- package/dist/chartex.js +42 -11
- package/dist/code-provenance.js +34 -10
- package/dist/content-topology.js +259 -52
- package/dist/document-provenance.js +77 -5
- package/dist/index.d.ts +17 -1
- package/dist/index.js +289 -67
- package/dist/logo-provenance.js +4 -3
- package/dist/metric-provenance.js +6 -3
- package/dist/native-text-style.js +17 -1
- package/dist/numbered-list.js +114 -0
- package/dist/run-colors.js +118 -0
- package/dist/script-fonts.js +90 -14
- package/dist/table-cell-import.js +3 -1
- package/dist/table-import.js +3 -2
- package/package.json +6 -6
package/dist/logo-provenance.js
CHANGED
|
@@ -36,10 +36,11 @@ function blipFillIdentity(blipFill) {
|
|
|
36
36
|
* left edge and vertically centered. A 90 degree EXIF orientation swaps the
|
|
37
37
|
* frame's axes, so its visual width is the stored height.
|
|
38
38
|
*/
|
|
39
|
-
export function logoFrame(metadata, box) {
|
|
39
|
+
export function logoFrame(metadata, box, anchor = 'left') {
|
|
40
40
|
const placed = pictureTransform(metadata, box, 'fit');
|
|
41
41
|
const visualWidth = (metadata.orientation ?? 1) >= 5 ? placed.h : placed.w;
|
|
42
|
-
|
|
42
|
+
// Right to left (RR-05): a cover logo sits at the top right, anchored to the box's right edge.
|
|
43
|
+
return {...placed, x: placed.x + (anchor === 'right' ? 1 : -1) * (box.w - visualWidth) / 2};
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
@@ -61,7 +62,7 @@ export function placeLogos(entries, logos, metadataFor, fail) {
|
|
|
61
62
|
const embed = picture.match(/<a:blip\b[^>]*r:embed="([^"]+)"/)?.[1];
|
|
62
63
|
const metadata = metadataFor(part, embed);
|
|
63
64
|
if (!metadata) fail(logo.path);
|
|
64
|
-
const placed = logoFrame(metadata, logo.box);
|
|
65
|
+
const placed = logoFrame(metadata, logo.box, logo.anchor);
|
|
65
66
|
const attributes = `${placed.rotation ? ` rot="${placed.rotation * 60000}"` : ''}${placed.flipH ? ' flipH="1"' : ''}${placed.flipV ? ' flipV="1"' : ''}`;
|
|
66
67
|
picture = picture.replace(/<a:xfrm\b[^>]*>[\s\S]*?<\/a:xfrm>/, `<a:xfrm${attributes}><a:off x="${emu(placed.x)}" y="${emu(placed.y)}"/><a:ext cx="${emu(placed.w)}" cy="${emu(placed.h)}"/></a:xfrm>`);
|
|
67
68
|
picture = picture.replace(/<a:srcRect\b[^>]*\/>/, '');
|
|
@@ -14,7 +14,7 @@ export function metricManifest(value,layout,group) {
|
|
|
14
14
|
return {v:1,group,role:'anchor',part:0,line:0,value,parts:layout.parts.map(part=>({role:part.role,visible:part.visible,
|
|
15
15
|
lines:part.fit?.sourceLines.map(({start,end,nextStart,boundary})=>({start,end,nextStart,boundary}))??[]}))};
|
|
16
16
|
}
|
|
17
|
-
export function attachMetricTags(entries,records) {return attachTextTags(entries,records,TAG,'opfMetric','metric');}
|
|
17
|
+
export function attachMetricTags(entries,records,descriptions) {return attachTextTags(entries,records,TAG,'opfMetric','metric',{descriptions});}
|
|
18
18
|
|
|
19
19
|
function validateManifest(manifest) {
|
|
20
20
|
if (!validatePresentation({slides:[{metric:manifest.value}]}).valid) throw new Error('Invalid metric source.');
|
|
@@ -59,10 +59,13 @@ export function importMetricGroups(shapes,paragraphs,relationships,entries,repor
|
|
|
59
59
|
const anchors=group.filter(item=>item.data.role==='anchor');
|
|
60
60
|
if (anchors.length!==1) throw new Error('Missing or duplicated metric anchor.');
|
|
61
61
|
const anchor=anchors[0],manifest=anchor.data;validateManifest(manifest);
|
|
62
|
+
// RR-07: the trend arrow (role 'mark') is generated decoration: at most one, consumed with the group, never source text.
|
|
63
|
+
const marks=group.filter(item=>item.data.role==='mark'),members=group.filter(item=>item.data.role!=='mark');
|
|
64
|
+
if (marks.length>1) throw new Error('Duplicated metric trend mark.');
|
|
62
65
|
const expected=manifest.parts.reduce((sum,part)=>sum+part.lines.length,0);
|
|
63
|
-
if (
|
|
66
|
+
if (members.length!==expected||new Set(members.map(item=>item.shape)).size!==expected) throw new Error('Incomplete or duplicated metric shapes.');
|
|
64
67
|
const nativeLines=new Map();
|
|
65
|
-
for (const item of
|
|
68
|
+
for (const item of members) {
|
|
66
69
|
const {role,part,line}=item.data,key=`${part}:${line}`;
|
|
67
70
|
if ((item!==anchor&&role!=='line')||!Number.isSafeInteger(part)||!Number.isSafeInteger(line)||!manifest.parts[part]?.lines[line]||nativeLines.has(key)) throw new Error('Ambiguous metric source line.');
|
|
68
71
|
nativeLines.set(key,item.text);
|
|
@@ -5,6 +5,18 @@ const boolean = value => ['1','true','on'].includes(value) ? true : ['0','false'
|
|
|
5
5
|
|
|
6
6
|
const themeFamilies = context => ['a:majorFont', 'a:minorFont'].map(key => context.fonts?.[key]?.['a:latin']?.typeface?.trim().toLowerCase()).filter(Boolean);
|
|
7
7
|
|
|
8
|
+
// A run painted with a bare theme slot (a:schemeClr, no color transform) remembers the slot as the OPF color name: the
|
|
9
|
+
// transient `_opfScheme` marker is read, and always removed, by restoreRunColors (run-colors.js) once the document's own
|
|
10
|
+
// colour scheme can confirm the slot holds that colour.
|
|
11
|
+
const OPF_SLOTS = {dk1: 'dark1', lt1: 'light1', dk2: 'dark2', lt2: 'light2', hlink: 'hyperlink', folHlink: 'followedHyperlink', accent1: 'accent1', accent2: 'accent2', accent3: 'accent3', accent4: 'accent4', accent5: 'accent5', accent6: 'accent6'};
|
|
12
|
+
function nativeSchemeSlot(fill, context) {
|
|
13
|
+
const node = fill?.['a:schemeClr'];
|
|
14
|
+
if (!node || Array.isArray(node) || Object.keys(fill).length !== 1 || Object.keys(node).some(key => key !== 'val') || typeof node.val !== 'string') return undefined;
|
|
15
|
+
// Own keys only: a slot name from the package (`__proto__`, `constructor`) must never reach an inherited property.
|
|
16
|
+
const mapping = context?.mapping, slot = mapping && Object.hasOwn(mapping, node.val) ? mapping[node.val] : node.val;
|
|
17
|
+
return typeof slot === 'string' && Object.hasOwn(OPF_SLOTS, slot) ? OPF_SLOTS[slot] : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
8
20
|
export function nativeRunStyle(properties, context, relationships, report, kind = 'table') {
|
|
9
21
|
const result = {};
|
|
10
22
|
for (const [native, opf] of [['b','bold'], ['i','italic']]) {
|
|
@@ -42,7 +54,11 @@ export function nativeRunStyle(properties, context, relationships, report, kind
|
|
|
42
54
|
}
|
|
43
55
|
if (properties['a:solidFill']) {
|
|
44
56
|
const color = readBackgroundColor(properties['a:solidFill'], context);
|
|
45
|
-
if (color)
|
|
57
|
+
if (color) {
|
|
58
|
+
result.color = color.hex + (color.alpha < 1 ? Math.round(color.alpha * 255).toString(16).padStart(2, '0').toUpperCase() : '');
|
|
59
|
+
const slot = color.alpha === 1 ? nativeSchemeSlot(properties['a:solidFill'], context) : undefined;
|
|
60
|
+
if (slot) result._opfScheme = slot;
|
|
61
|
+
}
|
|
46
62
|
else report(`unsupported-${kind}-text-color`, 'The native text color or its transforms could not be resolved.');
|
|
47
63
|
} else if (properties['a:noFill']) {
|
|
48
64
|
result.color = '#00000000';
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// RR-33: numbered lists. OPF `numbering` (a style, a start and a suffix per list level) is written as native
|
|
2
|
+
// `a:buAutoNum` and read back from it.
|
|
3
|
+
//
|
|
4
|
+
// Export: every marker line of a measured list is its own text box (see addMeasuredList), so each carries the number
|
|
5
|
+
// core counted as `startAt`; there is nothing for PowerPoint to count across boxes.
|
|
6
|
+
// Import: `a:buAutoNum` of a native paragraph is read with PowerPoint's own counting (consecutive paragraphs of one
|
|
7
|
+
// level and one scheme count up from `startAt`; a paragraph of another scheme or start begins a new sequence; a
|
|
8
|
+
// shallower or unnumbered paragraph ends the deeper sequences), then the numbers of the whole list are expressed as the
|
|
9
|
+
// shortest `numbering` plus per-entry `start` values that reproduce them.
|
|
10
|
+
|
|
11
|
+
const STYLES = ['arabic', 'roman-upper', 'roman-lower', 'alpha-upper', 'alpha-lower'];
|
|
12
|
+
const STYLE_STEMS = {arabic: 'arabic', 'roman-upper': 'romanUc', 'roman-lower': 'romanLc', 'alpha-upper': 'alphaUc', 'alpha-lower': 'alphaLc'};
|
|
13
|
+
const SUFFIX_TAILS = {period: 'Period', paren: 'ParenR', 'paren-both': 'ParenBoth'};
|
|
14
|
+
|
|
15
|
+
/** The `a:buAutoNum@type` scheme of an OPF style and suffix. */
|
|
16
|
+
export const autoNumScheme = (style, suffix = 'period') => `${STYLE_STEMS[style]}${SUFFIX_TAILS[suffix]}`;
|
|
17
|
+
|
|
18
|
+
const SCHEMES = new Map(STYLES.flatMap(style => Object.keys(SUFFIX_TAILS).map(suffix => [autoNumScheme(style, suffix), {style, suffix}])));
|
|
19
|
+
/** OPF style and suffix of a native scheme, or `undefined` for a scheme OPF has no equivalent for. */
|
|
20
|
+
export const schemeNumbering = type => SCHEMES.get(type);
|
|
21
|
+
|
|
22
|
+
/** `startAt` of a native auto-number: an integer from 1 to 32767, default 1. */
|
|
23
|
+
export function autoNumStart(value) {
|
|
24
|
+
if (value === undefined || value === '') return 1;
|
|
25
|
+
const number = Number(value);
|
|
26
|
+
return Number.isInteger(number) && number >= 1 && number <= 32767 ? number : undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The number each paragraph of one native shape displays, by PowerPoint's counting. `paragraphs` are
|
|
31
|
+
* `{level, autoNum?: {type, startAt}}`; returns the displayed numbers (`undefined` for an unnumbered paragraph).
|
|
32
|
+
*/
|
|
33
|
+
export function displayedNumbers(paragraphs) {
|
|
34
|
+
const sequences = [];
|
|
35
|
+
return paragraphs.map(paragraph => {
|
|
36
|
+
const level = Number.isInteger(paragraph.level) && paragraph.level >= 0 ? paragraph.level : 0;
|
|
37
|
+
if (!paragraph.autoNum) { sequences.length = Math.min(sequences.length, level); return undefined; }
|
|
38
|
+
const start = autoNumStart(paragraph.autoNum.startAt) ?? 1, type = paragraph.autoNum.type ?? 'arabicPeriod';
|
|
39
|
+
const current = sequences[level];
|
|
40
|
+
const value = current && current.type === type && current.start === start ? current.value + 1 : start;
|
|
41
|
+
sequences[level] = {type, start, value};
|
|
42
|
+
sequences.length = level + 1;
|
|
43
|
+
return value;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const entryOf = ({style, start, suffix}) => {
|
|
48
|
+
const fields = {...(style !== 'arabic' ? {style} : {}), ...(start !== 1 ? {start} : {}), ...(suffix !== 'period' ? {suffix} : {})};
|
|
49
|
+
const names = Object.keys(fields);
|
|
50
|
+
if (!names.length) return 'arabic';
|
|
51
|
+
return names.length === 1 && names[0] === 'style' ? style : fields;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The OPF numbering of a native list: `paragraphs` are `{level, number?: {type, value}}` in reading order (`number` is the
|
|
56
|
+
* displayed number and its scheme). Returns `{numbering, starts, diagnostics}`: `numbering` is the shortest authored form
|
|
57
|
+
* (`undefined` when the list is not numbered), `starts` maps a paragraph index to the entry `start` that restores its
|
|
58
|
+
* number, and `diagnostics` lists what could not be represented.
|
|
59
|
+
*/
|
|
60
|
+
export function deriveListNumbering(paragraphs) {
|
|
61
|
+
const diagnostics = [];
|
|
62
|
+
const numbered = paragraphs.filter(paragraph => paragraph.number);
|
|
63
|
+
if (!numbered.length) return {numbering: undefined, starts: new Map(), diagnostics};
|
|
64
|
+
if (numbered.length !== paragraphs.length) {
|
|
65
|
+
diagnostics.push({code: 'numbering-mixed', message: 'A list that mixes numbered and bulleted or plain paragraphs has no OPF equivalent (numbering applies to the whole list); it imports as bullets.'});
|
|
66
|
+
return {numbering: undefined, starts: new Map(), diagnostics};
|
|
67
|
+
}
|
|
68
|
+
const levels = [];
|
|
69
|
+
let unknown = false, mixedStyles = false;
|
|
70
|
+
for (const paragraph of numbered) {
|
|
71
|
+
const level = paragraph.level ?? 0;
|
|
72
|
+
const known = schemeNumbering(paragraph.number.type);
|
|
73
|
+
if (!known) { unknown = true; }
|
|
74
|
+
const resolved = known ?? {style: 'arabic', suffix: 'period'};
|
|
75
|
+
if (!levels[level]) levels[level] = {...resolved, start: paragraph.number.value};
|
|
76
|
+
else if (levels[level].style !== resolved.style || levels[level].suffix !== resolved.suffix) mixedStyles = true;
|
|
77
|
+
}
|
|
78
|
+
if (unknown) diagnostics.push({code: 'numbering-scheme-adapted', message: 'A native numbering scheme OPF has no equivalent for (an East Asian, Hebrew or Arabic scheme, or a bullet-only scheme) imports as arabic with a period.'});
|
|
79
|
+
if (mixedStyles) diagnostics.push({code: 'numbering-style-adapted', message: 'Entries of one list level use different native numbering schemes; OPF numbers a level in one style, so the first entry\'s style is used for the level.'});
|
|
80
|
+
const filled = [];
|
|
81
|
+
for (let level = 0; level < levels.length; level++) filled[level] = levels[level] ?? {...(filled[level - 1] ?? {style: 'arabic', suffix: 'period'}), start: 1};
|
|
82
|
+
// Per-entry starts: restart wherever the displayed number is not what counting gives.
|
|
83
|
+
const counters = [], starts = new Map();
|
|
84
|
+
paragraphs.forEach((paragraph, index) => {
|
|
85
|
+
const level = paragraph.level ?? 0, value = paragraph.number.value;
|
|
86
|
+
const natural = counters[level] === undefined ? filled[level].start : counters[level] + 1;
|
|
87
|
+
if (natural !== value) starts.set(index, value);
|
|
88
|
+
counters[level] = value;
|
|
89
|
+
counters.length = level + 1;
|
|
90
|
+
});
|
|
91
|
+
// The last entry repeats for deeper levels, so trailing equal entries are redundant.
|
|
92
|
+
const same = (a, b) => a.style === b.style && a.suffix === b.suffix && a.start === b.start;
|
|
93
|
+
while (filled.length > 1 && same(filled.at(-1), filled.at(-2))) filled.pop();
|
|
94
|
+
const entries = filled.map(entryOf);
|
|
95
|
+
return {numbering: entries.length === 1 ? entries[0] : entries, starts, diagnostics};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A canonical form of an authored `numbering`, for comparing two spellings of the same numbering. */
|
|
99
|
+
export function canonicalNumbering(numbering) {
|
|
100
|
+
const list = Array.isArray(numbering) ? numbering : [numbering];
|
|
101
|
+
const resolved = list.map(entry => typeof entry === 'string' ? {style: entry, start: 1, suffix: 'period'} : {style: entry?.style ?? 'arabic', start: entry?.start ?? 1, suffix: entry?.suffix ?? 'period'});
|
|
102
|
+
while (resolved.length > 1 && JSON.stringify(resolved.at(-1)) === JSON.stringify(resolved.at(-2))) resolved.pop();
|
|
103
|
+
return JSON.stringify(resolved);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Add `autoNum` (the paragraph's `a:buAutoNum` attributes) and the displayed `number` ({type, value}) to the numbered
|
|
108
|
+
* paragraphs of one native shape. Paragraphs without a native auto-number are returned unchanged.
|
|
109
|
+
*/
|
|
110
|
+
export function withDisplayedNumbers(paragraphs) {
|
|
111
|
+
if (!paragraphs.some(paragraph => paragraph.autoNum)) return paragraphs;
|
|
112
|
+
const numbers = displayedNumbers(paragraphs);
|
|
113
|
+
return paragraphs.map((paragraph, index) => numbers[index] === undefined ? paragraph : {...paragraph, number: {type: paragraph.autoNum.type ?? 'arabicPeriod', value: numbers[index]}});
|
|
114
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Run colours named by a reference (RR-08).
|
|
2
|
+
//
|
|
3
|
+
// A TextRun colour may be a ColorRef: a `var:` variable, a colour-scheme slot ('accent2') or a role ('primary'). The
|
|
4
|
+
// export resolves it (or writes the theme slot that holds the same colour) and PowerPoint has no field for the name, so
|
|
5
|
+
// import used to return only the resolved hex. Two sources can name the reference again, and both are gated by the
|
|
6
|
+
// imported document's own colour resolution: a name is restored only while it still resolves to the run's current colour,
|
|
7
|
+
// so a colour edited in PowerPoint keeps its edited value and nothing is invented.
|
|
8
|
+
//
|
|
9
|
+
// 1. Provenance (full mode): OPF_SLIDE_V1 `colors` = {n, at: [[slot, reference], ...]}. The slots are the text runs of the
|
|
10
|
+
// slide's content in a fixed order (slideRunSlots: text runs, list item and description runs; regions by name, then
|
|
11
|
+
// blocks and groups in order, or the root payload). `n` is how many slots the authored slide had; the record is used only
|
|
12
|
+
// when the imported slide has exactly that many, so a run can never be matched to another. It holds reference names
|
|
13
|
+
// only, never words.
|
|
14
|
+
// 2. A slide with no OPF record (plain PPTX): a run painted with a bare a:schemeClr (no colour transform) names that
|
|
15
|
+
// theme slot ('accent2', 'dark1', 'hyperlink'), restored when the document's colour scheme holds that exact colour for
|
|
16
|
+
// the slot. The exporter writes schemeClr only where the theme holds the colour, so this is the same convention.
|
|
17
|
+
|
|
18
|
+
import {ROOT_PAYLOAD_FIELDS, REGION_KEYS} from './content-topology.js';
|
|
19
|
+
|
|
20
|
+
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
21
|
+
const REGION_KEY_SET = new Set(REGION_KEYS);
|
|
22
|
+
const isGroup = block => object(block) && (block.type === 'group' || (block.type === undefined && Array.isArray(block.blocks)));
|
|
23
|
+
export const MAX_COLOR_SLOTS = 100000;
|
|
24
|
+
|
|
25
|
+
// A reference name: the schema's ColorRef forms other than a hex literal.
|
|
26
|
+
const REFERENCE = /^(?:var:[a-z][a-z0-9-]*|accent[1-6]|dark[12]|light[12]|hyperlink|followedHyperlink|primary|secondary|accent|background|surface|text|textSecondary)$/;
|
|
27
|
+
export const isColorReference = value => typeof value === 'string' && REFERENCE.test(value);
|
|
28
|
+
|
|
29
|
+
/** The payload objects of a slide in the fixed traversal order: regions (by name), else blocks (groups depth first), else the slide's root payload. */
|
|
30
|
+
function* leaves(slide) {
|
|
31
|
+
const walk = function* (block) {
|
|
32
|
+
if (!object(block)) return;
|
|
33
|
+
if (isGroup(block)) { for (const child of Array.isArray(block.blocks) ? block.blocks : []) yield* walk(child); return; }
|
|
34
|
+
yield block;
|
|
35
|
+
};
|
|
36
|
+
const regions = Object.keys(slide).filter(key => REGION_KEY_SET.has(key) && object(slide[key])).sort();
|
|
37
|
+
if (regions.length) { for (const key of regions) yield* walk(slide[key]); return; }
|
|
38
|
+
if (Array.isArray(slide.blocks)) { for (const block of slide.blocks) yield* walk(block); return; }
|
|
39
|
+
yield slide;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The text runs of the slide's text and list payloads, in order: an object (a run) or null (a plain string). */
|
|
43
|
+
export function slideRunSlots(slide) {
|
|
44
|
+
const slots = [];
|
|
45
|
+
const runs = value => {
|
|
46
|
+
if (Array.isArray(value)) for (const run of value) slots.push(object(run) ? run : null);
|
|
47
|
+
else if (typeof value === 'string') slots.push(null);
|
|
48
|
+
};
|
|
49
|
+
for (const leaf of leaves(slide)) {
|
|
50
|
+
for (const field of ROOT_PAYLOAD_FIELDS) {
|
|
51
|
+
const value = leaf[field];
|
|
52
|
+
if (field === 'text') { if (Array.isArray(value)) runs(value); }
|
|
53
|
+
else if ((field === 'items' || field === 'bullets') && Array.isArray(value)) {
|
|
54
|
+
for (const item of value) {
|
|
55
|
+
if (Array.isArray(item) || typeof item === 'string') runs(item);
|
|
56
|
+
else if (object(item)) { runs(item.text); runs(item.description); }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return slots;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The record stored for an authored slide, or undefined when no run names a reference. */
|
|
65
|
+
export function runColorRecord(slide) {
|
|
66
|
+
const slots = slideRunSlots(slide);
|
|
67
|
+
if (slots.length > MAX_COLOR_SLOTS) return undefined;
|
|
68
|
+
const at = [];
|
|
69
|
+
slots.forEach((run, index) => { if (run && isColorReference(run.color)) at.push([index, run.color]); });
|
|
70
|
+
return at.length ? {n: slots.length, at} : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Throws when `value` is not a well-formed colours record. */
|
|
74
|
+
export function validateRunColors(value) {
|
|
75
|
+
if (!object(value) || !Number.isSafeInteger(value.n) || value.n < 1 || value.n > MAX_COLOR_SLOTS || !Array.isArray(value.at) || !value.at.length || value.at.length > value.n) throw Error('Invalid run colours record.');
|
|
76
|
+
let previous = -1;
|
|
77
|
+
for (const entry of value.at) {
|
|
78
|
+
if (!Array.isArray(entry) || entry.length !== 2 || !Number.isSafeInteger(entry[0]) || entry[0] <= previous || entry[0] >= value.n || !isColorReference(entry[1])) throw Error('Invalid run colour entry.');
|
|
79
|
+
previous = entry[0];
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function* markedRuns(value) {
|
|
85
|
+
if (Array.isArray(value)) { for (const item of value) yield* markedRuns(item); return; }
|
|
86
|
+
if (!object(value)) return;
|
|
87
|
+
if (Object.hasOwn(value, '_opfScheme')) yield value;
|
|
88
|
+
for (const item of Object.values(value)) if (item !== null && typeof item === 'object') yield* markedRuns(item);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Restore run colour references on `slides` (the imported document's slides). `info[i]` is
|
|
93
|
+
* {record, structure, content}: the stored OPF_SLIDE_V1 value, whether the slide's arrangement matches it and whether its
|
|
94
|
+
* content structure was restored from it, or undefined for an untagged slide. `resolve(slide, name)` returns the '#RRGGBB'
|
|
95
|
+
* the name resolves to in the document (or undefined). Every transient `_opfScheme` marker is removed.
|
|
96
|
+
*/
|
|
97
|
+
export function restoreRunColors(slides, info, resolve, report) {
|
|
98
|
+
slides.forEach((slide, index) => {
|
|
99
|
+
const tag = info[index];
|
|
100
|
+
const marked = [...markedRuns(slide)];
|
|
101
|
+
const restoreName = (run, name) => {
|
|
102
|
+
const hex = typeof run.color === 'string' && /^#[0-9a-f]{6}$/i.test(run.color) ? run.color.toUpperCase() : undefined;
|
|
103
|
+
if (hex && resolve(slide, name)?.toUpperCase() === hex) run.color = name;
|
|
104
|
+
};
|
|
105
|
+
if (tag?.record?.colors) {
|
|
106
|
+
const {n, at} = tag.record.colors;
|
|
107
|
+
// When the content form was not restored its own diagnostic says why, and the slots cannot be aligned.
|
|
108
|
+
if (tag.structure && tag.content) {
|
|
109
|
+
const slots = slideRunSlots(slide);
|
|
110
|
+
if (slots.length !== n) report({code: 'color-reference-changed', path: `slides.${index}`, message: `The slide's text runs no longer match the stored colour references (${slots.length} runs now, ${n} at export), so the named run colours of slides.${index} (variables, scheme slots and roles) were not restored; the runs keep their resolved colours.`});
|
|
111
|
+
else for (const [slot, name] of at) if (slots[slot]) restoreName(slots[slot], name);
|
|
112
|
+
}
|
|
113
|
+
} else if (!tag?.record) {
|
|
114
|
+
for (const run of marked) if (typeof run._opfScheme === 'string') restoreName(run, run._opfScheme);
|
|
115
|
+
}
|
|
116
|
+
for (const run of marked) delete run._opfScheme;
|
|
117
|
+
});
|
|
118
|
+
}
|
package/dist/script-fonts.js
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
import * as opfCore from "@openpresentation/opf";
|
|
14
14
|
|
|
15
15
|
const resolver = typeof opfCore.resolveScriptFonts === "function" ? opfCore.resolveScriptFonts : null;
|
|
16
|
+
// RR-05: authored alignment is logical for right-to-left text (`left` is the start edge). Core owns the rule; a core without it
|
|
17
|
+
// reports no line directions either, so the fallback never has anything to flip.
|
|
18
|
+
export const physicalAlignment = typeof opfCore.physicalAlignment === "function" ? opfCore.physicalAlignment : alignment => alignment;
|
|
16
19
|
// The one paragraph-direction rule shared with the renderer (core FF-07).
|
|
17
20
|
const paragraphDirection = typeof opfCore.paragraphDirection === "function" ? opfCore.paragraphDirection : null;
|
|
18
21
|
|
|
@@ -126,24 +129,89 @@ const decodeText = value => value.replace(/&(?:#(\d+)|#x([0-9a-f]+)|(amp|lt|gt|q
|
|
|
126
129
|
decimal ? String.fromCodePoint(Number(decimal)) : hex ? String.fromCodePoint(parseInt(hex, 16)) : {amp: "&", lt: "<", gt: ">", quot: "\"", apos: "'"}[name.toLowerCase()]);
|
|
127
130
|
|
|
128
131
|
/**
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* right-to-left. Alignment is left as composed.
|
|
132
|
+
* RR-05 bidi runs. PowerPoint treats the digits of a run tagged with a right-to-left language as numbers of that language and
|
|
133
|
+
* orders them as their own item, so "v2.0" or "PowerPoint 365" inside an Arabic or Hebrew run reads "2.0v" or "365PowerPoint".
|
|
134
|
+
* Splitting each left-to-right Latin phrase into its own run tagged en-US keeps it one item that PowerPoint orders left to right.
|
|
133
135
|
*/
|
|
134
|
-
|
|
136
|
+
const LATIN_WORD = /[0-9\p{Script=Latin}]/u, LATIN_LETTER = /\p{Script=Latin}/u, PHRASE_JOINER = /[ \u00a0.,:;&'\u2019/+*_@#=-]/;
|
|
137
|
+
|
|
138
|
+
/** Ranges [start, end) of the Latin phrases of a decoded string: a Latin letter through the last Latin letter or digit joined by spaces and word punctuation. */
|
|
139
|
+
export function latinPhrases(text) {
|
|
140
|
+
const ranges = [];
|
|
141
|
+
for (let index = 0; index < text.length;) {
|
|
142
|
+
const char = String.fromCodePoint(text.codePointAt(index));
|
|
143
|
+
if (!LATIN_LETTER.test(char)) { index += char.length; continue; }
|
|
144
|
+
let end = index + char.length;
|
|
145
|
+
for (let cursor = end; cursor < text.length;) {
|
|
146
|
+
const next = String.fromCodePoint(text.codePointAt(cursor));
|
|
147
|
+
if (LATIN_WORD.test(next)) { cursor += next.length; end = cursor; continue; }
|
|
148
|
+
if (!PHRASE_JOINER.test(next)) break;
|
|
149
|
+
// Joiners count only when a Latin letter or digit follows them directly.
|
|
150
|
+
let after = cursor + next.length;
|
|
151
|
+
while (after < text.length && PHRASE_JOINER.test(text[after])) after += 1;
|
|
152
|
+
if (after < text.length && LATIN_WORD.test(String.fromCodePoint(text.codePointAt(after)))) cursor = after; else break;
|
|
153
|
+
}
|
|
154
|
+
ranges.push([index, end]);
|
|
155
|
+
index = end;
|
|
156
|
+
}
|
|
157
|
+
return ranges;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const encodeText = value => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
161
|
+
const RUN = /<a:r>(<a:rPr\b[^>]*\/>|<a:rPr\b[^>]*[^/]>[\s\S]*?<\/a:rPr>)<a:t>([^<]*)<\/a:t><\/a:r>/g;
|
|
162
|
+
|
|
163
|
+
/** Split the runs of a right-to-left paragraph so each Latin phrase is its own en-US run. */
|
|
164
|
+
function splitLatinRuns(body) {
|
|
165
|
+
return body.replace(RUN, (run, properties, escaped) => {
|
|
166
|
+
const text = decodeText(escaped), phrases = latinPhrases(text);
|
|
167
|
+
if (!phrases.length) return run;
|
|
168
|
+
const make = (value, lang) => `<a:r>${lang ? properties.replace(/\slang="[^"]*"/, ` lang="${lang}"`) : properties}<a:t>${encodeText(value)}</a:t></a:r>`;
|
|
169
|
+
const pieces = [];
|
|
170
|
+
let cursor = 0;
|
|
171
|
+
for (const [from, to] of phrases) {
|
|
172
|
+
if (from > cursor) pieces.push(make(text.slice(cursor, from)));
|
|
173
|
+
pieces.push(make(text.slice(from, to), "en-US"));
|
|
174
|
+
cursor = to;
|
|
175
|
+
}
|
|
176
|
+
if (cursor < text.length) pieces.push(make(text.slice(cursor)));
|
|
177
|
+
return pieces.join("");
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* In a right-to-left deck every paragraph states its direction. The writers mark the lines of a paragraph they laid out
|
|
183
|
+
* (rtl="1", the direction core computed once for the whole paragraph, so wrapped lines never differ); any other paragraph takes
|
|
184
|
+
* core's paragraphDirection() over its own text: rtl="1" when it is right-to-left, else an explicit rtl="0", because the master
|
|
185
|
+
* default levels start right-to-left. A right-to-left paragraph with no explicit alignment starts at the right edge (logical
|
|
186
|
+
* alignment: the master default is left), and its Latin phrases become en-US runs (see splitLatinRuns). With `notes` the
|
|
187
|
+
* alignment is also set on an explicit left paragraph, because the notes writers pass no alignment of their own.
|
|
188
|
+
*/
|
|
189
|
+
function paragraphRtl(xml, deckDirection, {notes = false} = {}) {
|
|
135
190
|
return xml.replace(/<a:p>([\s\S]*?)<\/a:p>/g, (paragraph, body) => {
|
|
136
191
|
const text = [...body.matchAll(/<a:t>([^<]*)<\/a:t>|<a:br\b/g)].map(match => match[1] === undefined ? "\n" : decodeText(match[1])).join("");
|
|
137
|
-
const value = paragraphDirection(text, deckDirection) === "rtl" ? "1" : "0";
|
|
138
192
|
const properties = /^(\s*)<a:pPr\b([^>]*?)(\/?)>/.exec(body);
|
|
139
|
-
|
|
140
|
-
const
|
|
193
|
+
const marked = properties && / rtl="([01])"/.exec(properties[2]);
|
|
194
|
+
const rtl = marked ? marked[1] === "1" : paragraphDirection(text, deckDirection) === "rtl";
|
|
195
|
+
const value = rtl ? "1" : "0";
|
|
196
|
+
if (rtl) body = splitLatinRuns(body);
|
|
197
|
+
if (!properties) return `<a:p><a:pPr rtl="${value}"${rtl ? " algn=\"r\"" : ""}/>${body}</a:p>`;
|
|
198
|
+
let attributes = / rtl="[^"]*"/.test(properties[2]) ? properties[2].replace(/ rtl="[^"]*"/, ` rtl="${value}"`) : `${properties[2]} rtl="${value}"`;
|
|
199
|
+
if (rtl) {
|
|
200
|
+
if (!/\salgn="/.test(attributes)) attributes += " algn=\"r\"";
|
|
201
|
+
else if (notes) attributes = attributes.replace(/ algn="l"/, " algn=\"r\"");
|
|
202
|
+
}
|
|
141
203
|
return `<a:p>${properties[1]}<a:pPr${attributes}${properties[3]}>${body.slice(properties[0].length)}</a:p>`;
|
|
142
204
|
});
|
|
143
205
|
}
|
|
144
206
|
|
|
145
|
-
/**
|
|
146
|
-
const
|
|
207
|
+
/** Tables of a right-to-left deck run right to left: the first column is the rightmost, as core's layout draws it. */
|
|
208
|
+
const tableRtl = xml => xml.replace(/<a:tblPr\b([^>]*?)(\/?)>/g, (match, attributes, close) => / rtl="/.test(attributes) ? match : `<a:tblPr rtl="1"${attributes}${close}>`);
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Master, layout and presentation default paragraph levels start right-to-left, and a left-aligned level starts at the right edge
|
|
212
|
+
* (logical alignment): text typed later in an Arabic or Hebrew deck then reads and aligns as the exported slides do.
|
|
213
|
+
*/
|
|
214
|
+
const levelRtl = xml => xml.replace(/<a:(?:lvl\dpPr|defPPr)\b[^>]*?\srtl="0"[^>]*>/g, level => level.replace(/\srtl="0"/, " rtl=\"1\"").replace(/\salgn="l"/, " algn=\"r\""));
|
|
147
215
|
|
|
148
216
|
/**
|
|
149
217
|
* Apply the plan to one generated XML part. `slideIndex` names the slide a
|
|
@@ -157,10 +225,10 @@ export function partScriptFonts(path, xml, plan, slideIndex) {
|
|
|
157
225
|
if (/^ppt\/slides\/slide\d+\.xml$/.test(path)) {
|
|
158
226
|
xml = xml.replace(/<p:(sp|graphicFrame)>[\s\S]*?<\/p:\1>/g, shape =>
|
|
159
227
|
runScriptFonts(shape, resolved, /<p:cNvPr\b[^>]*\bname="OPF heading /.test(shape)));
|
|
160
|
-
if (plan.rtl) xml = paragraphRtl(xml, plan.deck.direction);
|
|
228
|
+
if (plan.rtl) xml = tableRtl(paragraphRtl(xml, plan.deck.direction));
|
|
161
229
|
} else if (/^ppt\/(?:charts\/chart(?:Ex)?|notesSlides\/notesSlide)\d+\.xml$/.test(path)) {
|
|
162
230
|
xml = runScriptFonts(xml, resolved, false);
|
|
163
|
-
if (plan.rtl && path.startsWith("ppt/notesSlides/")) xml = paragraphRtl(xml, plan.deck.direction);
|
|
231
|
+
if (plan.rtl && path.startsWith("ppt/notesSlides/")) xml = paragraphRtl(xml, plan.deck.direction, {notes: true});
|
|
164
232
|
} else if (plan.rtl && /^ppt\/(?:slideMasters\/slideMaster\d+|slideLayouts\/slideLayout\d+|notesMasters\/notesMaster\d+|presentation)\.xml$/.test(path)) {
|
|
165
233
|
xml = levelRtl(xml);
|
|
166
234
|
}
|
|
@@ -252,6 +320,12 @@ export function reconcileLanguage(groups, observed, report) {
|
|
|
252
320
|
* language, and theme East Asian/complex-script fonts the document does not
|
|
253
321
|
* reproduce.
|
|
254
322
|
*/
|
|
323
|
+
/** Whether the language observed in the runs is written right to left. */
|
|
324
|
+
export function observedRtl(observed) {
|
|
325
|
+
if (observed.lang === undefined) return false;
|
|
326
|
+
try { return resolver ? resolver({language: observed.language}).rtl === true : observed.match?.record.direction === "rtl"; } catch { return false; }
|
|
327
|
+
}
|
|
328
|
+
|
|
255
329
|
export function languageDiagnostics(imported, observed, report) {
|
|
256
330
|
if (!report) return;
|
|
257
331
|
const {lang, match, ranked, rtlParagraphs, theme} = observed;
|
|
@@ -259,9 +333,11 @@ export function languageDiagnostics(imported, observed, report) {
|
|
|
259
333
|
if (rtlParagraphs) report({code: "rtl-language-mismatch", path: "language", message: `${rtlParagraphs} right-to-left paragraph(s) carry no run language, so no presentation language was imported.`});
|
|
260
334
|
return;
|
|
261
335
|
}
|
|
262
|
-
|
|
336
|
+
// RR-05: a right-to-left deck's Latin phrases are their own en-US runs; they are not a second presentation language.
|
|
337
|
+
const languages = observedRtl(observed) ? ranked.filter(([tag]) => tag === lang || tag !== "en-US") : ranked;
|
|
338
|
+
if (languages.length > 1) {
|
|
263
339
|
report({code: "mixed-run-languages", path: "language",
|
|
264
|
-
message: `Runs use ${
|
|
340
|
+
message: `Runs use ${languages.length} languages (${languages.map(([tag, count]) => `${tag} x${count}`).join(", ")}). OPF has one presentation language, so ${lang} was imported.`});
|
|
265
341
|
}
|
|
266
342
|
const kept = imported.language === observed.language;
|
|
267
343
|
if (kept && !match) report({code: "language-uncatalogued", path: "language", message: `Run language ${lang} matches no languages catalog record; it was imported as a BCP-47 tag.`});
|
|
@@ -63,7 +63,9 @@ export function nativeCellStyle(properties, body, context, scale, report) {
|
|
|
63
63
|
const listStyle = drawingObject(child(body,'a:lstStyle'));
|
|
64
64
|
const alignments = nodes(body,'a:p').map(paragraph => {
|
|
65
65
|
const p = drawingObject(paragraph['a:p'])['a:pPr'] ?? {};
|
|
66
|
-
|
|
66
|
+
const algn = p.algn ?? listStyle[`a:lvl${Number(p.lvl ?? 0)+1}pPr`]?.algn ?? listStyle['a:defPPr']?.algn ?? 'l';
|
|
67
|
+
// Alignment is logical for right-to-left text (RR-05): an rtl paragraph aligned right starts at the start edge.
|
|
68
|
+
return p.rtl === '1' || p.rtl === 1 ? ({l:'r',r:'l'}[algn] ?? algn) : algn;
|
|
67
69
|
});
|
|
68
70
|
const alignment = {l:'left',ctr:'center',r:'right'}[alignments[0] ?? 'l'];
|
|
69
71
|
if (alignment) style.align = alignment;
|
package/dist/table-import.js
CHANGED
|
@@ -121,7 +121,7 @@ function conditionalProperties(style, properties, row, column, rowCount, columnC
|
|
|
121
121
|
|
|
122
122
|
// Use the same direct graphic-frame ordering as the slide collector. The ordered
|
|
123
123
|
// reader retains interleaved runs, fields, breaks, empty paragraphs and spaces.
|
|
124
|
-
export function importTableFrames(slidePath, archive, relationships, report, dimensions) {
|
|
124
|
+
export function importTableFrames(slidePath, archive, relationships, report, dimensions, {rtlDeck = false} = {}) {
|
|
125
125
|
const context = readSlideTheme(slidePath, archive);
|
|
126
126
|
const scale = Math.min(dimensions.widthInches,dimensions.heightInches) * 96 / 720;
|
|
127
127
|
const root = child(context.parsedPart(slidePath)?.tree, 'p:sld');
|
|
@@ -133,7 +133,8 @@ export function importTableFrames(slidePath, archive, relationships, report, dim
|
|
|
133
133
|
const style = tableStyle(props, archive, context, (code, message) => report(frameIndex, '', code, message));
|
|
134
134
|
if (style?.['a:tblBg']) report(frameIndex, '', 'unsupported-table-cell-style', 'The native table style background is not represented; supported character styles are retained.');
|
|
135
135
|
const properties = attrs(table, 'a:tblPr');
|
|
136
|
-
|
|
136
|
+
// A right-to-left table of a right-to-left deck keeps its logical column order (RR-05); the deck language carries the direction.
|
|
137
|
+
if (boolean(properties.rtl) === true && !rtlDeck) report(frameIndex, '', 'unsupported-table-direction', 'Right-to-left table geometry is not represented; cells retain native source order.');
|
|
137
138
|
const headers = boolean(properties.firstRow) === true;
|
|
138
139
|
const nativeRows = nodes(table, 'a:tr');
|
|
139
140
|
const columnCount = nodes(child(table, 'a:tblGrid'), 'a:gridCol').length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-pptx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"sideEffects": false,
|
|
38
38
|
"scripts": {
|
|
39
39
|
"build": "node scripts/build.mjs",
|
|
40
|
-
"typecheck": "node --check src/index.js && node --check src/svg-image.js && node --check test/svg-image.mjs && node --check test/native-svg-decks.mjs && node --check src/chart-types.js && node --check src/chartex.js && node --check test/chartex.mjs && node --check test/chart-text-size.mjs && node --check test/chart-types.mjs && node --check test/design-alignment.mjs && node --check src/color-ref.js && node --check src/theme-colors.js && node --check test/theme-colors.mjs && node --check test/tag-colour.mjs && node --check test/table-theme-colors.mjs && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && node --check src/slide-image-provenance.js && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check src/watermark-provenance.js && node --check src/logo-provenance.js && node --check test/design-fields.mjs && node --check src/media-dedupe.js && node --check test/media-dedupe.mjs && node --check test/watermark.mjs && node --check test/image-fit.mjs && node --check test/image-orientation.mjs && node --check test/table-layout.mjs && node --check test/table-import.mjs && node --check test/object-ids.mjs && node --check test/export-corpus.mjs && node --check test/image-media-type.mjs && node --check src/image-fallback-node.js && node --check src/image-fallback-browser.js && node --check test/webp-fallback.mjs && node --check scripts/build-browser-check.mjs && node --check test/webp-fallback-browser.js && node --check test/image-fallback-boundary.mjs && node --check src/background.js && node --check test/background.mjs && node --check src/image-import.js && node --check test/image-import.mjs && node --check test/native-background.mjs && node --check src/background-import.js && node --check test/background-inheritance.mjs && node --check test/rich-table.mjs && node --check src/table-import.js && node --check test/rich-table-import.mjs && node --check test/rich-table-import-browser.js && node --check test/table-row-sizing.mjs && node --check test/table-styles.mjs && node --check test/table-styles-browser.js && node --check test/styled-table.mjs && node --check src/table-cell-import.js && node --check test/styled-table-import.mjs && node --check test/styled-table-import-browser.js && node --check src/table-border-import.js && node --check test/table-border-styles.mjs && node --check src/card-provenance.js && node --check test/content-cards.mjs && node --check src/chart-workbook.js && node --check test/chart-colors.mjs && node --check test/chart-workbook.mjs && node --check test/single-column-chart.mjs && node --check test/native-chart-colors.mjs && node --check src/heading-provenance.js && node --check test/accepted-text.mjs && node --check test/heading-provenance.mjs && node --check src/text-provenance.js && node --check test/plain-whitespace.mjs && node --check src/timeline-provenance.js && node --check src/quote-provenance.js && node --check test/quote-provenance.mjs && node --check test/shared-timeline.mjs && node --check test/shared-furniture.mjs && node --check src/furniture-provenance.js && node --check src/document-provenance.js && node --check src/content-topology.js && node --check src/sections.js && node --check test/document-provenance.mjs && node --check test/content-topology.mjs && node --check test/sections.mjs && node --check test/provenance-interop.mjs && node --check test/furniture-provenance.mjs && node --check src/furniture-fields.js && node --check test/furniture-fields.mjs && node --check src/native-furniture.js && node --check test/native-furniture.mjs && node --check test/furniture-wrapped-date.mjs && node --check test/native-picture.mjs && node --check test/compare-native-picture.mjs && node --check test/compare-native-picture-edit.mjs && node --check test/compare-native-picture-edit-controls.mjs && node --check test/native-font-embed-audit.mjs && node --check test/native-font-embed-controls.mjs && node --check test/native-font-embed-fixture-source.mjs && node --check test/native-font-edit-fixture.mjs && node --check test/native-mixed-edit-audit.mjs && node --check test/native-mixed-edit-controls.mjs && node --check test/master-bullet-font.mjs && node --check test/native-font-inventory-audit.mjs && node --check test/native-font-inventory-controls.mjs && node --check test/default-font-scheme.mjs && node --check test/export-chosen-fonts.mjs && node --check test/background-fills.mjs && node --check src/typeface-inventory.js && node --check src/package-fonts.js && node --check test/typeface-inventory.mjs && node --check src/script-fonts.js && node --check test/script-fonts.mjs && node --check test/theme-script-slots.mjs && node --check test/powershell-scan.mjs && node --check test/powershell-scan-probes.mjs && node --check test/socials-furniture.mjs && node --check src/media-provenance.js && node --check test/layout-parity.mjs && node --check test/cover-centering.mjs && node --check test/media-placeholder.mjs && node --check test/image-placeholder.mjs && node --check test/svg-image-degrade.mjs && node --check test/background-colorref.mjs && node --check test/import-fabrications.mjs && node --check test/list-shape-names.mjs && node --check test/layout-intent.mjs && node --check test/chart-cache-import.mjs && node --check test/zip-date.mjs && node --check test/native-text-whitespace.mjs && node --check test/native-underline.mjs && node --check src/native-text-style.js && node --check src/font-weights.js && node --check test/font-weight-import.mjs && node --check src/body-text-import.js && node --check test/native-body-rich-text.mjs && node --check test/native-body-rich-text-browser.js && node --check test/native-quote-import-contract.mjs && node --check test/native-quote-import-controls.mjs && node --check test/native-quote.mjs && node --check test/determinism.mjs && node --check test/determinism-fixtures.mjs && node --check test/table-frame-furniture-order.mjs && node --check test/shape-border-widths.mjs && node --check test/gallery-layout-alignment.mjs && node --check src/import-signals.js && node --check test/import-signals.mjs && node --check test/signals-deck.mjs",
|
|
41
|
-
"test": "npm run build && node test/chart-types.mjs && node test/chartex.mjs && node test/chart-text-size.mjs && node test/zip-date.mjs && node test/background-fills.mjs && npm run test:furniture && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/watermark.mjs && node test/design-fields.mjs && node test/media-dedupe.mjs && node test/document-provenance.mjs && node test/content-topology.mjs && node test/sections.mjs && node test/layout-intent.mjs && node test/master-bullet-font.mjs && node test/default-font-scheme.mjs && node test/script-fonts.mjs && node test/theme-script-slots.mjs && node test/export-chosen-fonts.mjs && node test/typeface-inventory.mjs && node test/color-ref-export.mjs && node test/theme-colors.mjs && node test/tag-colour.mjs && node test/table-theme-colors.mjs && node test/plain-whitespace.mjs && node test/native-text-whitespace.mjs && node test/native-body-rich-text.mjs && node test/native-underline.mjs && node test/font-weight-import.mjs && node test/design-alignment.mjs && node test/accepted-text.mjs && node test/heading-provenance.mjs && node test/native-quote-import-controls.mjs && node test/chart-colors.mjs && node test/content-cards.mjs && node test/chart-workbook.mjs && node test/single-column-chart.mjs && node --test test/chart-cache-import.mjs && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/layout-parity.mjs && node test/gallery-layout-alignment.mjs && node test/cover-centering.mjs && node test/table-frame-furniture-order.mjs && node test/media-placeholder.mjs && node test/shape-border-widths.mjs && node test/image-placeholder.mjs && node test/svg-image-degrade.mjs && node test/svg-image.mjs && node test/background-colorref.mjs && node test/import-fabrications.mjs && node test/list-shape-names.mjs && node test/shared-quote.mjs && node test/quote-provenance.mjs && node test/import-signals.mjs && npm run test:timeline && npm run test:determinism",
|
|
40
|
+
"typecheck": "node --check src/index.js && node --check src/annotation-export.js && node --check src/annotation-provenance.js && node --check test/annotations.mjs && node --check test/region-reading-order.mjs && node --check test/symbol-fonts.mjs && node --check test/math-emoji.mjs && node --check src/svg-image.js && node --check test/svg-image.mjs && node --check test/native-svg-decks.mjs && node --check src/chart-types.js && node --check src/chartex.js && node --check src/chart-options.js && node --check test/chart-options.mjs && node --check test/chartex.mjs && node --check test/chart-text-size.mjs && node --check test/chart-types.mjs && node --check test/design-alignment.mjs && node --check src/color-ref.js && node --check src/theme-colors.js && node --check test/theme-colors.mjs && node --check test/tag-colour.mjs && node --check test/table-theme-colors.mjs && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && node --check src/slide-image-provenance.js && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check src/watermark-provenance.js && node --check src/logo-provenance.js && node --check test/design-fields.mjs && node --check src/media-dedupe.js && node --check test/media-dedupe.mjs && node --check test/watermark.mjs && node --check test/image-fit.mjs && node --check test/image-orientation.mjs && node --check test/table-layout.mjs && node --check test/table-import.mjs && node --check test/object-ids.mjs && node --check test/export-corpus.mjs && node --check test/image-media-type.mjs && node --check src/image-fallback-node.js && node --check src/image-fallback-browser.js && node --check test/webp-fallback.mjs && node --check scripts/build-browser-check.mjs && node --check test/webp-fallback-browser.js && node --check test/image-fallback-boundary.mjs && node --check src/background.js && node --check test/background.mjs && node --check src/image-import.js && node --check test/image-import.mjs && node --check test/native-background.mjs && node --check src/background-import.js && node --check test/background-inheritance.mjs && node --check test/rich-table.mjs && node --check src/table-import.js && node --check test/rich-table-import.mjs && node --check test/rich-table-import-browser.js && node --check test/table-row-sizing.mjs && node --check test/table-styles.mjs && node --check test/table-styles-browser.js && node --check test/styled-table.mjs && node --check src/table-cell-import.js && node --check test/styled-table-import.mjs && node --check test/styled-table-import-browser.js && node --check src/table-border-import.js && node --check test/table-border-styles.mjs && node --check src/card-provenance.js && node --check test/content-cards.mjs && node --check src/chart-workbook.js && node --check test/chart-colors.mjs && node --check test/single-series-colour.mjs && node --check test/chart-workbook.mjs && node --check test/single-column-chart.mjs && node --check test/native-chart-colors.mjs && node --check src/heading-provenance.js && node --check test/accepted-text.mjs && node --check test/heading-provenance.mjs && node --check src/text-provenance.js && node --check test/plain-whitespace.mjs && node --check src/timeline-provenance.js && node --check src/quote-provenance.js && node --check test/quote-provenance.mjs && node --check test/shared-timeline.mjs && node --check test/shared-furniture.mjs && node --check src/furniture-provenance.js && node --check src/document-provenance.js && node --check src/content-topology.js && node --check src/sections.js && node --check test/document-provenance.mjs && node --check test/content-topology.mjs && node --check test/sections.mjs && node --check test/provenance-interop.mjs && node --check test/furniture-provenance.mjs && node --check src/furniture-fields.js && node --check test/furniture-fields.mjs && node --check src/native-furniture.js && node --check test/native-furniture.mjs && node --check test/furniture-wrapped-date.mjs && node --check test/native-picture.mjs && node --check test/compare-native-picture.mjs && node --check test/compare-native-picture-edit.mjs && node --check test/compare-native-picture-edit-controls.mjs && node --check test/native-font-embed-audit.mjs && node --check test/native-font-embed-controls.mjs && node --check test/native-font-embed-fixture-source.mjs && node --check test/native-font-edit-fixture.mjs && node --check test/native-mixed-edit-audit.mjs && node --check test/native-mixed-edit-controls.mjs && node --check test/master-bullet-font.mjs && node --check test/native-font-inventory-audit.mjs && node --check test/native-font-inventory-controls.mjs && node --check test/default-font-scheme.mjs && node --check test/export-chosen-fonts.mjs && node --check test/background-fills.mjs && node --check src/typeface-inventory.js && node --check src/package-fonts.js && node --check test/typeface-inventory.mjs && node --check src/script-fonts.js && node --check test/script-fonts.mjs && node --check test/rtl-layout.mjs && node --check test/theme-script-slots.mjs && node --check test/powershell-scan.mjs && node --check test/powershell-scan-probes.mjs && node --check test/socials-furniture.mjs && node --check src/media-provenance.js && node --check test/layout-parity.mjs && node --check test/cover-centering.mjs && node --check test/media-placeholder.mjs && node --check test/image-placeholder.mjs && node --check test/svg-image-degrade.mjs && node --check test/background-colorref.mjs && node --check test/import-fabrications.mjs && node --check src/run-colors.js && node --check test/hard-line-breaks.mjs && node --check test/import-details.mjs && node --check test/list-shape-names.mjs && node --check src/numbered-list.js && node --check test/numbered-lists.mjs && node --check test/layout-intent.mjs && node --check test/chart-cache-import.mjs && node --check test/zip-date.mjs && node --check test/native-text-whitespace.mjs && node --check test/native-underline.mjs && node --check src/native-text-style.js && node --check src/font-weights.js && node --check test/font-weight-import.mjs && node --check src/body-text-import.js && node --check test/native-body-rich-text.mjs && node --check test/native-body-rich-text-browser.js && node --check test/native-quote-import-contract.mjs && node --check test/native-quote-import-controls.mjs && node --check test/native-quote.mjs && node --check test/determinism.mjs && node --check test/determinism-fixtures.mjs && node --check test/table-frame-furniture-order.mjs && node --check test/shape-border-widths.mjs && node --check test/gallery-layout-alignment.mjs && node --check src/import-signals.js && node --check test/import-signals.mjs && node --check test/signals-deck.mjs && node --check test/rr-07-preview-polish.mjs && node --check scripts/rr07-native-set.mjs",
|
|
41
|
+
"test": "npm run build && node test/annotations.mjs && node test/region-reading-order.mjs && node test/symbol-fonts.mjs && node test/math-emoji.mjs && node test/chart-types.mjs && node test/chartex.mjs && node test/chart-options.mjs && node test/chart-text-size.mjs && node test/zip-date.mjs && node test/background-fills.mjs && node test/rr-07-preview-polish.mjs && npm run test:furniture && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/watermark.mjs && node test/design-fields.mjs && node test/media-dedupe.mjs && node test/document-provenance.mjs && node test/content-topology.mjs && node test/sections.mjs && node test/layout-intent.mjs && node test/master-bullet-font.mjs && node test/default-font-scheme.mjs && node test/script-fonts.mjs && node test/rtl-layout.mjs && node test/theme-script-slots.mjs && node test/export-chosen-fonts.mjs && node test/typeface-inventory.mjs && node test/color-ref-export.mjs && node test/theme-colors.mjs && node test/tag-colour.mjs && node test/table-theme-colors.mjs && node test/plain-whitespace.mjs && node test/native-text-whitespace.mjs && node test/native-body-rich-text.mjs && node test/native-underline.mjs && node test/font-weight-import.mjs && node test/design-alignment.mjs && node test/accepted-text.mjs && node test/heading-provenance.mjs && node test/native-quote-import-controls.mjs && node test/chart-colors.mjs && node test/single-series-colour.mjs && node test/content-cards.mjs && node test/chart-workbook.mjs && node test/single-column-chart.mjs && node --test test/chart-cache-import.mjs && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/layout-parity.mjs && node test/gallery-layout-alignment.mjs && node test/cover-centering.mjs && node test/table-frame-furniture-order.mjs && node test/media-placeholder.mjs && node test/shape-border-widths.mjs && node test/image-placeholder.mjs && node test/svg-image-degrade.mjs && node test/svg-image.mjs && node test/background-colorref.mjs && node test/import-fabrications.mjs && node test/hard-line-breaks.mjs && node test/import-details.mjs && node test/list-shape-names.mjs && node test/numbered-lists.mjs && node test/shared-quote.mjs && node test/quote-provenance.mjs && node test/import-signals.mjs && npm run test:timeline && npm run test:determinism && node test/template-variables.mjs",
|
|
42
42
|
"validate": "node scripts/validate-package.mjs",
|
|
43
43
|
"test:packed": "node test/packed-install.mjs && node test/provenance-interop.mjs",
|
|
44
44
|
"test:compare-published": "node test/compare-published.mjs",
|
|
@@ -61,14 +61,14 @@
|
|
|
61
61
|
"test:determinism": "node test/determinism.mjs"
|
|
62
62
|
},
|
|
63
63
|
"dependencies": {
|
|
64
|
-
"@openpresentation/opf": "^0.
|
|
64
|
+
"@openpresentation/opf": "^0.12.0",
|
|
65
65
|
"fast-xml-parser": "^5.8.0",
|
|
66
66
|
"fflate": "^0.8.3",
|
|
67
67
|
"jszip": "3.10.2",
|
|
68
68
|
"sharp": "0.35.4"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@openpresentation/opf-render": "^0.
|
|
71
|
+
"@openpresentation/opf-render": "^0.12.0"
|
|
72
72
|
},
|
|
73
73
|
"peerDependenciesMeta": {
|
|
74
74
|
"@openpresentation/opf-render": {
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@expo-google-fonts/carlito": "0.4.1",
|
|
80
|
-
"@openpresentation/opf-render": "0.
|
|
80
|
+
"@openpresentation/opf-render": "0.12.0",
|
|
81
81
|
"esbuild": "0.28.2",
|
|
82
82
|
"playwright": "1.63.0"
|
|
83
83
|
},
|