@openpresentation/opf-pptx 0.26.10 → 0.28.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/DEPENDENCY-NOTES.md +1 -1
- package/dist/content-topology.js +3 -1
- package/dist/index.js +23 -14
- package/dist/native-body.js +1 -1
- package/dist/quote-provenance.js +10 -6
- package/dist/rich-heading.js +4 -3
- package/package.json +8 -8
package/DEPENDENCY-NOTES.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
PPTX 0.11.4 depends on published [`@openpresentation/opf@^0.11.3`](https://www.npmjs.com/package/@openpresentation/opf) (reference layer: ColorRef, document `variables`, exported `resolveColorRef()`). The optional renderer peer is [`@openpresentation/opf-render@^0.11.0`](https://www.npmjs.com/package/@openpresentation/opf-render) so editor 0.10.0 + render 0.11.0 + pptx 0.11.0 install together. CI checks out OPF [`b8a1faf`](https://github.com/OpenPresentation/opf/commit/b8a1faf24203635bd36b09ea19f44bcd53781f16) (`opf-v0.11.0`) for coordinated source linking. Named content colors resolve through core `resolveColorRef()`; eight-digit hex alpha stays a local export concern because core `normalizeHexColor` drops the alpha byte. The deck color scheme is written as the theme `clrScheme`, and named slot and role colors (text, table cells and borders, hyperlinks, backgrounds) are written as `schemeClr` where the deck theme holds exactly that color; card, timeline and media colors, literal hex, variables, translucent colors and slide-override colors stay literal `srgbClr`.
|
|
6
6
|
|
|
7
|
-
Since RR-17 (opf-pptx#162) the vendored engine is [pptxgenjs-plus](https://github.com/lofcz/pptxgenjs-plus) 4.3.4, the maintained MIT fork of PptxGenJS (same public API and default export): `vendor/pptxgenjs/pptxgen.es.js` is the exact, unmodified `package/dist/pptxgen.es.js` of the npm archive, with its MIT license, the archive's SHA-512 integrity, per-file SHA-256 hashes and the source commit of its npm SLSA provenance (`lofcz/pptxgenjs-plus@ecfa5086`) in `UPSTREAM.json`. Its imports are declared directly and pinned: `@node-projects/jszip` 4.3.0 (replacing `jszip` 3.10.2), `@rgrove/parse-xml`
|
|
7
|
+
Since RR-17 (opf-pptx#162) the vendored engine is [pptxgenjs-plus](https://github.com/lofcz/pptxgenjs-plus) 4.3.4, the maintained MIT fork of PptxGenJS (same public API and default export): `vendor/pptxgenjs/pptxgen.es.js` is the exact, unmodified `package/dist/pptxgen.es.js` of the npm archive, with its MIT license, the archive's SHA-512 integrity, per-file SHA-256 hashes and the source commit of its npm SLSA provenance (`lofcz/pptxgenjs-plus@ecfa5086`) in `UPSTREAM.json`. Its imports are declared directly and pinned: `@node-projects/jszip` 4.3.0 (replacing `jszip` 3.10.2), `@rgrove/parse-xml` 5.0.0 and `pako` 3.0.2. `@node-projects/jszip` declares `@types/node` as a runtime dependency (type declarations, no code), so it is installed too. opf-pptx's own package reading and writing stays on `fflate`. Every output change and how it was measured is in [docs/pptxgenjs-plus-migration.md](docs/pptxgenjs-plus-migration.md); `src/vendor-compat.js` keeps the PptxGenJS 4.0.1 output where opf-pptx's contracts need it.
|
|
8
8
|
|
|
9
9
|
Browser builds of the engine's ZIP dependency (RR-20, 0.13.1): `@node-projects/jszip` 4.3.0's `lib/support.js` probes `require("readable-stream")` inside try/catch. Node ESM has no `require`, so the probe always fails there, but bundlers follow the literal: when the consumer's tree holds readable-stream 2 (for example through jszip 3, as in opf-editor's packed consumer), esbuild bundles it and fails on `events` and `buffer`, and Turbopack pulls it in with Node polyfills. `vendor/jszip/index-min.js` is the exact, unmodified `package/lib/index-min.js` of the pinned npm archive (the package's own prepublish bundle of `lib/`, which esbuild 0.28.2 reproduces byte for byte; it calls the probe through a wrapper bundlers do not follow), with its license (`LICENSE.md`, MIT or GPL-3.0-or-later; opf-pptx uses it under MIT), the archive's SHA-512 integrity and per-file SHA-256 hashes in `UPSTREAM.json`. The browser field of `vendor/pptxgenjs/package.json` maps the engine's `@node-projects/jszip` import to it (esbuild, webpack and Turbopack honour it); Node ignores the browser field and keeps the installed package. Every build (`scripts/verify-vendor.mjs`) requires the vendored version to equal the pinned dependency and the lockfile integrity, and its bytes to equal the installed package's bundle, so a dependency bump without re-vendoring fails. Refresh it by copying `node_modules/@node-projects/jszip/lib/index-min.js` and `LICENSE.md` after `npm ci` and updating `UPSTREAM.json`. `test/zip-bundle-boundary.mjs` (offline) and the browser suite `test/packed-bundle-browser.mjs` (packed tarball installed next to readable-stream, bundled with esbuild, the core examples exported in Chromium byte-identical to Node) guard it.
|
|
10
10
|
|
package/dist/content-topology.js
CHANGED
|
@@ -375,7 +375,9 @@ export function listLineRoles(items, omit = () => {}) {
|
|
|
375
375
|
const lists = [];
|
|
376
376
|
for (const item of items ?? []) {
|
|
377
377
|
if (!['items', 'bullets'].includes(item?.field) || !Array.isArray(item?.text?.listEntries)) continue;
|
|
378
|
-
|
|
378
|
+
// A list without markers (design.listBullet: none) records its roles too: without a bullet, the first line of an entry is
|
|
379
|
+
// only known from this record.
|
|
380
|
+
if (!item.text.listEntries.some(entry => entry.description?.richLines?.length || entry.marker?.none)) continue;
|
|
379
381
|
const lines = [];
|
|
380
382
|
item.text.listEntries.forEach((entry, index) => {
|
|
381
383
|
for (const [role, text] of [['text', entry.text], ['description', entry.description]])
|
package/dist/index.js
CHANGED
|
@@ -52,8 +52,8 @@ import { prepareSvg, svgDataUriBytes, svgRasterScale, svgBlipRelationship, attac
|
|
|
52
52
|
import { chartPaletteSchemes, applyChartSchemeColors } from './chart-scheme-colors.js';
|
|
53
53
|
import { rasterMetadata, pictureTransform, normalizeImageOrientation } from './image-geometry.js';
|
|
54
54
|
import {
|
|
55
|
-
catalogRecords, clrMapAttributes, normalizeColorVariant, chartPaletteForFill, chartSeriesPalette, chartTextSize as coreChartTextSize, codeHighlightBands, codeHighlightColors, codeHighlightLines, codeSyntaxPaletteForRoles, readableSchemeColor, codeLineNumbers, codeLineRuns, codeSyntaxPaletteForScheme, composeSlide,
|
|
56
|
-
ENGINE_DEFAULT_FONT_SCHEME, defaultSlideBackground, fitImage, fitText, layoutTable, layoutWatermark, metricTrendMark, resolveCanvasDimensions, resolveColorRoles, resolveFontFamilies,
|
|
55
|
+
catalogRecords, clrMapAttributes, normalizeColorVariant, chartLeadFromScheme, chartPaletteForFill, chartSeriesPalette, chartTextSize as coreChartTextSize, codeHighlightBands, codeHighlightColors, codeHighlightLines, codeSyntaxPaletteForRoles, readableSchemeColor, codeLineNumbers, codeLineRuns, codeSyntaxPaletteForScheme, composeSlide,
|
|
56
|
+
ENGINE_DEFAULT_FONT_SCHEME, defaultSlideBackground, fitImage, fitText, layoutTable, layoutWatermark, metricTrendMark, metricValueColor, resolveCanvasDimensions, resolveColorRoles, resolveFontFamilies,
|
|
57
57
|
resolveChartColors, resolveReference, resolveScriptFonts, resolveTextStyle, textColorForFill, textWidthMeasurer, timelineMarkerShapes, timelineTextColor, tokenizeCode
|
|
58
58
|
} from "@openpresentation/opf/composition";
|
|
59
59
|
import { colorContext, resolveColorRefValue, resolveExportColor, resolveVariableColors } from "./color-ref.js";
|
|
@@ -1480,11 +1480,15 @@ function mergeOpfListShapes(items, breaks = null, report = () => {}, runs = null
|
|
|
1480
1480
|
const merged = new Map(), consumed = new Set();
|
|
1481
1481
|
for (const [path, entries] of lists) {
|
|
1482
1482
|
entries.sort((a, b) => a.line - b.line);
|
|
1483
|
-
if (!entries[0].item.paragraphs[0].bullet) continue;
|
|
1484
1483
|
const expectedRoles = roles?.get(path);
|
|
1485
|
-
|
|
1484
|
+
// design.listBullet: none writes no bullet at all (a:buNone): the list is unambiguous only through its recorded roles, whose
|
|
1485
|
+
// first lines start the entries. Without that record the shapes stay as they are.
|
|
1486
|
+
const unbulleted = entries.every(({item}) => !item.paragraphs[0].bullet);
|
|
1487
|
+
if (!entries[0].item.paragraphs[0].bullet && !(unbulleted && expectedRoles?.[0]?.[2])) continue;
|
|
1488
|
+
const validRoles = expectedRoles && expectedRoles.length === entries.length && entries.every(({item, line}, index) => line === index && (unbulleted || Boolean(item.paragraphs[0].bullet) === expectedRoles[index][2]));
|
|
1486
1489
|
const lineRoles = validRoles ? expectedRoles : null;
|
|
1487
1490
|
if (expectedRoles && !validRoles) report({code: 'list-line-role-changed', message: `The line topology of ${path} changed; list roles were not restored.`});
|
|
1491
|
+
if (unbulleted && !validRoles) continue;
|
|
1488
1492
|
const look = runs?.get(path), textLook = cleanBase(look?.text), descriptionLook = cleanBase(look?.description);
|
|
1489
1493
|
// The line's runs without the inherited look, or the paragraph's own richText when there is no record for this kind of line.
|
|
1490
1494
|
// Each run is cleaned on its own and none merge, so the runs stay the slots the run colour record counts (src/run-colors.js);
|
|
@@ -1512,7 +1516,7 @@ function mergeOpfListShapes(items, breaks = null, report = () => {}, runs = null
|
|
|
1512
1516
|
const [paragraph] = item.paragraphs, current = paragraphs.at(-1);
|
|
1513
1517
|
const gap = recorded?.get(line);
|
|
1514
1518
|
const join = (a, b) => gap === undefined ? joinRich(a, b) : joinWrappedText([a, b], [gap]);
|
|
1515
|
-
if (paragraph.bullet) { paragraphs.push({...paragraph, richText: own(paragraph, textLook)}); size = paragraph.maxFontSize; }
|
|
1519
|
+
if (unbulleted ? lineRoles?.[line][2] : paragraph.bullet) { paragraphs.push({...paragraph, bullet: true, richText: own(paragraph, textLook)}); size = paragraph.maxFontSize; }
|
|
1516
1520
|
else if (paragraph.text === '') { /* a blank line of the entry: no text, no description */ }
|
|
1517
1521
|
else if (lineRoles ? lineRoles[line][1] === 'description' : paragraph.maxFontSize < size - 0.01) {
|
|
1518
1522
|
const text = own(paragraph, descriptionLook) ?? paragraph.text;
|
|
@@ -2554,7 +2558,9 @@ async function addMeasuredList(slide,fit,context,path,bulletImage,presentation,s
|
|
|
2554
2558
|
for(const entry of fit.listEntries){
|
|
2555
2559
|
const addLines=(text,box,color,native,withBullet)=>{
|
|
2556
2560
|
text.richLines.forEach((line,index)=>{
|
|
2557
|
-
|
|
2561
|
+
// design.listBullet: none (entry.marker.none): the first line has no marker, no inset and no hanging indent (a:buNone); core
|
|
2562
|
+
// already moved a nested level's text box, so the paragraph carries no indent level.
|
|
2563
|
+
const first=withBullet&&index===0,markerless=first&&entry.marker.none===true,level=Math.min(8,entry.level),inset=first&&!markerless?entry.marker.indent*(level+1):0;
|
|
2558
2564
|
// Right to left (RR-05): the bullet column is at the right, so the first line's box extends right to hold it and the paragraph
|
|
2559
2565
|
// is rtl with its text aligned to the right; marL/indent are the start-side margin and hanging indent in PowerPoint.
|
|
2560
2566
|
const entryRtl=entry.direction==='rtl';
|
|
@@ -2563,8 +2569,8 @@ async function addMeasuredList(slide,fit,context,path,bulletImage,presentation,s
|
|
|
2563
2569
|
const lineAlign=physicalAlignment('left',lineDirection);
|
|
2564
2570
|
const objectName=`OPF list ${path} line ${lineNumber++}`;
|
|
2565
2571
|
const number=first?entry.marker.number:undefined;
|
|
2566
|
-
if(first)context.listMarkers.set(objectName,{fontFamily:entry.marker.style.fontFamily,fontSize:entry.marker.fontSize*.75,color:pptxColor(context.textColor),picture:number?false:picture});
|
|
2567
|
-
const paragraph=first?{bullet:number?{type:'number',style:autoNumScheme(number.style,number.suffix),startAt:number.value,indent:entry.marker.indent*.75}:{characterCode:entry.marker.text.codePointAt(0).toString(16).padStart(4,'0'),indent:entry.marker.indent*.75},indentLevel:level}:{bullet:false};
|
|
2572
|
+
if(first&&!markerless)context.listMarkers.set(objectName,{fontFamily:entry.marker.style.fontFamily,fontSize:entry.marker.fontSize*.75,color:pptxColor(context.textColor),picture:number?false:picture});
|
|
2573
|
+
const paragraph=first&&!markerless?{bullet:number?{type:'number',style:autoNumScheme(number.style,number.suffix),startAt:number.value,indent:entry.marker.indent*.75}:{characterCode:entry.marker.text.codePointAt(0).toString(16).padStart(4,'0'),indent:entry.marker.indent*.75},indentLevel:level}:{bullet:false};
|
|
2568
2574
|
const runs=richLineRuns(line,color,native,context,path);
|
|
2569
2575
|
if(!runs.length)runs.push({text:'',options:{}});
|
|
2570
2576
|
// Keep paragraph intent identical across runs. ZIP normalization below
|
|
@@ -2928,7 +2934,8 @@ function addChartPayload(slide, chart, region, context, options = {}, path = "ch
|
|
|
2928
2934
|
// `chart.colors` scheme slot or role) is written as a schemeClr, so re-theming in PowerPoint recolours the series.
|
|
2929
2935
|
const colorContext = {roles: context.roles, variables: context.variables, accents: context.roles.map?.accents};
|
|
2930
2936
|
const preferredPalette = chartSeriesPalette(context.colorScheme, chart.colors, colorContext);
|
|
2931
|
-
|
|
2937
|
+
// WI-5 (P18, D3): series 1 from the scheme reaches 4.5:1 on a dark panel, as the preview draws it; a lifted colour is written as srgbClr.
|
|
2938
|
+
const adjustedPalette = chartPaletteForFill(panelFill, preferredPalette, {legibleLead: chartLeadFromScheme(chart.colors, context.colorScheme, colorContext)});
|
|
2932
2939
|
const palette = adjustedPalette.map(color=>normalizeHex(color));
|
|
2933
2940
|
const paletteSchemes = chartPaletteSchemes(chart.colors, preferredPalette, adjustedPalette, (reference, hex) => schemeColorValue(reference, `#${hex}`, context), context.roles.map?.accents);
|
|
2934
2941
|
Object.assign(context.chartHeadings.get(objectName), {palette, paletteSchemes, circular});
|
|
@@ -3131,7 +3138,7 @@ function addTablePayload(slide, authoredTable, region, context, options, path, p
|
|
|
3131
3138
|
recordPayloadData(context, objectName, 'table', presentation, path, layout);
|
|
3132
3139
|
// FA-27: the table's text alternative (the frame's descr; the empty string is PowerPoint's decorative marker).
|
|
3133
3140
|
if (typeof authoredTable?.alt === 'string') context.frameAlts.set(objectName, authoredTable.alt);
|
|
3134
|
-
if (layout.rows.some(row => row.cells.some(cell => Object.keys(cell.style ?? {}).length))) context.tableCells.set(objectName, {layout, scale, context, defaultBorder:{color:`#${context.colors.border}`,width:1/scale}});
|
|
3141
|
+
if (layout.rows.some(row => row.cells.some(cell => Object.keys(cell.style ?? {}).length))) context.tableCells.set(objectName, {layout, scale, context, defaultBorder:{color:`#${context.colors.border}`,width:1/scale}, hiddenBorder:{color:'#000000',width:0}});
|
|
3135
3142
|
slide.addTable(rows, {
|
|
3136
3143
|
objectName,
|
|
3137
3144
|
x: region.x,
|
|
@@ -3142,12 +3149,14 @@ function addTablePayload(slide, authoredTable, region, context, options, path, p
|
|
|
3142
3149
|
// frame height is the row total so the XML matches what both engines draw.
|
|
3143
3150
|
h: layout.height / 96,
|
|
3144
3151
|
rowH: layout.rows.map(row => row.box.height / 96),
|
|
3145
|
-
|
|
3152
|
+
// WI-5 (P20): the column widths core sized from content, the same ones the preview draws (logical column order, as a:tblGrid lists them).
|
|
3153
|
+
colW: layout.columnWidths.map(width => width / 96),
|
|
3146
3154
|
autoPage: false,
|
|
3147
3155
|
fontFace: context.fonts.body,
|
|
3148
3156
|
fontSize: 15 * scale * 0.75,
|
|
3149
3157
|
color: context.colors.text,
|
|
3150
|
-
|
|
3158
|
+
// WI-5 (P20): horizontal rules only (top and bottom of every cell), as the preview draws them; an explicit cell border is written over this.
|
|
3159
|
+
border: [{ type: "solid", color: context.colors.border, pt: 0.75 }, { type: "none" }, { type: "solid", color: context.colors.border, pt: 0.75 }, { type: "none" }],
|
|
3151
3160
|
margin: [6 * scale, 7.5 * scale, 3 * scale, 7.5 * scale],
|
|
3152
3161
|
valign: "top"
|
|
3153
3162
|
});
|
|
@@ -3439,7 +3448,7 @@ function addMetricPayload(slide,value,layout,context,path,options) {
|
|
|
3439
3448
|
const group=String(context.metricTags.size+1),manifest=metricManifest(value,layout,group);
|
|
3440
3449
|
// RR-07: a trend colours the trend and delta text and adds one native arrow shape, from core's accepted geometry.
|
|
3441
3450
|
const trendMark=metricTrendMark(layout,{background:`#${normalizeHex(context.colors.background)}`,...(context.roles.neutrals?.trendNeutral ? {neutral: context.roles.neutrals.trendNeutral.hex} : {})});
|
|
3442
|
-
const partColor=part=>trendMark&&(part.role==='trend'||part.role==='delta')?trendMark.color.slice(1):part.role==='value'?context.colors.accent:context.textColor;
|
|
3451
|
+
const partColor=part=>trendMark&&(part.role==='trend'||part.role==='delta')?trendMark.color.slice(1):part.role==='value'?metricValueColor(`#${normalizeHex(context.colors.accent)}`,`#${normalizeHex(context.colors.background)}`).slice(1).toUpperCase():context.textColor;
|
|
3443
3452
|
for (const [partIndex,part] of layout.parts.entries()) {
|
|
3444
3453
|
const invalid=/[\u0000-\u0008\u000B\u000C\u000E-\u001F\uD800-\uDFFF\uFFFE\uFFFF]/u.exec(part.text);
|
|
3445
3454
|
if (invalid) throw new OPFPptxError('invalid-metric-text',`Metric text contains U+${invalid[0].codePointAt(0).toString(16).toUpperCase().padStart(4,'0')} at UTF-16 offset ${invalid.index}, which XML cannot represent; edit that character before exporting.`,{path:part.path});
|
|
@@ -4681,7 +4690,7 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
4681
4690
|
// Anchor-only styling truncates dashes and restores hidden segments.
|
|
4682
4691
|
const perimeter = {left:currentColumn === owner.column, right:currentColumn === owner.column + owner.colSpan - 1,
|
|
4683
4692
|
top:currentRow === owner.row, bottom:currentRow === owner.row + owner.rowSpan - 1};
|
|
4684
|
-
const style = cell ? {...cell.style} : {borders:Object.fromEntries(Object.keys(perimeter).map(edge => [edge,perimeter[edge] ? owner.style?.borders?.[edge] ?? table.defaultBorder : {color:'#000000',width:0}]))};
|
|
4693
|
+
const style = cell ? {...cell.style} : {borders:Object.fromEntries(Object.keys(perimeter).map(edge => [edge,perimeter[edge] ? owner.style?.borders?.[edge] ?? (edge === 'left' || edge === 'right' ? table.hiddenBorder : table.defaultBorder) : {color:'#000000',width:0}]))};
|
|
4685
4694
|
style.borders = {...style.borders};
|
|
4686
4695
|
// Native shared-edge precedence can let an implicit neighbor cover
|
|
4687
4696
|
// an explicit merge border. Give both physical sides that border.
|
package/dist/native-body.js
CHANGED
|
@@ -47,7 +47,7 @@ function planOne(item, geometry, automatic, reject) {
|
|
|
47
47
|
let values, numbering;
|
|
48
48
|
if (list) {
|
|
49
49
|
const entries = item.text?.listEntries;
|
|
50
|
-
if (item.bulletImage || !entries?.length || entries.some(entry => entry.level !== 0 || entry.description) ||
|
|
50
|
+
if (item.bulletImage || !entries?.length || entries.some(entry => entry.level !== 0 || entry.description || entry.marker?.none) ||
|
|
51
51
|
item.value.some(entry => entry && !Array.isArray(entry) && typeof entry === 'object' && Object.keys(entry).some(key => key !== 'text' && key !== 'level'))) {
|
|
52
52
|
reject('unsupported-list'); return undefined;
|
|
53
53
|
}
|
package/dist/quote-provenance.js
CHANGED
|
@@ -3,6 +3,7 @@ import {isValidFormat} from './format-check.js';
|
|
|
3
3
|
import {attachTextTags,decodeTextTag,groupUnchanged} from './code-provenance.js';
|
|
4
4
|
import {sourceLineParagraphs} from './text-provenance.js';
|
|
5
5
|
import {cleanBase,headingValue,joinRichLines,unwrapQuoteRuns} from './rich-heading.js';
|
|
6
|
+
import {unwrapQuoteMarks} from '@openpresentation/opf/composition';
|
|
6
7
|
|
|
7
8
|
// FF-57: a `quote` payload exports as native text lines (a body part and an optional footer part). Each line shape carries an
|
|
8
9
|
// OPF_QUOTE_V1 tag, so an unchanged export imports back as {quote: ...} instead of loose text blocks. Like timelines, only topology
|
|
@@ -11,10 +12,11 @@ import {cleanBase,headingValue,joinRichLines,unwrapQuoteRuns} from './rich-headi
|
|
|
11
12
|
// FA-12: a quote with a `photo` also exports one native picture (an ellipse), tagged with the same group; import restores `photo` from
|
|
12
13
|
// that picture's current bytes and description, and `role` from the footer.
|
|
13
14
|
const TAG='OPF_QUOTE_V1',REL='http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags';
|
|
14
|
-
|
|
15
|
+
// Core joins the source with an en dash (WI-4); a file exported before then holds a hyphen, which import still reads.
|
|
16
|
+
const SEPARATOR=' \u2013 ',LEGACY_SEPARATOR=' - ',FIELDS=['attribution','role','source'];
|
|
15
17
|
// Every non-empty footer field list, in core's order (attribution, role, source).
|
|
16
18
|
const FOOTERS=['["attribution"]','["role"]','["source"]','["attribution","role"]','["attribution","source"]','["role","source"]','["attribution","role","source"]'];
|
|
17
|
-
// Core joins the role to the line above with a line break and the source with "
|
|
19
|
+
// Core joins the role to the line above with a line break and the source with " \u2013 ".
|
|
18
20
|
const separatorBefore=field=>field==='role'?'\n':SEPARATOR;
|
|
19
21
|
const parser=new XMLParser({ignoreAttributes:false,attributeNamePrefix:'',parseTagValue:false,trimValues:false});
|
|
20
22
|
const decoder=new TextDecoder('utf-8',{fatal:true}),array=value=>value===undefined?[]:Array.isArray(value)?value:[value];
|
|
@@ -22,7 +24,7 @@ export const attachQuoteTags=(entries,records)=>attachTextTags(entries,records,T
|
|
|
22
24
|
export const quotePhotoName=group=>`OPF quote photo ${group}`;
|
|
23
25
|
|
|
24
26
|
// The manifest rides on the first body line. `footer` lists which fields the footer part joins (core writes the attribution, the role
|
|
25
|
-
// on its own line and the source after "
|
|
27
|
+
// on its own line and the source after " \u2013 ", skipping an absent field) and, from two fields on, where each separator starts, so an
|
|
26
28
|
// unchanged footer splits back exactly. `photo` records that a headshot picture belongs to the quote.
|
|
27
29
|
export function quoteManifest(value,layout) {
|
|
28
30
|
const shorthand=typeof value==='string',fields=shorthand?[]:FIELDS.filter(field=>value[field]);
|
|
@@ -55,7 +57,7 @@ function validateManifest(manifest) {
|
|
|
55
57
|
return count;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
const unwrap=
|
|
60
|
+
const unwrap=unwrapQuoteMarks;
|
|
59
61
|
// Split the current footer text back into fields. A separator whose recorded position no longer matches is searched for from the end of
|
|
60
62
|
// the previous field; a footer with a missing separator keeps everything as the attribution.
|
|
61
63
|
function footerFields(text,footer,report) {
|
|
@@ -64,8 +66,10 @@ function footerFields(text,footer,report) {
|
|
|
64
66
|
const out={};let from=0;
|
|
65
67
|
for(let index=1;index<fields.length;index++){
|
|
66
68
|
const separator=separatorBefore(fields[index]),recorded=footer.splits[index-1];
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
+
const legacy=separator===SEPARATOR?LEGACY_SEPARATOR:separator,both=[separator,legacy];
|
|
70
|
+
const found=both.find(candidate=>recorded>=from&&text.startsWith(candidate,recorded));
|
|
71
|
+
const at=found!==undefined?recorded:Math.min(...both.map(candidate=>text.indexOf(candidate,from)).filter(position=>position>=0),Infinity);
|
|
72
|
+
if(at===Infinity){
|
|
69
73
|
report({code:'quote-footer-merged',message:'The quote footer no longer separates its attribution, role and source; the current text is kept as the attribution.'});
|
|
70
74
|
return {attribution:text};
|
|
71
75
|
}
|
package/dist/rich-heading.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// are joined with the boundaries their tags record; soft wraps insert nothing and hard boundaries keep their separator.
|
|
8
8
|
|
|
9
9
|
import {splitWeightFace} from './font-weights.js';
|
|
10
|
+
import {isWrappedInQuoteMarks, unwrapQuoteMarks} from '@openpresentation/opf/composition';
|
|
10
11
|
|
|
11
12
|
const object = value => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
12
13
|
// The run properties that describe how a heading looks. `link` and `text` are never defaults.
|
|
@@ -88,11 +89,11 @@ export function headingValue(parts, base) {
|
|
|
88
89
|
return merged.map(run => Object.keys(run).length === 1 ? run.text : run);
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
/** Remove the opening and closing quotation mark an exported quote body carries (core joins them to the first and last run). */
|
|
92
|
+
/** Remove the opening and closing quotation mark an exported quote body carries (core joins them to the first and last run; the pair follows the deck language, or is the older ASCII pair). */
|
|
92
93
|
export function unwrapQuoteRuns(value) {
|
|
93
|
-
if (!Array.isArray(value)) return typeof value === 'string'
|
|
94
|
+
if (!Array.isArray(value)) return typeof value === 'string' ? unwrapQuoteMarks(value) : value;
|
|
94
95
|
const text = run => typeof run === 'string' ? run : run.text;
|
|
95
|
-
if (!value.length || !text(value[0])
|
|
96
|
+
if (!value.length || !isWrappedInQuoteMarks(text(value[0])[0] + text(value.at(-1)).at(-1)) || (value.length === 1 && text(value[0]).length < 2)) return value;
|
|
96
97
|
const runs = value.map(run => typeof run === 'string' ? run : {...run});
|
|
97
98
|
const edit = (index, change) => {
|
|
98
99
|
const run = runs[index];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-pptx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
4
4
|
"description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
"sideEffects": false,
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@node-projects/jszip": "4.3.0",
|
|
41
|
-
"@rgrove/parse-xml": "
|
|
41
|
+
"@rgrove/parse-xml": "5.0.0",
|
|
42
42
|
"fast-xml-parser": "^5.8.0",
|
|
43
43
|
"fflate": "^0.8.3",
|
|
44
44
|
"pako": "3.0.2",
|
|
45
45
|
"sharp": "0.35.5",
|
|
46
|
-
"@openpresentation/gallery": "^0.
|
|
47
|
-
"@openpresentation/opf": "^0.
|
|
46
|
+
"@openpresentation/gallery": "^0.28.0",
|
|
47
|
+
"@openpresentation/opf": "^0.28.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@openpresentation/opf-render": "^0.
|
|
50
|
+
"@openpresentation/opf-render": "^0.28.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependenciesMeta": {
|
|
53
53
|
"@openpresentation/opf-render": {
|
|
@@ -73,8 +73,8 @@
|
|
|
73
73
|
"@expo-google-fonts/tinos": "0.4.2",
|
|
74
74
|
"@resvg/resvg-js": "2.6.2",
|
|
75
75
|
"esbuild": "0.28.2",
|
|
76
|
-
"playwright": "1.64.
|
|
77
|
-
"@openpresentation/opf-render": "0.
|
|
76
|
+
"playwright": "1.64.1",
|
|
77
|
+
"@openpresentation/opf-render": "0.28.0"
|
|
78
78
|
},
|
|
79
79
|
"imports": {
|
|
80
80
|
"#image-fallback": {
|
|
@@ -109,5 +109,5 @@
|
|
|
109
109
|
"test:native-text": "node test/native-text-whitespace.mjs",
|
|
110
110
|
"test:determinism": "node test/determinism.mjs"
|
|
111
111
|
},
|
|
112
|
-
"gitHead": "
|
|
112
|
+
"gitHead": "abc56634a584db38a305a27edc3bc76b4116637b"
|
|
113
113
|
}
|