@openpresentation/opf-pptx 0.3.0 → 0.5.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 +33 -9
- package/dist/background-import.js +14 -8
- package/dist/index.d.ts +1 -1
- package/dist/index.js +88 -41
- package/dist/table-border-import.js +73 -0
- package/dist/table-cell-import.js +138 -0
- package/dist/table-import.js +212 -0
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -67,7 +67,9 @@ The first importer is mechanical and schema-compatible:
|
|
|
67
67
|
- Remaining text boxes map to `blocks[]` as text or list payloads, sorted by OOXML position.
|
|
68
68
|
- PowerPoint tables map to OPF table blocks, embedded images map to data URI image blocks, and cached chart series map to basic OPF chart blocks.
|
|
69
69
|
- Table imports retain empty rows. A native `firstRow` flag of `1` or `true` maps the first row to column labels; absent/false flags retain every row as data. New exports set this flag from OPF columns. Older exports without the flag retain their labels as the first data row rather than inferring headers.
|
|
70
|
-
-
|
|
70
|
+
- Native table text preserves run/field/break order, significant whitespace, and blank paragraphs. Cells return canonical `{value, style}` objects; `value` retains scalar text or supported rich runs. Covered merge positions are `null`. Explicit normal headers override OPF’s bold header default. Numeric/boolean/null source types cannot be reconstructed from native display text.
|
|
71
|
+
- Imported runs retain bold, italic, underline, strike, point sizes, Latin font families, solid colors/alpha, external hyperlink URLs, and superscript/subscript direction. List-level and paragraph defaults apply before run overrides; supported theme fonts/colors resolve from the archive. Field values become their cached text, and underline/strike variants and baseline magnitudes reduce to OPF booleans.
|
|
72
|
+
- Conditional table styles, merged-cell geometry, cell fills/borders/alignment, unsupported text fills/colors, and internal hyperlink actions are not fully reconstructed. `onDiagnostic` reports unsupported table style references, merges, fonts/colors/fills and links with native frame/cell paths. Table paths use the native graphic-frame and row indexes, including a header row. The shared core 0.6.0 layout sizes rows from their content and reports `text-overflow` when text cannot fit at the minimum size. These checks establish native XML conversion, not visual parity with PowerPoint.
|
|
71
73
|
- Unknown non-text shapes and unsupported graphic frames become editable text fallback blocks instead of failing the import.
|
|
72
74
|
|
|
73
75
|
There is no AI classification pass in the OSS runtime. Hosts can run optional cleanup or semantic remapping after `fromPptx` returns.
|
|
@@ -109,11 +111,11 @@ Required first-publish setup:
|
|
|
109
111
|
|
|
110
112
|
This repo does not require an npm automation token when Trusted Publishing is configured.
|
|
111
113
|
|
|
112
|
-
## Shared dynamic composition
|
|
114
|
+
## Shared dynamic composition
|
|
113
115
|
|
|
114
116
|
The current checkout uses `@openpresentation/opf/composition` for portable geometry. Slides can select `auto`, `row`, `column`, or `grid`, set weighted tracks, and request path-specific overflow diagnostics. See the sibling OPF repo's `docs/dynamic-composition.md` for the complete contract.
|
|
115
117
|
|
|
116
|
-
Version 0.
|
|
118
|
+
Version 0.4.0 requires published `@openpresentation/opf@^0.6.0`. The optional renderer peer requires `@openpresentation/opf-render@^0.4.0`. Clean registry installs support the new composition APIs without sibling checkouts. For coordinated source development, build OPF and run `node scripts/link-ecosystem.mjs` there; `pnpm test:ecosystem` verifies shared geometry and import/export behavior.
|
|
117
119
|
|
|
118
120
|
For crowded drafts, run `paginatePresentation` from `@openpresentation/opf/pagination` first, then pass its returned presentation to both preview and `toPptx`. Native table row sizing now follows shared reference geometry; the exporter does not add hidden table continuation slides.
|
|
119
121
|
|
|
@@ -123,15 +125,15 @@ PptxGenJS is pinned to 4.0.1. Its unused `image-size` dependency remains flagged
|
|
|
123
125
|
|
|
124
126
|
### Native table fitting
|
|
125
127
|
|
|
126
|
-
|
|
128
|
+
Version 0.4.0 imports and exports supported rich table cells and headers as editable runs, retaining resolved fonts, emphasis, color/alpha, hyperlinks, script positions and explicit line breaks. Import reads native XML, including paragraph defaults and significant whitespace; unstyled body cells remain strings. Shared core 0.6.0 layout grows rows for multiline content and fits text consistently with renderer 0.4.0 without inserting measured soft wraps. Native PowerPoint rendering remains unverified.
|
|
127
129
|
|
|
128
|
-
The
|
|
130
|
+
The exporter measures every cell with the same `textMeasurement` provider, font roles and effective nested `minFontSize` used by the SVG preview. Native table cells retain the original strings and values as text, with matching fitted sizes, line spacing, alignment, margins and row/column geometry. Uneven rows receive empty cells for missing columns. Theme border colors now use the same slot as the preview.
|
|
129
131
|
|
|
130
|
-
`npm test` compares exported OOXML against the published SVG renderer across 168 cells, including 24 cases that require
|
|
132
|
+
`npm test` compares exported OOXML against the published SVG renderer across 168 cells, including 24 cases that require taller rows, Roboto and Calibri-to-Carlito substitution, two canvas sizes, headers and all three alignments. PowerPoint still performs its own natural wrapping and needs the resolved fonts installed. These document-property checks do not establish native raster parity or lossless typed-cell import.
|
|
131
133
|
|
|
132
134
|
A local macOS Quick Look check opened both Roboto and system-Arial specimens. Quick Look substituted a serif font for uninstalled Roboto; the Arial specimen used a sans-serif face but still differed in table wrapping and row proportions. This is evidence of remaining viewer differences, not a passing PowerPoint raster comparison.
|
|
133
135
|
|
|
134
|
-
## Image geometry
|
|
136
|
+
## Image geometry
|
|
135
137
|
|
|
136
138
|
Native image exports now follow the browser's `design.imageFill`: `fit` (the default) centers an image without changing its aspect ratio, and `crop` fills the allocated box with a centered native crop. Slide settings override presentation settings. Geometry is calculated from the exact bytes embedded after asset resolution, so host resolvers are called once. PNG, JPEG, GIF and WebP dimension headers are supported; unsupported or unreadable dimensions produce a path-specific error rather than a distorted picture. Supply supported raster bytes through `imageResolver` for other formats.
|
|
137
139
|
|
|
@@ -156,7 +158,7 @@ Node conversion lazily loads the pinned open-source Sharp dependency and require
|
|
|
156
158
|
`npm test` includes the Node pixel-reference cases and verifies browser bundling. To run the browser pixel checks, run `npm run build:browser-check`, serve this repository locally, and open `/artifacts/webp-fallback/browser/index.html`. The page reports 13 checks covering embedded PNG pixels, alpha, EXIF, the first animation frame, fit/crop, resolver calls and DOM canvas fallback. These are browser export checks, separate from the recorded Keynote viewing evidence.
|
|
157
159
|
|
|
158
160
|
|
|
159
|
-
## Native background fills
|
|
161
|
+
## Native background fills
|
|
160
162
|
|
|
161
163
|
Fixed solid and linear-gradient backgrounds now export as native slide fills, keeping the background editable without rasterizing slide content. Deck defaults, inline theme overrides and per-slide overrides are resolved before export. Solid opacity, gradient stop colors/positions and combined color/background alpha are preserved. Empty and single-stop gradients follow the SVG preview's transparent/solid behavior; descending stop positions clamp to the preceding stop.
|
|
162
164
|
|
|
@@ -167,7 +169,7 @@ Import reads supported native RGB solid/linear fills directly; it uses no hidden
|
|
|
167
169
|
Node 20/24 tests and the 126-deck / 805-slide structural corpus pass. This proves serialization and the mathematical mapping, not native viewer pixels. Keynote 14.4 recognizes the editable native gradients. Twelve captured native PNGs now support 18 comparisons, including a Keynote-generated PPTX import: opaque differences are at most 4/255 per channel (mean below 0.38), and transparent portrait alpha differs by at most 1/255. The checked-in references run in ordinary Node tests without Keynote. Quick Look still renders these specimens as a flat average color, so its thumbnails are not evidence of their native appearance. Microsoft PowerPoint remains unavailable and unverified. Pattern/image backgrounds, theme-aware native fills, and other design decorations remain separate fidelity work.
|
|
168
170
|
|
|
169
171
|
|
|
170
|
-
## JPEG orientation on import
|
|
172
|
+
## JPEG orientation on import
|
|
171
173
|
|
|
172
174
|
`fromPptx` now preserves native quarter-turns and mirroring for JPEG pictures by writing the combined orientation into EXIF metadata. Existing embedded EXIF orientation is applied before the native transform. This requires no pixel decoder, recompression, upload or new dependency. The original PPTX remains unchanged, and alternative text survives. The eight orientations produced by this exporter restore the exact source JPEG bytes through repeated fit-mode export/import cycles.
|
|
173
175
|
|
|
@@ -190,3 +192,25 @@ Native dimensions retain full precision through import. Premature six-decimal in
|
|
|
190
192
|
Background-only and empty slides now remain blank during PPTX import; the importer no longer inserts a synthetic “Slide N” title. Speaker notes remain separate from visible content. Native fixture verification covers this behavior using an actual Keynote-exported presentation.
|
|
191
193
|
|
|
192
194
|
Version 0.2.0 requires Node 20.9 or later for native image decoding. Browser bundles continue using browser-safe entrypoints.
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
## Conditional table text styles (since 0.5.0)
|
|
198
|
+
|
|
199
|
+
The importer resolves referenced table styles through the archive's presentation relationship, including custom part paths, and accepts inline definitions. It applies whole-table, alternating row/column, edge and corner text styles using [Microsoft's DrawingML precedence](https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oi29500/90de4085-5fbf-47a7-bc37-d59509481d0a). Direct list-level, paragraph and run properties override the inherited style. Supported character properties are bold/italic, explicit or major/minor-theme Latin fonts, and the existing RGB/system/theme color and alpha transforms. Font-reference placeholder colors resolve after style inheritance. The table-style list's insertion default does not silently restyle existing tables without a reference.
|
|
200
|
+
|
|
201
|
+
Version 0.5.0 adds conditional table styles. Missing definitions (including built-in Office IDs whose definitions are absent from the archive) produce `unsupported-table-style`; no style data is fetched. Unsupported effects and table backgrounds produce `unsupported-table-cell-style`. Supported conditional solid fills, borders and their theme references use the same precedence as character styles. Native right-to-left geometry reports `unsupported-table-direction` and retains source cell order. Script-specific font selection, unsupported border geometry/color models and native PowerPoint raster parity remain separate work. Native re-export preserves effective character formatting but does not reconstruct the original style reference or redundant explicit normal flags.
|
|
202
|
+
|
|
203
|
+
`test/table-styles.mjs` builds independent native XML fixtures covering band offsets, overlapping first/last flags, all four corners, theme and explicit fonts, placeholder alpha, inheritance/defaults, direct resets, absent/external definitions and repeated conversion. The checked-in `test/fixtures/table-styles/conditional.pptx` is project-authored test data, not a captured native-viewer reference. After `npm run build:browser-check`, serve this repository and open `/artifacts/native-table-styles/browser/index.html` for 11 browser import, preview-trace and re-export checks.
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
## Styled and merged table cells (since 0.5.0)
|
|
207
|
+
|
|
208
|
+
The importer returns canonical `{value, style}` cell objects. Values retain supported rich runs, and styles retain direct solid fills/alpha, individual borders, horizontal/vertical alignment and reference-pixel padding. Rectangular native merges use `rowSpan`/`colSpan` anchors with explicit `null` at covered grid positions. Numeric source types cannot be recovered from displayed native text.
|
|
209
|
+
|
|
210
|
+
Import validates native continuation flags, bounds and covered text before applying merges. A malformed merge falls back to separate styled cells and reports `unsupported-table-merge`, retaining every source cell's text. If a valid merge crosses a flagged first row, the table keeps that row in its body with explicit formatting and reports `table-header-in-body`; OPF repeated headers cannot extend into body rows. Mixed/justified paragraph alignment, vertical text, unsupported fills/lines and 3D/diagonal effects remain explicit diagnostics. Unequal native column widths still need canonical representation.
|
|
211
|
+
|
|
212
|
+
Conditional borders keep the whole-table outer frame separate from interior horizontal/vertical lines. Row/column bands, edges and corners inherit line properties before direct cell overrides. Archive-local line references resolve theme placeholder colors and alpha. Missing references report `unsupported-table-border` unless an invisible or complete direct line masks them. A merged anchor uses its full span to select outer edges; differing native continuation border segments report `unsupported-table-merge-border` and retain the anchor border. `test/table-border-styles.mjs` covers these cases with native XML fixtures and repeated conversion.
|
|
213
|
+
|
|
214
|
+
Export consumes the same core geometry as SVG. It writes native merged cells and corrects PptxGenJS's padding-unit heuristic and missing dotted/transparent-border options in generated XML. Rich runs retain their own resolved opacity, including opaque overrides within a translucent cell. Tests inspect actual native XML and repeated conversions; native PowerPoint raster parity is not established.
|
|
215
|
+
|
|
216
|
+
Version 0.5.0 requires core 0.7.0 and renderer 0.5.0 for coordinated previews. `npm run test:styled-table` runs export/import regressions. For local browser verification, set `OPF_CORE_ROOT` to the core repository and `OPF_RENDER_ROOT` to the coordinated renderer worktree when running `npm run build:browser-check`. Serve the repository and open `/artifacts/native-styled-table-import/browser/index.html`. Omit those variables to exercise installed package dependencies.
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {XMLParser} from 'fast-xml-parser';
|
|
2
2
|
import {readNativeBackground, readBackgroundColor, colorTransforms} from './background.js';
|
|
3
3
|
|
|
4
|
-
const orderedParser = new XMLParser({ignoreAttributes: false, attributeNamePrefix: '', preserveOrder: true, parseAttributeValue: false, parseTagValue: false});
|
|
4
|
+
const orderedParser = new XMLParser({ignoreAttributes: false, attributeNamePrefix: '', preserveOrder: true, parseAttributeValue: false, parseTagValue: false, trimValues: false});
|
|
5
5
|
const defaultMapping = {bg1:'lt1',tx1:'dk1',bg2:'lt2',tx2:'dk2'};
|
|
6
6
|
const children = (nodes, name) => nodes?.find(node => Object.hasOwn(node, name))?.[name];
|
|
7
|
-
function
|
|
7
|
+
export function drawingObject(nodes) {
|
|
8
8
|
const result = Object.create(null);
|
|
9
9
|
for (const node of nodes ?? []) for (const [name, value] of Object.entries(node)) {
|
|
10
10
|
if (name === ':@' || name === '#text') continue;
|
|
11
|
-
const child = {...(node[':@'] ?? {}), ...
|
|
11
|
+
const child = {...(node[':@'] ?? {}), ...drawingObject(value)};
|
|
12
12
|
if (['a:srgbClr', 'a:sysClr', 'a:schemeClr'].includes(name)) {
|
|
13
13
|
child[colorTransforms] = (value ?? []).flatMap(item => Object.keys(item)
|
|
14
14
|
.filter(key => key !== ':@' && key !== '#text')
|
|
15
|
-
.map(key => [key, {...(item[':@'] ?? {}), ...
|
|
15
|
+
.map(key => [key, {...(item[':@'] ?? {}), ...drawingObject(item[key])}]));
|
|
16
16
|
}
|
|
17
17
|
if (Object.hasOwn(result, name)) result[name] = Array.isArray(result[name]) ? [...result[name], child] : [result[name], child];
|
|
18
18
|
else result[name] = child;
|
|
@@ -27,13 +27,13 @@ function parsed(bytes, parse) {
|
|
|
27
27
|
if (!bytes) return undefined;
|
|
28
28
|
if (!parsedParts.has(bytes)) {
|
|
29
29
|
const tree = parse();
|
|
30
|
-
parsedParts.set(bytes, {tree, value:
|
|
30
|
+
parsedParts.set(bytes, {tree, value: drawingObject(tree)});
|
|
31
31
|
}
|
|
32
32
|
return parsedParts.get(bytes);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Resolve only relationships inside the input archive; never fetch theme URLs.
|
|
36
|
-
export function
|
|
36
|
+
export function readSlideTheme(slidePath, {part, relationships, bytes}) {
|
|
37
37
|
const parsedPart = path => parsed(bytes(path), () => part(path, orderedParser));
|
|
38
38
|
const value = path => parsedPart(path)?.value;
|
|
39
39
|
const related = (path, type) => [...relationships(path).values()].find(rel => rel.type.endsWith('/' + type) && bytes(rel.path))?.path;
|
|
@@ -49,7 +49,7 @@ export function importBackground(slidePath, dimensions, {part, relationships, by
|
|
|
49
49
|
if (override?.['a:overrideClrMapping']) mapping = {...defaultMapping, ...override['a:overrideClrMapping']};
|
|
50
50
|
else if (override && Object.hasOwn(override, 'a:masterClrMapping')) mapping = masterMapping;
|
|
51
51
|
}
|
|
52
|
-
let colors, format, formatPath;
|
|
52
|
+
let colors, fonts, format, formatPath;
|
|
53
53
|
for (const {path} of [...chain].reverse()) {
|
|
54
54
|
for (const type of ['theme', 'themeOverride']) {
|
|
55
55
|
const themePath = related(path, type);
|
|
@@ -57,9 +57,15 @@ export function importBackground(slidePath, dimensions, {part, relationships, by
|
|
|
57
57
|
const doc = value(themePath);
|
|
58
58
|
const elements = type === 'theme' ? doc?.['a:theme']?.['a:themeElements'] : doc?.['a:themeOverride'];
|
|
59
59
|
if (elements?.['a:clrScheme']) colors = elements['a:clrScheme'];
|
|
60
|
+
if (elements?.['a:fontScheme']) fonts = elements['a:fontScheme'];
|
|
60
61
|
if (elements?.['a:fmtScheme']) {format = elements['a:fmtScheme'];formatPath = themePath;}
|
|
61
62
|
}
|
|
62
63
|
}
|
|
64
|
+
return {chain, colors, mapping, fonts, format, formatPath, parsedPart};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function importBackground(slidePath, dimensions, archive, report) {
|
|
68
|
+
const {chain, colors, mapping, format, formatPath, parsedPart} = readSlideTheme(slidePath, archive);
|
|
63
69
|
const background = chain.map(item => item.root?.['p:cSld']?.['p:bg']).find(value => value !== undefined);
|
|
64
70
|
if (!background) return undefined;
|
|
65
71
|
const unsupported = () => {
|
|
@@ -84,5 +90,5 @@ export function importBackground(slidePath, dimensions, {part, relationships, by
|
|
|
84
90
|
const style = styles?.[index < 1000 ? index - 1 : index - 1001];
|
|
85
91
|
if (!style) return unsupported();
|
|
86
92
|
context.placeholder = readBackgroundColor(reference, context);
|
|
87
|
-
return readNativeBackground(
|
|
93
|
+
return readNativeBackground(drawingObject([style]), dimensions, report, context);
|
|
88
94
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export interface ToPptxOptions {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
export interface FromPptxOptions {
|
|
51
|
-
/** Reports native
|
|
51
|
+
/** Reports native background, picture and table details that OPF import cannot preserve. Table paths identify the native graphic-frame and row/cell indexes (including headers). */
|
|
52
52
|
onDiagnostic?: (diagnostic: {code: string; path: string; message: string}) => void;
|
|
53
53
|
fallbackName?: string;
|
|
54
54
|
schema?: string;
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import {importTableFrames} from './table-import.js';
|
|
1
2
|
import {importImageOrientation} from './image-import.js';
|
|
2
3
|
import {nativeBackgroundFill} from './background.js';
|
|
3
4
|
import {importBackground} from './background-import.js';
|
|
4
5
|
import { webpToPng } from '#image-fallback';
|
|
5
6
|
import { rasterMetadata, pictureTransform, normalizeImageOrientation } from './image-geometry.js';
|
|
6
|
-
import { composeSlide, fitText, fitRichText, textWidthMeasurer, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle } from "@openpresentation/opf/composition";
|
|
7
|
+
import { layoutTable, composeSlide, fitText, fitRichText, textWidthMeasurer, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle } from "@openpresentation/opf/composition";
|
|
7
8
|
import PptxGenJS from "pptxgenjs";
|
|
8
9
|
import { unzipSync, zipSync } from "fflate";
|
|
9
10
|
import { XMLParser } from "fast-xml-parser";
|
|
@@ -166,6 +167,7 @@ export async function toPptx(input, options = {}) {
|
|
|
166
167
|
const context = resolvePresentationContext(presentation, {...options,textMeasurement:undefined});
|
|
167
168
|
context.listMarkers = new Map();
|
|
168
169
|
context.tableHeaders = new Map();
|
|
170
|
+
context.tableCells = new Map();
|
|
169
171
|
context.imagePlacements = new Map();
|
|
170
172
|
context.backgroundFills = new Map();
|
|
171
173
|
context.imageFormat = options.imageFormat ?? "compatible";
|
|
@@ -282,6 +284,7 @@ function parseRelationships(entries, sourcePartPath) {
|
|
|
282
284
|
id: relationship.Id,
|
|
283
285
|
type: relationship.Type ?? "",
|
|
284
286
|
target: relationship.Target ?? "",
|
|
287
|
+
targetMode: relationship.TargetMode ?? "Internal",
|
|
285
288
|
path: resolveRelationshipTarget(sourcePartPath, relationship.Target ?? "")
|
|
286
289
|
});
|
|
287
290
|
}
|
|
@@ -404,8 +407,13 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
|
|
|
404
407
|
if (item) items.push(item);
|
|
405
408
|
}
|
|
406
409
|
|
|
407
|
-
|
|
408
|
-
|
|
410
|
+
const frames = asArray(tree?.["p:graphicFrame"]);
|
|
411
|
+
const tables = frames.some(frame => frame['a:graphic']?.['a:graphicData']?.['a:tbl'])
|
|
412
|
+
? importTableFrames(slidePath, {
|
|
413
|
+
part: (path, parser) => parseRequiredXml(entries, path, parser), relationships: path => parseRelationships(entries, path), bytes: path => entries[path]
|
|
414
|
+
}, relationships, (frame, cell, code, message) => options.onDiagnostic?.({code, message, path: `slides.${slideIndex}.tables.${frame}${cell ? '.' + cell : ''}`}), dimensions) : [];
|
|
415
|
+
for (const [index, frame] of frames.entries()) {
|
|
416
|
+
const item = importGraphicFrame(entries, frame, slidePath, relationships, tables[index]);
|
|
409
417
|
if (item) items.push(item);
|
|
410
418
|
}
|
|
411
419
|
|
|
@@ -446,7 +454,7 @@ function importShape(shape, dimensions) {
|
|
|
446
454
|
};
|
|
447
455
|
}
|
|
448
456
|
|
|
449
|
-
function importGraphicFrame(entries, frame, slidePath, relationships) {
|
|
457
|
+
function importGraphicFrame(entries, frame, slidePath, relationships, importedTable) {
|
|
450
458
|
const bounds = shapeBounds(frame["p:xfrm"]);
|
|
451
459
|
const name = scalarText(frame["p:nvGraphicFramePr"]?.["p:cNvPr"]?.name).trim();
|
|
452
460
|
const graphicData = frame["a:graphic"]?.["a:graphicData"];
|
|
@@ -458,7 +466,7 @@ function importGraphicFrame(entries, frame, slidePath, relationships) {
|
|
|
458
466
|
name,
|
|
459
467
|
payload: {
|
|
460
468
|
type: "table",
|
|
461
|
-
table:
|
|
469
|
+
table: importedTable
|
|
462
470
|
}
|
|
463
471
|
};
|
|
464
472
|
}
|
|
@@ -632,18 +640,6 @@ function payloadFromSlideItem(item) {
|
|
|
632
640
|
return null;
|
|
633
641
|
}
|
|
634
642
|
|
|
635
|
-
function tableFromXml(table) {
|
|
636
|
-
const rows = asArray(table["a:tr"])
|
|
637
|
-
.map((row) => asArray(row?.["a:tc"]).map((cell) => textFromTextBody(cell?.["a:txBody"])));
|
|
638
|
-
// DrawingML's firstRow flag applies header-row formatting. Without that
|
|
639
|
-
// signal, retain all rows as data instead of guessing from their contents.
|
|
640
|
-
const firstRow = table["a:tblPr"]?.firstRow;
|
|
641
|
-
const hasHeaders = firstRow === "1" || firstRow === "true";
|
|
642
|
-
return hasHeaders && rows.length
|
|
643
|
-
? { columns: rows[0], rows: rows.slice(1) }
|
|
644
|
-
: { rows };
|
|
645
|
-
}
|
|
646
|
-
|
|
647
643
|
function chartFromRelationship(entries, slidePath, relationships, relId) {
|
|
648
644
|
const relationship = relationships.get(relId);
|
|
649
645
|
if (!relationship?.path || !entries[relationship.path]) return null;
|
|
@@ -1110,28 +1106,23 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1110
1106
|
return;
|
|
1111
1107
|
}
|
|
1112
1108
|
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
const
|
|
1122
|
-
const
|
|
1123
|
-
const text = stringifyText(
|
|
1124
|
-
const style = resolveTextStyle({ fontFamily: context.fonts.body, fontWeight: header ? 700 : 400, italic: false, path: cellPath }, options.textMeasurement);
|
|
1125
|
-
const rich = Array.isArray(row[columnIndex]);
|
|
1126
|
-
const fit = rich
|
|
1127
|
-
? fitRichText(row[columnIndex], cellBox, 15 * scale, (context.composition?.minFontSize ?? 16) * scale, {style,textMeasurement:options.textMeasurement})
|
|
1128
|
-
: fitText(text, cellBox, 15 * scale, (context.composition?.minFontSize ?? 16) * scale, textWidthMeasurer(style, options.textMeasurement));
|
|
1109
|
+
const layout = layoutTable(table, {x:region.x*96,y:region.y*96,width:region.w*96,height:region.h*96}, {
|
|
1110
|
+
scale, minFontSize:context.composition?.minFontSize, fontFamily:context.fonts.body, textMeasurement:options.textMeasurement, path
|
|
1111
|
+
});
|
|
1112
|
+
const columnCount = layout.columnCount;
|
|
1113
|
+
const rows = layout.rows.map(row => row.cells.map(cell => {
|
|
1114
|
+
const {header,rich,fit} = cell;
|
|
1115
|
+
const cellStyle = cell.style ?? {};
|
|
1116
|
+
const baseColor = (cellStyle.color ?? (header ? '#FFFFFF' : '#' + context.colors.text)).replace(/^#/, '');
|
|
1117
|
+
const baseFill = (cellStyle.fill ?? (header ? '#' + context.colors.accent : '#' + context.colors.surface)).replace(/^#/, '');
|
|
1118
|
+
const alpha = value => value.length === 8 ? (1 - parseInt(value.slice(6), 16) / 255) * 100 : 0;
|
|
1119
|
+
const text = stringifyText(cell.value), style = cell.textStyle;
|
|
1129
1120
|
const fragments = rich ? fit.richLines.flatMap(line => line.fragments) : [];
|
|
1130
|
-
const runs = rich ?
|
|
1121
|
+
const runs = rich ? cell.value.flatMap((value, index) => {
|
|
1131
1122
|
const run = typeof value === 'string' ? {text:value} : value;
|
|
1132
1123
|
const fragment = fragments.find(item => item.runIndex === index);
|
|
1133
1124
|
const runStyle = fragment?.style ?? resolveTextStyle({...style,fontFamily:run.fontFamily ?? style.fontFamily,fontWeight:run.bold === undefined ? style.fontWeight : run.bold ? 700 : 400,italic:run.italic ?? style.italic}, options.textMeasurement);
|
|
1134
|
-
const rawColor = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(run.color ?? '') ? run.color.slice(1) :
|
|
1125
|
+
const rawColor = /^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(run.color ?? '') ? run.color.slice(1) : baseColor;
|
|
1135
1126
|
const color = normalizeHex(rawColor), transparency = rawColor.length === 8 ? (1 - parseInt(rawColor.slice(6), 16) / 255) * 100 : 0;
|
|
1136
1127
|
const runOptions = {
|
|
1137
1128
|
fontFace:runStyle.fontFamily,fontSize:fragment ? fragment.fontSize * .75 : fit.fontSize * .75,
|
|
@@ -1160,21 +1151,28 @@ function addTablePayload(slide, table, region, context, options, path) {
|
|
|
1160
1151
|
italic: rich ? false : style.italic,
|
|
1161
1152
|
lineSpacing: fit.lineHeight * 0.75,
|
|
1162
1153
|
paraSpaceAfter: 0,
|
|
1163
|
-
align: context.contentAlignment,
|
|
1164
|
-
|
|
1165
|
-
|
|
1154
|
+
align: cellStyle.align ?? context.contentAlignment,
|
|
1155
|
+
valign: cellStyle.verticalAlign ?? 'top',
|
|
1156
|
+
...(cell.colSpan > 1 ? {colspan:cell.colSpan} : {}),
|
|
1157
|
+
...(cell.rowSpan > 1 ? {rowspan:cell.rowSpan} : {}),
|
|
1158
|
+
color: normalizeHex(baseColor),
|
|
1159
|
+
// Rich runs carry resolved alpha. A translucent cell default would
|
|
1160
|
+
// overwrite an explicit opaque run because PptxGenJS inherits falsy 0.
|
|
1161
|
+
...(cellStyle.color && !rich ? {transparency:alpha(baseColor)} : {}),
|
|
1162
|
+
fill: { color: normalizeHex(baseFill), ...(cellStyle.fill ? {transparency:alpha(baseFill)} : {}) },
|
|
1166
1163
|
},
|
|
1167
1164
|
};
|
|
1168
1165
|
}));
|
|
1169
1166
|
const objectName = `OPF table ${context.tableHeaders.size + 1}`;
|
|
1170
1167
|
context.tableHeaders.set(objectName, hasHeaders);
|
|
1168
|
+
if (layout.rows.some(row => row.cells.some(cell => Object.keys(cell.style ?? {}).length))) context.tableCells.set(objectName, {layout, scale, defaultBorder:{color:"#"+context.colors.border,width:1/scale}});
|
|
1171
1169
|
slide.addTable(rows, {
|
|
1172
1170
|
objectName,
|
|
1173
1171
|
x: region.x,
|
|
1174
1172
|
y: region.y,
|
|
1175
1173
|
w: region.w,
|
|
1176
|
-
h:
|
|
1177
|
-
rowH:
|
|
1174
|
+
h: layout.height / 96,
|
|
1175
|
+
rowH: layout.rows.map(row => row.box.height / 96),
|
|
1178
1176
|
colW: Array(columnCount).fill(region.w / columnCount),
|
|
1179
1177
|
autoPage: false,
|
|
1180
1178
|
fontFace: context.fonts.body,
|
|
@@ -1643,7 +1641,56 @@ function normalizePartBytes(path, bytes, context, renameMaps, entries, imageMeta
|
|
|
1643
1641
|
xml = xml.replace(/<p:graphicFrame>([\s\S]*?)<\/p:graphicFrame>/g, frame => {
|
|
1644
1642
|
const name = frame.match(/name="(OPF table \d+)"/)?.[1];
|
|
1645
1643
|
if (!context.tableHeaders.has(name)) return frame;
|
|
1646
|
-
|
|
1644
|
+
frame = frame.replace('<a:tblPr/>', `<a:tblPr firstRow="${context.tableHeaders.get(name) ? 1 : 0}"/>`);
|
|
1645
|
+
const table = context.tableCells.get(name);
|
|
1646
|
+
if (!table) return frame;
|
|
1647
|
+
const anchors = table.layout.rows.flatMap(row => row.cells);
|
|
1648
|
+
const ownerAt = (row,column) => anchors.find(anchor => row >= anchor.row && row < anchor.row + anchor.rowSpan && column >= anchor.column && column < anchor.column + anchor.colSpan);
|
|
1649
|
+
let rowIndex = 0;
|
|
1650
|
+
return frame.replace(/<a:tr\b[^>]*>[\s\S]*?<\/a:tr>/g, rowXml => {
|
|
1651
|
+
const currentRow = rowIndex++;
|
|
1652
|
+
const cells = table.layout.rows[currentRow].cells;
|
|
1653
|
+
let column = 0;
|
|
1654
|
+
return rowXml.replace(/<a:tc\b[^>]*>[\s\S]*?<\/a:tc>/g, cellXml => {
|
|
1655
|
+
const currentColumn = column++;
|
|
1656
|
+
const cell = cells.find(cell => cell.column === currentColumn);
|
|
1657
|
+
const owner = cell ?? ownerAt(currentRow,currentColumn);
|
|
1658
|
+
if (!owner) return cellXml;
|
|
1659
|
+
// PowerPoint reads each physical continuation's perimeter border.
|
|
1660
|
+
// Anchor-only styling truncates dashes and restores hidden segments.
|
|
1661
|
+
const perimeter = {left:currentColumn === owner.column, right:currentColumn === owner.column + owner.colSpan - 1,
|
|
1662
|
+
top:currentRow === owner.row, bottom:currentRow === owner.row + owner.rowSpan - 1};
|
|
1663
|
+
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}]))};
|
|
1664
|
+
style.borders = {...style.borders};
|
|
1665
|
+
// Native shared-edge precedence can let an implicit neighbor cover
|
|
1666
|
+
// an explicit merge border. Give both physical sides that border.
|
|
1667
|
+
for (const [edge,opposite,dr,dc] of [['left','right',0,-1],['right','left',0,1],['top','bottom',-1,0],['bottom','top',1,0]]) {
|
|
1668
|
+
if (!perimeter[edge] || style.borders[edge] || owner.rowSpan > 1 || owner.colSpan > 1) continue;
|
|
1669
|
+
const neighbor = ownerAt(currentRow+dr,currentColumn+dc);
|
|
1670
|
+
if (neighbor?.style?.borders?.[opposite]) style.borders[edge] = neighbor.style.borders[opposite];
|
|
1671
|
+
}
|
|
1672
|
+
cellXml = cellXml.replace(/<a:tcPr\b([^>]*)\/>/, '<a:tcPr$1></a:tcPr>');
|
|
1673
|
+
return cellXml.replace(/<a:tcPr\b([^>]*)>([\s\S]*?)<\/a:tcPr>/, (properties, attributes, contents) => {
|
|
1674
|
+
if (style.padding) {
|
|
1675
|
+
const padding = {top:8, right:10, bottom:4, left:10, ...style.padding};
|
|
1676
|
+
for (const [edge, key] of [['top','marT'],['right','marR'],['bottom','marB'],['left','marL']]) {
|
|
1677
|
+
attributes = attributes.replace(new RegExp(` ${key}="[^"]*"`), '');
|
|
1678
|
+
attributes += ` ${key}="${Math.round(padding[edge] * table.scale * 9525)}"`;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
for (const [edge, native] of [['left','lnL'],['right','lnR'],['top','lnT'],['bottom','lnB']]) {
|
|
1682
|
+
const border = style.borders?.[edge];
|
|
1683
|
+
if (!border) continue;
|
|
1684
|
+
const fill = border.width === 0 ? '<a:noFill/>' : nativeBackgroundFill({type:'solid',color:border.color},{width:1,height:1});
|
|
1685
|
+
const dash = {solid:'solid',dash:'dash',dot:'sysDot'}[border.dash ?? 'solid'];
|
|
1686
|
+
const line = `<a:${native} w="${Math.round(border.width * table.scale * 9525)}" cap="flat" cmpd="sng" algn="ctr">${fill}<a:prstDash val="${dash}"/></a:${native}>`;
|
|
1687
|
+
const existing = new RegExp(`<a:${native}\\b[^>]*>[\\s\\S]*?<\\/a:${native}>`);
|
|
1688
|
+
contents = existing.test(contents) ? contents.replace(existing, line) : contents + line;
|
|
1689
|
+
}
|
|
1690
|
+
return `<a:tcPr${attributes}>${contents}</a:tcPr>`;
|
|
1691
|
+
});
|
|
1692
|
+
});
|
|
1693
|
+
});
|
|
1647
1694
|
});
|
|
1648
1695
|
// Image data is already resolved and embedded by PptxGenJS. Read those
|
|
1649
1696
|
// exact bytes instead of fetching or resolving the source a second time.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {readBackgroundColor} from './background.js';
|
|
2
|
+
|
|
3
|
+
const has=(value,key)=>Object.hasOwn(value??{},key);
|
|
4
|
+
const fills=['a:solidFill','a:noFill','a:gradFill','a:blipFill','a:pattFill','a:grpFill'];
|
|
5
|
+
|
|
6
|
+
export function mergeNativeLine(...levels){
|
|
7
|
+
const result={};
|
|
8
|
+
for(const level of levels){
|
|
9
|
+
if(!level)continue;
|
|
10
|
+
if(fills.some(key=>has(level,key)))for(const key of fills)delete result[key];
|
|
11
|
+
if(has(level,'a:prstDash')||has(level,'a:custDash')){delete result['a:prstDash'];delete result['a:custDash'];}
|
|
12
|
+
// A complete direct line or an invisible line masks an unavailable theme
|
|
13
|
+
// reference; partial overrides still need the missing inherited properties.
|
|
14
|
+
if(has(level,'a:noFill')||Number(level.w)===0||(has(level,'w')&&fills.some(key=>has(level,key))&&has(level,'a:prstDash')))delete result._opfUnresolvedTableLine;
|
|
15
|
+
Object.assign(result,level);
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function nativeStyleLine(border,context){
|
|
21
|
+
if(border['a:ln'])return border['a:ln'];
|
|
22
|
+
const reference=border['a:lnRef'];
|
|
23
|
+
if(!reference)return undefined;
|
|
24
|
+
const unresolved={_opfUnresolvedTableLine:true};
|
|
25
|
+
if(!/^\d+$/.test(reference.idx??''))return unresolved;
|
|
26
|
+
const index=Number(reference.idx);
|
|
27
|
+
if(index===0)return undefined;
|
|
28
|
+
const lines=context.format?.['a:lnStyleLst']?.['a:ln'];
|
|
29
|
+
const selected=(Array.isArray(lines)?lines:lines?[lines]:[])[index-1];
|
|
30
|
+
if(!selected)return unresolved;
|
|
31
|
+
const line={...selected};
|
|
32
|
+
if(line['a:solidFill']){
|
|
33
|
+
const color=readBackgroundColor(line['a:solidFill'],{...context,placeholder:readBackgroundColor(reference,context)});
|
|
34
|
+
if(!color)return {...line,...unresolved};
|
|
35
|
+
line['a:solidFill']={'a:srgbClr':{val:color.hex.slice(1),'a:alpha':{val:String(Math.round(color.alpha*100000))}}};
|
|
36
|
+
}
|
|
37
|
+
return line;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function nativeCellExtent(row,column,rowCount,columnCount,geometry={}){
|
|
41
|
+
const span=(value,remaining)=>{const n=Number(value??1);return Number.isSafeInteger(n)&&n>=1&&n<=remaining?n:1;};
|
|
42
|
+
return {top:row===0,left:column===0,bottom:row+span(geometry.rowSpan,rowCount-row)===rowCount,right:column+span(geometry.gridSpan,columnCount-column)===columnCount};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Keep named edge and interior properties distinct until inheritance finishes.
|
|
46
|
+
* wholeTbl edges belong to its outer frame; conditional explicit edges belong
|
|
47
|
+
* to the selected cell. Interior lines apply only away from the outer frame. */
|
|
48
|
+
export function inheritNativeBorders(previous,decoration,name,extent,context){
|
|
49
|
+
const result={...previous},borders=decoration['a:tcBdr']??{};
|
|
50
|
+
for(const key of ['left','right','top','bottom','insideH','insideV','tl2br','tr2bl']){
|
|
51
|
+
const border=borders['a:'+key];
|
|
52
|
+
if(!border || name==='wholeTbl'&&has(extent,key)&&!extent[key])continue;
|
|
53
|
+
const line=nativeStyleLine(border,context);
|
|
54
|
+
if(line!==undefined)result[key]=mergeNativeLine(result[key],line);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function nativeBorderProperties(borders,extent){
|
|
60
|
+
const result={};
|
|
61
|
+
for(const [edge,key,inside] of [['left','a:lnL','insideV'],['right','a:lnR','insideV'],['top','a:lnT','insideH'],['bottom','a:lnB','insideH']]){
|
|
62
|
+
const base=extent[edge]?undefined:borders[inside],line=borders[edge];
|
|
63
|
+
if(base||line)result[key]=mergeNativeLine(base,line);
|
|
64
|
+
}
|
|
65
|
+
for(const [edge,key] of [['tl2br','a:lnTlToBr'],['tr2bl','a:lnBlToTr']])if(borders[edge])result[key]=borders[edge];
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function mergeCellBorderOverrides(inherited,direct,merged){
|
|
70
|
+
const result={...merged};
|
|
71
|
+
for(const key of ['a:lnL','a:lnR','a:lnT','a:lnB','a:lnTlToBr','a:lnBlToTr'])if(inherited[key]&&direct[key])result[key]=mergeNativeLine(inherited[key],direct[key]);
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import {drawingObject} from './background-import.js';
|
|
2
|
+
import {readBackgroundColor} from './background.js';
|
|
3
|
+
|
|
4
|
+
const has = (value, key) => Object.hasOwn(value ?? {}, key);
|
|
5
|
+
const nodes = (tree, tag) => (tree ?? []).filter(node => has(node, tag));
|
|
6
|
+
const child = (tree, tag) => nodes(tree, tag)[0]?.[tag];
|
|
7
|
+
const truth = value => ['1','true','on'].includes(value);
|
|
8
|
+
const text = value => Array.isArray(value) ? value.map(run => typeof run === 'string' ? run : run.text).join('') : value ?? '';
|
|
9
|
+
const hex = color => color.hex + (color.alpha < 1 ? Math.round(color.alpha * 255).toString(16).padStart(2,'0').toUpperCase() : '');
|
|
10
|
+
|
|
11
|
+
// Preserve ordered fill-style indexes: grouping XML tags would reorder mixed
|
|
12
|
+
// solid/gradient/image entries. Resolve phClr against this reference only.
|
|
13
|
+
export function nativeStyleFill(style, context) {
|
|
14
|
+
if (style['a:fill']) return style['a:fill'];
|
|
15
|
+
const reference=style['a:fillRef'];
|
|
16
|
+
if (!reference) return {};
|
|
17
|
+
const unresolved={_opfUnresolvedTableFill:true};
|
|
18
|
+
if (!/^\d+$/.test(reference.idx ?? '')) return unresolved;
|
|
19
|
+
const index=Number(reference.idx);
|
|
20
|
+
if (index===0 || index===1000) return {'a:noFill':{}};
|
|
21
|
+
const tree=context.formatPath && context.parsedPart(context.formatPath)?.tree;
|
|
22
|
+
const theme=child(tree,'a:theme'),elements=theme?child(theme,'a:themeElements'):child(tree,'a:themeOverride');
|
|
23
|
+
const format=child(elements,'a:fmtScheme');
|
|
24
|
+
const entries=child(format,index<1000?'a:fillStyleLst':'a:bgFillStyleLst')?.filter(node=>Object.keys(node).some(key=>key!=='#text'&&key!==':@'));
|
|
25
|
+
const entry=entries?.[index<1000?index-1:index-1001];
|
|
26
|
+
if (!entry) return unresolved;
|
|
27
|
+
const properties=drawingObject([entry]);
|
|
28
|
+
if (properties['a:solidFill']) {
|
|
29
|
+
const color=readBackgroundColor(properties['a:solidFill'],{...context,placeholder:readBackgroundColor(reference,context)});
|
|
30
|
+
if (!color) return unresolved;
|
|
31
|
+
return {'a:solidFill':{'a:srgbClr':{val:color.hex.slice(1),'a:alpha':{val:String(Math.round(color.alpha*100000))}}}};
|
|
32
|
+
}
|
|
33
|
+
return properties;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function solid(properties, context, report, kind) {
|
|
37
|
+
if (has(properties,'a:noFill')) return '#00000000';
|
|
38
|
+
if (properties['a:solidFill']) {
|
|
39
|
+
const color = readBackgroundColor(properties['a:solidFill'], context);
|
|
40
|
+
if (color) return hex(color);
|
|
41
|
+
report(`unsupported-table-${kind}`, 'The native solid color or its transforms could not be resolved.');
|
|
42
|
+
} else if (['a:gradFill','a:blipFill','a:pattFill','a:grpFill'].some(key => has(properties,key))) {
|
|
43
|
+
report(`unsupported-table-${kind}`, 'Only solid cell fills and border colors are represented by OPF table styles.');
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function nativeCellStyle(properties, body, context, scale, report) {
|
|
49
|
+
const style = {fill:'#00000000', align:'left', verticalAlign:'top', padding:{}, borders:{}};
|
|
50
|
+
const fill = solid(properties, context, report, 'cell-fill');
|
|
51
|
+
if (properties._opfUnresolvedTableFill) report('unsupported-table-cell-fill','The native fill style reference or placeholder color could not be resolved from this archive.');
|
|
52
|
+
if (fill !== undefined) style.fill = fill;
|
|
53
|
+
// DrawingML defaults are 0.1 inch horizontally and 0.05 inch vertically.
|
|
54
|
+
for (const [edge,key,fallback] of [['top','marT',45720],['right','marR',91440],['bottom','marB',45720],['left','marL',91440]]) {
|
|
55
|
+
const value = Number(properties[key] ?? fallback) / (9525 * scale);
|
|
56
|
+
if (Number.isFinite(value) && value >= 0 && value <= 200) style.padding[edge] = value;
|
|
57
|
+
else {style.padding[edge] = Math.min(200,fallback / (9525 * scale));report('unsupported-table-padding','The native cell margin is outside the canonical 0–200 reference-pixel range.');}
|
|
58
|
+
}
|
|
59
|
+
const vertical = {t:'top',ctr:'middle',b:'bottom'}[properties.anchor ?? 't'];
|
|
60
|
+
if (vertical) style.verticalAlign = vertical;
|
|
61
|
+
else report('unsupported-table-alignment','Distributed or justified vertical cell alignment is not represented.');
|
|
62
|
+
if (truth(properties.anchorCtr) || properties.vert && properties.vert !== 'horz') report('unsupported-table-alignment','Centered anchoring or vertical text direction is not represented.');
|
|
63
|
+
const listStyle = drawingObject(child(body,'a:lstStyle'));
|
|
64
|
+
const alignments = nodes(body,'a:p').map(paragraph => {
|
|
65
|
+
const p = drawingObject(paragraph['a:p'])['a:pPr'] ?? {};
|
|
66
|
+
return p.algn ?? listStyle[`a:lvl${Number(p.lvl ?? 0)+1}pPr`]?.algn ?? listStyle['a:defPPr']?.algn ?? 'l';
|
|
67
|
+
});
|
|
68
|
+
const alignment = {l:'left',ctr:'center',r:'right'}[alignments[0] ?? 'l'];
|
|
69
|
+
if (alignment) style.align = alignment;
|
|
70
|
+
if (!alignment || new Set(alignments).size > 1) report('unsupported-table-alignment','Mixed or justified paragraph alignment cannot be represented by one cell alignment.');
|
|
71
|
+
for (const [edge,key] of [['left','a:lnL'],['right','a:lnR'],['top','a:lnT'],['bottom','a:lnB']]) {
|
|
72
|
+
const line = properties[key];
|
|
73
|
+
style.borders[edge] = {color:'#00000000',width:0};
|
|
74
|
+
if (!line) continue;
|
|
75
|
+
if (has(line,'a:noFill') || line.w === '0') continue;
|
|
76
|
+
if (line._opfUnresolvedTableLine) report('unsupported-table-border','The native line style reference or placeholder color could not be resolved from this archive.');
|
|
77
|
+
const hasFill=['a:solidFill','a:noFill','a:gradFill','a:blipFill','a:pattFill','a:grpFill'].some(key=>has(line,key));
|
|
78
|
+
const color = solid(line,context,report,'border') ?? (!hasFill&&!line._opfUnresolvedTableLine?'#000000':undefined);
|
|
79
|
+
const width = Number(line.w ?? 12700) / (9525 * scale);
|
|
80
|
+
const dash = {solid:'solid',dash:'dash',sysDash:'dash',sysDot:'dot',dot:'dot'}[line['a:prstDash']?.val ?? 'solid'];
|
|
81
|
+
if (color !== undefined && Number.isFinite(width) && width >= 0 && width <= 32 && dash) style.borders[edge] = {color,width,...(dash === 'solid' ? {} : {dash})};
|
|
82
|
+
else report('unsupported-table-border','The native border color, width or dash pattern cannot be represented.');
|
|
83
|
+
if (line.cmpd && line.cmpd !== 'sng' || line.algn && line.algn !== 'ctr' || has(line,'a:custDash')) report('unsupported-table-border','Compound, inset or custom-dash border geometry is not represented.');
|
|
84
|
+
}
|
|
85
|
+
if (['a:lnTlToBr','a:lnBlToTr','a:cell3D'].some(key => has(properties,key))) report('unsupported-table-cell-effect','Diagonal cell borders and 3D cell effects are not represented.');
|
|
86
|
+
return style;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Accept a native dense merge only when every covered position is empty and
|
|
90
|
+
* carries the corresponding continuation flags. On malformed input, retain all
|
|
91
|
+
* source cells without merges rather than hiding text behind a guessed anchor. */
|
|
92
|
+
export function nativeTableGrid(cells, columnCount, headers, report) {
|
|
93
|
+
const owners = cells.map(row => Array(row.length));
|
|
94
|
+
let malformed = false;
|
|
95
|
+
const bad = (r,c,message) => {malformed=true;report(`rows.${r}.${c}`,'unsupported-table-merge',message);};
|
|
96
|
+
const rows = cells.map(row => row.map(cell => ({value:cell.value,style:cell.style})));
|
|
97
|
+
for (let r=0;r<cells.length;r++) for (let c=0;c<cells[r].length;c++) {
|
|
98
|
+
const cell=cells[r][c], geometry=cell.geometry, owner=owners[r][c];
|
|
99
|
+
const horizontal=truth(geometry.hMerge),vertical=truth(geometry.vMerge);
|
|
100
|
+
if (owner) {
|
|
101
|
+
if (text(cell.value) !== '' || horizontal !== (c > owner.c) || vertical !== (r > owner.r)) bad(r,c,'Native merge continuations conflict with their anchor or contain text; all source cell text is retained.');
|
|
102
|
+
rows[r][c]=null;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
const rowSpan=Number(geometry.rowSpan ?? 1),colSpan=Number(geometry.gridSpan ?? 1);
|
|
106
|
+
if (horizontal || vertical || !Number.isSafeInteger(rowSpan) || !Number.isSafeInteger(colSpan) || rowSpan<1 || colSpan<1 || r+rowSpan>cells.length || c+colSpan>columnCount) {
|
|
107
|
+
bad(r,c,'Native merge geometry is incomplete or outside the table grid; all source cell text is retained.');continue;
|
|
108
|
+
}
|
|
109
|
+
if (rowSpan>1) rows[r][c].rowSpan=rowSpan;
|
|
110
|
+
if (colSpan>1) rows[r][c].colSpan=colSpan;
|
|
111
|
+
if (rowSpan === 1 && colSpan === 1) continue;
|
|
112
|
+
for (let y=r;y<r+rowSpan;y++) for (let x=c;x<c+colSpan;x++) {
|
|
113
|
+
if (!cells[y]?.[x] || owners[y][x]) bad(r,c,'Native merge rectangles overlap or omit covered cells; all source cell text is retained.');
|
|
114
|
+
else owners[y][x]={r,c};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (malformed) return {headers,rows:cells.map(row=>row.map(cell=>({value:cell.value,style:cell.style})))};
|
|
118
|
+
// A canonical merged cell has one border per full edge. Native continuation
|
|
119
|
+
// cells can specify different edge segments; retain the anchor and report
|
|
120
|
+
// the loss rather than silently claiming that segmented border is preserved.
|
|
121
|
+
for (let r=0;r<cells.length;r++) for (let c=0;c<cells[r].length;c++) {
|
|
122
|
+
const owner=owners[r][c];
|
|
123
|
+
if (!owner || owner.r===r && owner.c===c) continue;
|
|
124
|
+
const anchor=rows[owner.r][owner.c],cell=cells[r][c];
|
|
125
|
+
const perimeter={top:r===owner.r,left:c===owner.c,bottom:r===owner.r+(anchor.rowSpan??1)-1,right:c===owner.c+(anchor.colSpan??1)-1};
|
|
126
|
+
const differs=edge=>{
|
|
127
|
+
const a=anchor.style.borders[edge],b=cell.style.borders[edge];
|
|
128
|
+
if (a.width===0 && b.width===0) return false;
|
|
129
|
+
return a.width!==b.width || a.color!==b.color || (a.dash??'solid')!==(b.dash??'solid');
|
|
130
|
+
};
|
|
131
|
+
if ((cell.borderEdges??[]).some(edge=>perimeter[edge]&&differs(edge))) report(`rows.${r}.${c}`,'unsupported-table-merge-border','Native continuation cells specify differing border segments around a merged cell. The merge and anchor borders are retained; segmented borders are not represented.');
|
|
132
|
+
}
|
|
133
|
+
if (headers && rows[0]?.some(cell=>cell?.rowSpan>1)) {
|
|
134
|
+
headers=false;
|
|
135
|
+
report('','table-header-in-body','A native merge crosses the first row. It is retained in body rows with explicit formatting, because canonical repeated headers cannot span into body rows.');
|
|
136
|
+
}
|
|
137
|
+
return {headers,rows};
|
|
138
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import {drawingObject, readSlideTheme} from './background-import.js';
|
|
2
|
+
import {readBackgroundColor} from './background.js';
|
|
3
|
+
import {nativeCellStyle, nativeTableGrid, nativeStyleFill} from './table-cell-import.js';
|
|
4
|
+
import {nativeCellExtent,inheritNativeBorders,nativeBorderProperties,mergeCellBorderOverrides} from './table-border-import.js';
|
|
5
|
+
|
|
6
|
+
const nodes = (tree, tag) => (tree ?? []).filter(node => Object.hasOwn(node, tag));
|
|
7
|
+
const child = (tree, tag) => nodes(tree, tag)[0]?.[tag];
|
|
8
|
+
const attrs = (tree, tag) => nodes(tree, tag)[0]?.[':@'] ?? {};
|
|
9
|
+
const text = tree => (tree ?? []).map(node => node['#text'] ?? '').join('');
|
|
10
|
+
const boolean = value => ['1','true','on'].includes(value) ? true : ['0','false','off'].includes(value) ? false : undefined;
|
|
11
|
+
|
|
12
|
+
function runStyle(properties, context, relationships, report) {
|
|
13
|
+
const result = {};
|
|
14
|
+
for (const [native, opf] of [['b','bold'], ['i','italic']]) {
|
|
15
|
+
const value = boolean(properties[native]);
|
|
16
|
+
if (value !== undefined) result[opf] = value;
|
|
17
|
+
}
|
|
18
|
+
for (const [native, opf] of [['u','underline'], ['strike','strikethrough']]) {
|
|
19
|
+
if (properties[native] !== undefined) result[opf] = properties[native] !== 'none' && properties[native] !== 'noStrike';
|
|
20
|
+
}
|
|
21
|
+
const size = Number(properties.sz);
|
|
22
|
+
if (Number.isFinite(size) && size > 0) result.fontSize = size / 100;
|
|
23
|
+
const baseline = Number(properties.baseline);
|
|
24
|
+
if (Number.isFinite(baseline)) {
|
|
25
|
+
result.superscript = baseline > 0;
|
|
26
|
+
result.subscript = baseline < 0;
|
|
27
|
+
}
|
|
28
|
+
const font = properties['a:latin']?.typeface;
|
|
29
|
+
if (properties['a:latin'] && !font) report('unsupported-table-font', 'The native text font has no usable Latin face or theme reference.');
|
|
30
|
+
if (font) {
|
|
31
|
+
const match = font.match(/^\+(mj|mn)-(lt|ea|cs)$/);
|
|
32
|
+
const family = match ? context.fonts?.[match[1] === 'mj' ? 'a:majorFont' : 'a:minorFont']?.[{lt:'a:latin',ea:'a:ea',cs:'a:cs'}[match[2]]]?.typeface : font;
|
|
33
|
+
if (family) result.fontFamily = family;
|
|
34
|
+
else report('unsupported-table-font', 'The theme font reference could not be resolved from this archive.');
|
|
35
|
+
}
|
|
36
|
+
if (properties['a:solidFill']) {
|
|
37
|
+
const color = readBackgroundColor(properties['a:solidFill'], context);
|
|
38
|
+
if (color) result.color = color.hex + (color.alpha < 1 ? Math.round(color.alpha * 255).toString(16).padStart(2, '0').toUpperCase() : '');
|
|
39
|
+
else report('unsupported-table-text-color', 'The native text color or its transforms could not be resolved.');
|
|
40
|
+
} else if (properties['a:noFill']) {
|
|
41
|
+
result.color = '#00000000';
|
|
42
|
+
}
|
|
43
|
+
if (['a:gradFill','a:blipFill','a:pattFill','a:grpFill'].some(key => properties[key])) report('unsupported-table-text-fill', 'Only solid native text fills are represented by OPF runs.');
|
|
44
|
+
const hyperlink = properties['a:hlinkClick'];
|
|
45
|
+
if (hyperlink) {
|
|
46
|
+
const relationship = relationships.get(hyperlink['r:id']);
|
|
47
|
+
if (relationship?.type.endsWith('/hyperlink') && relationship.targetMode === 'External' && relationship.target && !hyperlink.action) result.link = relationship.target;
|
|
48
|
+
else report('unsupported-table-link', 'The native hyperlink action or relationship cannot be represented as a URL.');
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Merge at the native property level before conversion: an explicit normal run
|
|
54
|
+
// overrides a bold paragraph default, and a new fill replaces the inherited fill.
|
|
55
|
+
function mergeProperties(...levels) {
|
|
56
|
+
const result = {};
|
|
57
|
+
const fills = ['a:solidFill','a:noFill','a:gradFill','a:blipFill','a:pattFill','a:grpFill','_opfUnresolvedTableFill'];
|
|
58
|
+
for (const level of levels) {
|
|
59
|
+
if (!level) continue;
|
|
60
|
+
if (fills.some(key => Object.hasOwn(level, key))) for (const key of fills) delete result[key];
|
|
61
|
+
Object.assign(result, level);
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cellText(body, context, relationships, report, header, tableDefaults) {
|
|
67
|
+
const listStyle = drawingObject(child(body, 'a:lstStyle'));
|
|
68
|
+
const paragraphs = nodes(body, 'a:p');
|
|
69
|
+
const runs = [];
|
|
70
|
+
const append = (value, style = {}) => {
|
|
71
|
+
if (!value) return;
|
|
72
|
+
runs.push(Object.keys(style).length ? {text:value, ...style} : value);
|
|
73
|
+
};
|
|
74
|
+
paragraphs.forEach((paragraph, index) => {
|
|
75
|
+
const content = paragraph['a:p'];
|
|
76
|
+
if (index) append('\n');
|
|
77
|
+
const pPr = drawingObject(content)['a:pPr'] ?? {};
|
|
78
|
+
const level = Number(pPr.lvl ?? 0) + 1;
|
|
79
|
+
const defaults = mergeProperties(header ? {b:'0'} : undefined, tableDefaults, listStyle['a:defPPr']?.['a:defRPr'], listStyle[`a:lvl${level}pPr`]?.['a:defRPr'], pPr['a:defRPr']);
|
|
80
|
+
for (const node of content) {
|
|
81
|
+
const tag = ['a:r', 'a:fld', 'a:br'].find(tag => Object.hasOwn(node, tag));
|
|
82
|
+
if (!tag) continue;
|
|
83
|
+
const properties = mergeProperties(defaults, drawingObject(node[tag])['a:rPr']);
|
|
84
|
+
append(tag === 'a:br' ? '\n' : text(child(node[tag], 'a:t')), runStyle(properties, context, relationships, report));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
return runs.some(run => typeof run !== 'string') ? runs : runs.join('');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Table styles are archive data, resolved through the presentation relationship.
|
|
91
|
+
// The list's def attribute is an insertion default, not an implicit style for
|
|
92
|
+
// existing unstyled tables. Built-in IDs without archive definitions stay explicit
|
|
93
|
+
// diagnostics; do not substitute a guessed theme or download style definitions.
|
|
94
|
+
function tableStyle(props, archive, context, report) {
|
|
95
|
+
const inline = child(props, 'a:tableStyle');
|
|
96
|
+
if (inline) return drawingObject(inline);
|
|
97
|
+
const id = text(child(props, 'a:tableStyleId')).trim().toLowerCase();
|
|
98
|
+
if (!id) return undefined;
|
|
99
|
+
const relationship = [...archive.relationships('ppt/presentation.xml').values()]
|
|
100
|
+
.find(rel => rel.type.endsWith('/tableStyles') && rel.targetMode !== 'External' && archive.bytes(rel.path));
|
|
101
|
+
const list = relationship && child(context.parsedPart(relationship.path)?.tree, 'a:tblStyleLst');
|
|
102
|
+
const selected = nodes(list, 'a:tblStyle').find(node => node[':@']?.styleId?.trim().toLowerCase() === id);
|
|
103
|
+
if (selected) return drawingObject(selected['a:tblStyle']);
|
|
104
|
+
report('unsupported-table-style', 'The referenced native table style has no definition in this archive; direct text formatting is retained.');
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const colorKinds = ['a:srgbClr','a:sysClr','a:schemeClr','a:scrgbClr','a:hslClr','a:prstClr'];
|
|
109
|
+
function tableTextProperties(style, context) {
|
|
110
|
+
const result = {};
|
|
111
|
+
for (const key of ['b','i']) {
|
|
112
|
+
const value = boolean(style[key]);
|
|
113
|
+
if (value !== undefined) result[key] = value ? '1' : '0';
|
|
114
|
+
}
|
|
115
|
+
if (style['a:font']) {
|
|
116
|
+
result['a:latin'] = style['a:font']['a:latin'] ?? {};
|
|
117
|
+
} else if (style['a:fontRef']) {
|
|
118
|
+
const index = style['a:fontRef'].idx;
|
|
119
|
+
result['a:latin'] = {typeface:index === 'major' ? '+mj-lt' : index === 'minor' ? '+mn-lt' : ''};
|
|
120
|
+
}
|
|
121
|
+
const color = Object.fromEntries(colorKinds.filter(key => Object.hasOwn(style, key)).map(key => [key, style[key]]));
|
|
122
|
+
if (Object.keys(color).length) {
|
|
123
|
+
const placeholder = readBackgroundColor(style['a:fontRef'], context);
|
|
124
|
+
const resolved = readBackgroundColor(color, {...context, placeholder});
|
|
125
|
+
// Normalize style placeholder colors before merging direct paragraph/run fills.
|
|
126
|
+
result['a:solidFill'] = resolved ? {'a:srgbClr':{val:resolved.hex.slice(1), 'a:alpha':{val:String(Math.round(resolved.alpha * 100000))}}} : color;
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function conditionalProperties(style, properties, row, column, rowCount, columnCount, context, report, geometry) {
|
|
132
|
+
if (!style) return {text:undefined,cell:{}};
|
|
133
|
+
const enabled = key => boolean(properties[key]) === true;
|
|
134
|
+
const firstRow = enabled('firstRow') && row === 0;
|
|
135
|
+
const lastRow = enabled('lastRow') && row === rowCount - 1;
|
|
136
|
+
const firstCol = enabled('firstCol') && column === 0;
|
|
137
|
+
const lastCol = enabled('lastCol') && column === columnCount - 1;
|
|
138
|
+
// DrawingML / MS-OI29500 2.1.1265 order (not WordprocessingML order).
|
|
139
|
+
const parts = ['wholeTbl'];
|
|
140
|
+
if (enabled('bandRow') && !firstRow && !lastRow) parts.push((row + Number(enabled('firstRow'))) % 2 ? 'band2H' : 'band1H');
|
|
141
|
+
if (enabled('bandCol') && !firstCol && !lastCol) parts.push((column + Number(enabled('firstCol'))) % 2 ? 'band2V' : 'band1V');
|
|
142
|
+
if (lastCol) parts.push('lastCol');
|
|
143
|
+
if (firstCol) parts.push('firstCol');
|
|
144
|
+
if (lastRow) parts.push('lastRow');
|
|
145
|
+
if (lastRow && lastCol) parts.push('seCell');
|
|
146
|
+
if (lastRow && firstCol) parts.push('swCell');
|
|
147
|
+
if (firstRow) parts.push('firstRow');
|
|
148
|
+
if (firstRow && lastCol) parts.push('neCell');
|
|
149
|
+
if (firstRow && firstCol) parts.push('nwCell');
|
|
150
|
+
const textStyle = {};
|
|
151
|
+
let cellStyle = {};
|
|
152
|
+
let borders = {};
|
|
153
|
+
const extent=nativeCellExtent(row,column,rowCount,columnCount,geometry);
|
|
154
|
+
for (const name of parts) {
|
|
155
|
+
const part = style['a:' + name];
|
|
156
|
+
if (!part) continue;
|
|
157
|
+
const decoration=part['a:tcStyle'] ?? {};
|
|
158
|
+
cellStyle=mergeProperties(cellStyle,nativeStyleFill(decoration,context));
|
|
159
|
+
borders=inheritNativeBorders(borders,decoration,name,extent,context);
|
|
160
|
+
if (Object.keys(decoration).some(key=>!['a:fill','a:fillRef','a:tcBdr'].includes(key))) report('unsupported-table-cell-style','The conditional cell style includes effects that are not represented; supported fills, borders and character styles are retained.');
|
|
161
|
+
const current = part['a:tcTxStyle'] ?? {};
|
|
162
|
+
// Resolve choices after inheritance so a later phClr can use an inherited
|
|
163
|
+
// fontRef, and an explicit font collection replaces a themed reference.
|
|
164
|
+
if (current['a:font'] || current['a:fontRef']) {delete textStyle['a:font'];delete textStyle['a:fontRef'];}
|
|
165
|
+
if (colorKinds.some(key => Object.hasOwn(current, key))) for (const key of colorKinds) delete textStyle[key];
|
|
166
|
+
for (const [key, value] of Object.entries(current)) {
|
|
167
|
+
if (['b','i'].includes(key) && value === 'def') continue;
|
|
168
|
+
textStyle[key] = value;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {text:tableTextProperties(textStyle, context),cell:{...cellStyle,...nativeBorderProperties(borders,extent)}};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Use the same direct graphic-frame ordering as the slide collector. The ordered
|
|
175
|
+
// reader retains interleaved runs, fields, breaks, empty paragraphs and spaces.
|
|
176
|
+
export function importTableFrames(slidePath, archive, relationships, report, dimensions) {
|
|
177
|
+
const context = readSlideTheme(slidePath, archive);
|
|
178
|
+
const scale = Math.min(dimensions.widthInches,dimensions.heightInches) * 96 / 720;
|
|
179
|
+
const root = child(context.parsedPart(slidePath)?.tree, 'p:sld');
|
|
180
|
+
const tree = child(child(root, 'p:cSld'), 'p:spTree');
|
|
181
|
+
return nodes(tree, 'p:graphicFrame').map((frame, frameIndex) => {
|
|
182
|
+
const table = child(child(child(frame['p:graphicFrame'], 'a:graphic'), 'a:graphicData'), 'a:tbl');
|
|
183
|
+
if (!table) return undefined;
|
|
184
|
+
const props = child(table, 'a:tblPr');
|
|
185
|
+
const style = tableStyle(props, archive, context, (code, message) => report(frameIndex, '', code, message));
|
|
186
|
+
if (style?.['a:tblBg']) report(frameIndex, '', 'unsupported-table-cell-style', 'The native table style background is not represented; supported character styles are retained.');
|
|
187
|
+
const properties = attrs(table, 'a:tblPr');
|
|
188
|
+
if (boolean(properties.rtl) === true) report(frameIndex, '', 'unsupported-table-direction', 'Right-to-left table geometry is not represented; cells retain native source order.');
|
|
189
|
+
const headers = boolean(properties.firstRow) === true;
|
|
190
|
+
const nativeRows = nodes(table, 'a:tr');
|
|
191
|
+
const columnCount = nodes(child(table, 'a:tblGrid'), 'a:gridCol').length;
|
|
192
|
+
const rows = nativeRows.map((row, rowIndex) => nodes(row['a:tr'], 'a:tc').map((cell, columnIndex) => {
|
|
193
|
+
const cellPath = `rows.${rowIndex}.${columnIndex}`;
|
|
194
|
+
const reported = new Set();
|
|
195
|
+
const emit = (code, message) => {if (!reported.has(code)) {reported.add(code);report(frameIndex, cellPath, code, message);}};
|
|
196
|
+
const geometry = cell[':@'] ?? {};
|
|
197
|
+
const defaults = conditionalProperties(style, properties, rowIndex, columnIndex, nativeRows.length, columnCount, context, emit, geometry);
|
|
198
|
+
const body = child(cell['a:tc'],'a:txBody');
|
|
199
|
+
const cellProperties = {...attrs(cell['a:tc'],'a:tcPr'),...drawingObject(child(cell['a:tc'],'a:tcPr'))};
|
|
200
|
+
const inherited=mergeProperties(defaults.cell,cellProperties);
|
|
201
|
+
return {
|
|
202
|
+
geometry,
|
|
203
|
+
value:cellText(body, context, relationships, emit, headers && rowIndex === 0, defaults.text),
|
|
204
|
+
style:nativeCellStyle(mergeCellBorderOverrides(defaults.cell,cellProperties,inherited),body,context,scale,emit),
|
|
205
|
+
borderEdges:[['left','a:lnL'],['right','a:lnR'],['top','a:lnT'],['bottom','a:lnB']]
|
|
206
|
+
.filter(([,key])=>Object.hasOwn(inherited,key)).map(([edge])=>edge),
|
|
207
|
+
};
|
|
208
|
+
}));
|
|
209
|
+
const grid = nativeTableGrid(rows,columnCount,headers,(cell,code,message)=>report(frameIndex,cell,code,message));
|
|
210
|
+
return grid.headers && grid.rows.length ? {columns:grid.rows[0], rows:grid.rows.slice(1)} : {rows:grid.rows};
|
|
211
|
+
});
|
|
212
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-pptx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -35,21 +35,22 @@
|
|
|
35
35
|
"sideEffects": false,
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "node scripts/build.mjs",
|
|
38
|
-
"typecheck": "node --check src/index.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && 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",
|
|
39
|
-
"test": "npm run build && node test/dependency-boundary.mjs && npm run build:browser-check",
|
|
38
|
+
"typecheck": "node --check src/index.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && 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",
|
|
39
|
+
"test": "npm run build && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table",
|
|
40
40
|
"validate": "node scripts/validate-package.mjs",
|
|
41
41
|
"prepack": "npm run build",
|
|
42
|
-
"build:browser-check": "node scripts/build-browser-check.mjs"
|
|
42
|
+
"build:browser-check": "node scripts/build-browser-check.mjs",
|
|
43
|
+
"test:styled-table": "node test/styled-table.mjs && node test/styled-table-import.mjs"
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
45
|
-
"@openpresentation/opf": "^0.
|
|
46
|
+
"@openpresentation/opf": "^0.7.0",
|
|
46
47
|
"fast-xml-parser": "^5.8.0",
|
|
47
48
|
"fflate": "^0.8.3",
|
|
48
49
|
"pptxgenjs": "4.0.1",
|
|
49
50
|
"sharp": "0.35.4"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|
|
52
|
-
"@openpresentation/opf-render": "^0.
|
|
53
|
+
"@openpresentation/opf-render": "^0.5.0"
|
|
53
54
|
},
|
|
54
55
|
"peerDependenciesMeta": {
|
|
55
56
|
"@openpresentation/opf-render": {
|
|
@@ -57,7 +58,7 @@
|
|
|
57
58
|
}
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
60
|
-
"@openpresentation/opf-render": "0.
|
|
61
|
+
"@openpresentation/opf-render": "0.5.0",
|
|
61
62
|
"esbuild": "0.28.2"
|
|
62
63
|
},
|
|
63
64
|
"imports": {
|