@openpresentation/opf 0.5.0 → 0.6.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.
@@ -1,4 +1,4 @@
1
- import { MAX_COMPOSITION_DEPTH } from './chunk-WG5A4KZG.js';
1
+ import { MAX_COMPOSITION_DEPTH } from './chunk-C7GKRLI6.js';
2
2
  import { schemas } from './chunk-KSN6RQG2.js';
3
3
  import { catalogSchemaNames } from './chunk-TWMRZ43O.js';
4
4
  import Ajv2020 from 'ajv/dist/2020.js';
@@ -1,5 +1,5 @@
1
- import { assertValidPresentation } from './chunk-PFJE2IW4.js';
2
- import { resolveFontFamilies, resolveCanvasDimensions, composeSlide } from './chunk-WG5A4KZG.js';
1
+ import { assertValidPresentation } from './chunk-23MXJL2K.js';
2
+ import { resolveFontFamilies, resolveCanvasDimensions, composeSlide } from './chunk-C7GKRLI6.js';
3
3
  import { catalogs } from './chunk-TWMRZ43O.js';
4
4
 
5
5
  // src/pagination.ts
@@ -160,6 +160,16 @@ function richTextLayouter(input, box, requestedSize, options) {
160
160
  y += height;
161
161
  return line;
162
162
  });
163
+ if (options.uniformLineHeight) {
164
+ const height = Math.max(...richLines.map((line) => line.height));
165
+ y = 0;
166
+ for (const line of richLines) {
167
+ line.baseline += y - line.y;
168
+ line.y = y;
169
+ line.height = height;
170
+ y += height;
171
+ }
172
+ }
163
173
  return { lines: ranges.map((range) => whole.slice(range.start, range.end)), fontSize, lineHeight: Math.max(fontSize * 1.22, ...richLines.map((line) => line.height)), richLines, height: y, overflow: y > box.height + 0.01 || richLines.some((line) => line.width > box.width + 0.01) };
164
174
  };
165
175
  return layout;
@@ -234,17 +244,48 @@ ${flatten(entry.description)}` : ""}`;
234
244
  if (field === "quote") return flatten(record(value).text ?? value);
235
245
  return void 0;
236
246
  }
237
- function tableOverflows(value, box, scale, settings, options) {
238
- const table = record(value), rows2 = Array.isArray(table.rows) ? table.rows : [];
239
- const allRows = Array.isArray(table.columns) && table.columns.length ? [table.columns, ...rows2] : rows2;
240
- const columns2 = Math.max(1, ...allRows.map((row) => row.length));
241
- const width = box.width / columns2 - 20 * scale;
242
- const height = Math.min(54 * scale, box.height / Math.max(1, allRows.length)) - 12 * scale;
243
- return width <= 0 || height <= 0 || allRows.some((row, index) => row.some((value2) => {
244
- const style = { fontFamily: options.fonts?.body ?? "sans-serif", fontWeight: index === 0 && table.columns?.length ? 700 : 400 };
245
- const cell = { width, height }, minimum = (settings.minFontSize ?? 16) * scale;
246
- return (Array.isArray(value2) ? fitRichText(value2, cell, 15 * scale, minimum, { style, textMeasurement: options.textMeasurement }) : fitText(flatten(value2), cell, 15 * scale, minimum, textWidthMeasurer(resolveTextStyle(style, options.textMeasurement), options.textMeasurement))).overflow;
247
- }));
247
+ function layoutTable(value, box, options = {}) {
248
+ const scale = options.scale ?? 1, requested = 15 * scale, minimum = (options.minFontSize ?? 16) * scale;
249
+ if (![box.x, box.y, box.width, box.height, scale, minimum].every(Number.isFinite) || box.width <= 0 || box.height <= 0 || scale <= 0 || minimum <= 0) throw new RangeError("Table dimensions, scale and font sizes must be finite and positive.");
250
+ const table = record(value), headers = Array.isArray(table.columns) && table.columns.length > 0;
251
+ const values = [...headers ? [table.columns] : [], ...Array.isArray(table.rows) ? table.rows : []];
252
+ const columnCount = Math.max(1, ...values.map((row) => row.length)), cellWidth = box.width / columnCount;
253
+ const width = Math.max(scale, cellWidth - 20 * scale), padding = 12 * scale;
254
+ const fitCell = (cell, height, min) => {
255
+ const textBox = { width, height: Math.max(scale, height) };
256
+ return Array.isArray(cell.value) ? fitRichText(cell.value, textBox, requested, min, { style: cell.style, textMeasurement: options.textMeasurement, uniformLineHeight: true }) : fitText(flatten(cell.value), textBox, requested, min, textWidthMeasurer(resolveTextStyle(cell.style, options.textMeasurement), options.textMeasurement));
257
+ };
258
+ const textHeight = (fit) => "height" in fit ? fit.height : fit.lines.length * fit.lineHeight;
259
+ const measured = values.map((row, index) => {
260
+ const header = headers && index === 0;
261
+ const cells = Array.from({ length: columnCount }, (_, column) => {
262
+ const path = `${options.path ?? "table"}.${header ? "columns" : `rows.${index - Number(headers)}`}.${column}`;
263
+ return { value: row[column], path, header, style: { fontFamily: options.fontFamily ?? "sans-serif", fontWeight: header ? 700 : 400, italic: false, path } };
264
+ });
265
+ const natural2 = Math.max(...cells.map((cell) => textHeight(fitCell(cell, scale, requested)) + padding));
266
+ const preferred2 = Math.max(54 * scale, natural2);
267
+ const needed2 = Math.min(preferred2, Math.max(...cells.map((cell) => textHeight(fitCell(cell, scale, minimum)) + padding)));
268
+ return { cells, preferred: preferred2, natural: natural2, needed: needed2 };
269
+ });
270
+ const preferred = measured.reduce((sum, row) => sum + row.preferred, 0), natural = measured.reduce((sum, row) => sum + row.natural, 0), needed = measured.reduce((sum, row) => sum + row.needed, 0);
271
+ let y = box.y, overflow = cellWidth <= 20 * scale;
272
+ const rows2 = measured.map((row) => {
273
+ const height = preferred <= box.height ? row.preferred : natural <= box.height ? row.natural + (row.preferred - row.natural) * (box.height - natural) / Math.max(Number.EPSILON, preferred - natural) : needed <= box.height ? row.needed + (row.natural - row.needed) * (box.height - needed) / Math.max(Number.EPSILON, natural - needed) : row.needed * box.height / needed;
274
+ const rowBox = { x: box.x, y, width: box.width, height };
275
+ const cells = row.cells.map((cell, column) => {
276
+ const cellBox = { x: box.x + column * cellWidth, y, width: cellWidth, height };
277
+ const textBox = { x: cellBox.x + 10 * scale, y: y + 8 * scale, width, height: Math.max(scale, height - padding) };
278
+ const fit = fitCell(cell, textBox.height, minimum);
279
+ overflow ||= height <= padding || fit.overflow;
280
+ return { value: cell.value, path: cell.path, header: cell.header, rich: Array.isArray(cell.value), box: cellBox, textBox, textStyle: resolveTextStyle(cell.style, options.textMeasurement), fit };
281
+ });
282
+ y += height;
283
+ return { box: rowBox, cells };
284
+ });
285
+ return { rows: rows2, columnCount, height: y - box.y, overflow };
286
+ }
287
+ function tableOverflows(value, box, scale, settings, options, path) {
288
+ return box.width <= 0 || box.height <= 0 || layoutTable(value, box, { scale, minFontSize: settings.minFontSize, fontFamily: options.fonts?.body, textMeasurement: options.textMeasurement, path }).overflow;
248
289
  }
249
290
  function flatten(value) {
250
291
  if (value == null) return "";
@@ -373,7 +414,7 @@ function composeSlide(input, options = {}) {
373
414
  const fit = fitContent(node.field, node.value, text, box, 25 * scale, (settings.minFontSize ?? 16) * scale, node.path);
374
415
  score += (25 * scale - fit.fontSize) / scale + (fit.overflow ? 1e3 : 0);
375
416
  }
376
- if (node.field === "table" && tableOverflows(node.value, box, scale, settings, options)) score += 1e3;
417
+ if (node.field === "table" && tableOverflows(node.value, box, scale, settings, options, node.path)) score += 1e3;
377
418
  if (box.width < 100 * scale || box.height < 60 * scale) score += 100;
378
419
  return score;
379
420
  };
@@ -428,7 +469,7 @@ function composeSlide(input, options = {}) {
428
469
  arrange(pending, contentBox, rootSettings, composition.mode ? 0 : placeholders.length);
429
470
  for (const item of items) {
430
471
  for (const key of ["x", "y", "width", "height"]) item.box[key] = round(item.box[key]);
431
- if (item.field === "table" && tableOverflows(item.value, item.box, scale, item.composition, options)) diagnostics.push({ code: "text-overflow", path: item.path, message: "Table cells do not fit; use fewer rows, fewer columns, or split the table across slides." });
472
+ if (item.field === "table" && tableOverflows(item.value, item.box, scale, item.composition, options, item.path)) diagnostics.push({ code: "text-overflow", path: item.path, message: "Table cells do not fit; use fewer rows, fewer columns, or split the table across slides." });
432
473
  if (item.text?.overflow) diagnostics.push({ code: "text-overflow", path: item.path, message: "Text exceeds its cell at the minimum font size; shorten it, increase its space, or split the slide." });
433
474
  }
434
475
  for (const group of groups) for (const box of [group.box, group.contentBox]) for (const key of ["x", "y", "width", "height"]) box[key] = round(box[key]);
@@ -455,4 +496,4 @@ function resolveCanvasDimensions(input) {
455
496
  return { width, height };
456
497
  }
457
498
 
458
- export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, wrapText };
499
+ export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, wrapText };
@@ -144,6 +144,8 @@ interface RichTextFit extends TextFit {
144
144
  interface RichTextOptions {
145
145
  style: TextStyle;
146
146
  textMeasurement?: TextMeasurement;
147
+ /** Use one measured line advance for every line, as native table cells do. */
148
+ uniformLineHeight?: boolean;
147
149
  }
148
150
  /** Fit mixed styles without flattening font metrics. Run fontSize is in points. */
149
151
  declare function fitRichText(input: readonly (string | RichTextRun)[], box: LayoutBox, requestedSize?: number, minFontSize?: number, options?: RichTextOptions): RichTextFit;
@@ -179,6 +181,39 @@ interface ListFit extends TextFit {
179
181
  }
180
182
  /** Shared hanging indents, mixed-run fitting and description spacing for list payloads. */
181
183
  declare function fitList(input: readonly ListValue[], box: LayoutBox, requestedSize?: number, minFontSize?: number, options?: RichTextOptions): ListFit;
184
+ interface TableLayoutOptions {
185
+ scale?: number;
186
+ minFontSize?: number;
187
+ fontFamily?: string;
188
+ textMeasurement?: TextMeasurement;
189
+ path?: string;
190
+ }
191
+ interface TableCellLayout {
192
+ value: unknown;
193
+ path: string;
194
+ header: boolean;
195
+ rich: boolean;
196
+ box: LayoutBox;
197
+ textBox: LayoutBox;
198
+ textStyle: TextStyle;
199
+ fit: TextFit | RichTextFit;
200
+ }
201
+ interface TableRowLayout {
202
+ box: LayoutBox;
203
+ cells: TableCellLayout[];
204
+ }
205
+ interface TableLayout {
206
+ rows: TableRowLayout[];
207
+ columnCount: number;
208
+ height: number;
209
+ overflow: boolean;
210
+ }
211
+ /** Shared cell geometry and font fitting for SVG, native PPTX and pagination.
212
+ * Short rows retain their 54px preferred height. Multiline rows use the space
213
+ * their text needs; constrained tables consume row padding before readable text.
214
+ * All dimensions are canvas pixels; minFontSize is an unscaled canvas size.
215
+ */
216
+ declare function layoutTable(value: unknown, box: LayoutBox, options?: TableLayoutOptions): TableLayout;
182
217
  /** Compose a validated Slide. Layout resolution remains the caller's responsibility. */
183
218
  declare function composeSlide(input: unknown, options?: ComposeSlideOptions): SlideComposition;
184
219
  /** Canonical physical slide size, converted to reference pixels at 96 pixels/inch. */
@@ -187,4 +222,4 @@ declare function resolveCanvasDimensions(input: unknown): {
187
222
  height: number;
188
223
  };
189
224
 
190
- export { type ComposeSlideOptions, type ComposedFlow, type ComposedGroup, type ComposedItem, type Composition, type CompositionTrack, type FontFamilies, type LayoutBox, type LayoutDiagnostic, type ListEntryLayout, type ListFit, type ListText, type ListValue, MAX_COMPOSITION_DEPTH, type MeasureTextWidth, OPFCompositionError, type RichTextFit, type RichTextFragment, type RichTextLine, type RichTextOptions, type RichTextRun, type SlideComposition, type TextFit, type TextMeasurement, type TextStyle, composeSlide, fitList, fitRichText, fitText, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, wrapText };
225
+ export { type ComposeSlideOptions, type ComposedFlow, type ComposedGroup, type ComposedItem, type Composition, type CompositionTrack, type FontFamilies, type LayoutBox, type LayoutDiagnostic, type ListEntryLayout, type ListFit, type ListText, type ListValue, MAX_COMPOSITION_DEPTH, type MeasureTextWidth, OPFCompositionError, type RichTextFit, type RichTextFragment, type RichTextLine, type RichTextOptions, type RichTextRun, type SlideComposition, type TableCellLayout, type TableLayout, type TableLayoutOptions, type TableRowLayout, type TextFit, type TextMeasurement, type TextStyle, composeSlide, fitList, fitRichText, fitText, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, wrapText };
@@ -1 +1 @@
1
- export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, wrapText } from './chunk-WG5A4KZG.js';
1
+ export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, layoutTable, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, wrapText } from './chunk-C7GKRLI6.js';
package/dist/docs.js CHANGED
@@ -22,7 +22,7 @@ var docsData = Object.freeze([
22
22
  "slug": "content-payloads",
23
23
  "file": "docs/content-payloads.md",
24
24
  "title": "Content Payloads",
25
- "markdown": '# Content Payloads\n\nSlide content lives directly on a slide as a full-slide payload, in layout-agnostic `blocks`, or inside a promoted region key such as `left`, `center+right`, or `top:left`.\n\nThe optional payload `type` can make intent explicit, but OPF should usually infer the content kind from the field present:\n\n| Field | Inferred type | Notes |\n| --- | --- | --- |\n| `text` | `text` | Plain string or `TextRun[]`. |\n| `bullets` | `text` | Simple text bullets, usually `string[]`. |\n| `items` | `list` | Generic list payload, usually `string[]` or `ListItem[]`. |\n| `image` | `image` | Asset string shorthand or `Asset` object with `src` and optional metadata. |\n| `video` | `video` | Asset string shorthand or `Asset` object with `src` and optional metadata. |\n| `chart` | `chart` | Chart object with `type` and tabular `data`. |\n| `table` | `table` | Table object with optional `columns` and required `rows`. |\n| `code` | `code` | String shorthand or `Code` object with `source`, `language`, and `filename`. |\n| `metric` | `metric` | String/number shorthand or `Metric` object with `value`, `label`, `description`, `unit`, `delta`, and `trend`. |\n| `quote` | `quote` | String shorthand or `Quote` object with `text`, `attribution`, and `source`. |\n| `timeline` | `timeline` | Array shorthand or `Timeline` object with `name`, `description`, and `events`. |\n\n## Blocks\n\nUse slide-level `blocks` when a slide contains multiple content payloads, but exact placement should be inferred by the renderer. Blocks may contain a concrete content payload or a nested group with its own `blocks` and optional `composition`. Groups cannot mix child blocks with leaf payload fields. See [dynamic composition](dynamic-composition.md) for nesting and inheritance rules.\n\n```json\n{\n "title": "Customer Feedback Summary",\n "blocks": [\n {\n "table": {\n "columns": ["Theme", "Mentions"],\n "rows": [\n ["Speed", 42],\n ["Ease of use", 31]\n ]\n }\n },\n {\n "quote": {\n "text": "The new workflow cut review time in half.",\n "attribution": "Operations Lead",\n "source": "Customer interview"\n }\n }\n ]\n}\n```\n\nAt slide root only, multiple content payload kinds are accepted as shorthand for the equivalent blocks form when there is no explicit `type`, no `blocks`, and no promoted region keys:\n\n```json\n{\n "title": "Habitat & Territory",\n "text": "Jaguars are strongly associated with presence of water and dense cover.",\n "items": [\n "Primary habitats include dense rainforests, swamps, and seasonally flooded wetlands.",\n "Solitary animals that establish and defend large territories."\n ]\n}\n```\n\nThe same shorthand works for other content kinds:\n\n```json\n{\n "title": "Evidence Snapshot",\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Quarter", "Sightings"],\n "rows": [\n ["Q1", 12],\n ["Q2", 18]\n ]\n }\n },\n "quote": {\n "text": "Jaguar conservation depends on connected habitat.",\n "attribution": "Field researcher"\n }\n}\n```\n\n## Chart\n\nChart-specific fields are grouped under `chart`. Do not put loose chart data directly on a slide or region.\n\n```json\n{\n "title": "Revenue Trend",\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Quarter", "Revenue", "Costs"],\n "rows": [\n ["Q1", 12, 8],\n ["Q2", 18, 11],\n ["Q3", 24, 15]\n ]\n }\n }\n}\n```\n\nInline chart data is tabular by default. Renderers convert `columns` and `rows` into series, axes, legends, and workbook data internally.\n\nAsset-backed data is still table-oriented:\n\n```json\n{\n "chart": {\n "type": "column",\n "data": {\n "src": "asset:revenue-csv",\n "columns": ["Quarter", "Revenue"]\n }\n }\n}\n```\n\n## Table\n\nTable-specific fields are grouped under `table`. Do not put loose `columns` or `rows` directly on a slide or region.\n\n```json\n{\n "title": "Pipeline",\n "table": {\n "columns": ["Stage", "Count", "Value"],\n "rows": [\n ["Qualified", 42, "$1.2M"],\n ["Proposal", 18, "$840K"]\n ]\n }\n}\n```\n\nTable body cells accept strings, numbers, booleans, or `null`. In the development schema, a cell or column header also accepts the same `TextRun[]` used by rich text:\n\n```json\n{\n "table": {\n "columns": [["Quarter ", {"text": "growth", "bold": true}], "Value"],\n "rows": [\n [["Up ", {"text": "12%", "color": "#008800"}], 12]\n ]\n }\n}\n```\n\nThis form is unreleased and requires coordinated core, renderer and PPTX development packages. Core measures run styles when checking overflow and keeps each row intact when paginating. The renderer traces rich cells for the editor\'s existing formatting, typing and undo controls; the exporter emits editable native text runs. Native PPTX table import currently flattens runs to strings, and native PowerPoint visual parity is not yet verified. Per-cell fills, borders, alignment and merged cells are separate work.\n\n## Code\n\nCode-specific fields are grouped under `code`. A string value is shorthand for `code.source`; use object form when syntax highlighting or a file label matters. In object form, `source` is required.\n\n```json\n{\n "title": "Decision Rule",\n "code": {\n "source": "if risk > threshold:\\n escalate(owner)\\nelse:\\n approve(change)",\n "language": "python",\n "filename": "decision.py"\n }\n}\n```\n\n## Metric\n\nMetric-specific fields are grouped under `metric`. A string or number value is shorthand for `metric.value`; numeric values stay numeric and are formatted by renderers at display time. Use object form when labels, descriptions, units, deltas, or trends matter.\n\n```json\n{\n "title": "Operating Metric",\n "metric": {\n "value": "42%",\n "label": "Review cycle reduction",\n "description": "Median reduction across customer review workflows.",\n "delta": "+11 pts",\n "trend": "up"\n }\n}\n```\n\n## Quote\n\nQuote-specific fields are grouped under `quote`. A string value is shorthand for `quote.text`; use object form when attribution or citation matters.\n\n```json\n{\n "title": "Customer Proof",\n "quote": {\n "text": "The new workflow made exceptions visible before they became escalations.",\n "attribution": "VP Operations, Acme Corp",\n "source": "Customer interview"\n }\n}\n```\n\n## Timeline\n\nTimeline-specific fields are grouped under `timeline`. An array value is shorthand for `timeline.events`; use object form when the timeline needs a name or description. Timeline events use `when`, `what`, and `description`.\n\n```json\n{\n "title": "Rollout Plan",\n "timeline": {\n "name": "Regional Rollout",\n "description": "Major milestones for the rollout.",\n "events": [\n {\n "when": "Q1",\n "what": "Pilot",\n "description": "Launch with one operations team."\n },\n {\n "when": "Q2",\n "what": "Rollout",\n "description": "Expand to all regions."\n }\n ]\n }\n}\n```\n\n## Regions\n\nRegion keys address a 3\xD73 grid of rows (`top`, `middle`, `bottom`) and columns (`left`, `center`, `right`):\n\n```\n left center right\n +--------------------+--------------------+--------------------+\n top | top:left | top:center | top:right |\n +--------------------+--------------------+--------------------+\n middle | middle:left | middle:center | middle:right |\n +--------------------+--------------------+--------------------+\n bottom | bottom:left | bottom:center | bottom:right |\n +--------------------+--------------------+--------------------+\n```\n\n- A bare column key (`left`) spans all three rows; a bare row key (`top`) spans all three columns.\n- `+` spans adjacent rows or columns: `center+right`, `top+middle`.\n- `row:column` combines the two: `top:left`, `middle+bottom:center+right`.\n- Keys on one slide must not overlap, and regions cannot be mixed with root payload fields.\n\nSpans compose into common slide shapes:\n\n```\n "left" + "center+right" "top" + "middle+bottom"\n (sidebar + main) (headline band + body)\n +----------+------------------+ +-------------------------------+\n | | | | top |\n | | | +-------------------------------+\n | left | center+right | | |\n | | | | middle+bottom |\n | | | | |\n +----------+------------------+ +-------------------------------+\n\n "top" + "middle+bottom:left" + "middle+bottom:center+right"\n (headline band, then sidebar + main)\n +---------------------------------------------+\n | top |\n +---------------+-----------------------------+\n | | |\n | middle+bottom | middle+bottom:center+right |\n | :left | |\n | | |\n +---------------+-----------------------------+\n```\n\nThe same payload objects work inside regions \u2014 here, the sidebar-plus-main shape:\n\n```json\n{\n "title": "Operating Snapshot",\n "left": {\n "table": {\n "columns": ["Metric", "Value"],\n "rows": [\n ["Revenue", "$4.2M"],\n ["Gross margin", "68%"]\n ]\n }\n },\n "center+right": {\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Month", "Revenue"],\n "rows": [\n ["Jan", 3.4],\n ["Feb", 3.8],\n ["Mar", 4.2]\n ]\n }\n }\n }\n}\n```\n'
25
+ "markdown": '# Content Payloads\n\nSlide content lives directly on a slide as a full-slide payload, in layout-agnostic `blocks`, or inside a promoted region key such as `left`, `center+right`, or `top:left`.\n\nThe optional payload `type` can make intent explicit, but OPF should usually infer the content kind from the field present:\n\n| Field | Inferred type | Notes |\n| --- | --- | --- |\n| `text` | `text` | Plain string or `TextRun[]`. |\n| `bullets` | `text` | Simple text bullets, usually `string[]`. |\n| `items` | `list` | Generic list payload, usually `string[]` or `ListItem[]`. |\n| `image` | `image` | Asset string shorthand or `Asset` object with `src` and optional metadata. |\n| `video` | `video` | Asset string shorthand or `Asset` object with `src` and optional metadata. |\n| `chart` | `chart` | Chart object with `type` and tabular `data`. |\n| `table` | `table` | Table object with optional `columns` and required `rows`. |\n| `code` | `code` | String shorthand or `Code` object with `source`, `language`, and `filename`. |\n| `metric` | `metric` | String/number shorthand or `Metric` object with `value`, `label`, `description`, `unit`, `delta`, and `trend`. |\n| `quote` | `quote` | String shorthand or `Quote` object with `text`, `attribution`, and `source`. |\n| `timeline` | `timeline` | Array shorthand or `Timeline` object with `name`, `description`, and `events`. |\n\n## Blocks\n\nUse slide-level `blocks` when a slide contains multiple content payloads, but exact placement should be inferred by the renderer. Blocks may contain a concrete content payload or a nested group with its own `blocks` and optional `composition`. Groups cannot mix child blocks with leaf payload fields. See [dynamic composition](dynamic-composition.md) for nesting and inheritance rules.\n\n```json\n{\n "title": "Customer Feedback Summary",\n "blocks": [\n {\n "table": {\n "columns": ["Theme", "Mentions"],\n "rows": [\n ["Speed", 42],\n ["Ease of use", 31]\n ]\n }\n },\n {\n "quote": {\n "text": "The new workflow cut review time in half.",\n "attribution": "Operations Lead",\n "source": "Customer interview"\n }\n }\n ]\n}\n```\n\nAt slide root only, multiple content payload kinds are accepted as shorthand for the equivalent blocks form when there is no explicit `type`, no `blocks`, and no promoted region keys:\n\n```json\n{\n "title": "Habitat & Territory",\n "text": "Jaguars are strongly associated with presence of water and dense cover.",\n "items": [\n "Primary habitats include dense rainforests, swamps, and seasonally flooded wetlands.",\n "Solitary animals that establish and defend large territories."\n ]\n}\n```\n\nThe same shorthand works for other content kinds:\n\n```json\n{\n "title": "Evidence Snapshot",\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Quarter", "Sightings"],\n "rows": [\n ["Q1", 12],\n ["Q2", 18]\n ]\n }\n },\n "quote": {\n "text": "Jaguar conservation depends on connected habitat.",\n "attribution": "Field researcher"\n }\n}\n```\n\n## Chart\n\nChart-specific fields are grouped under `chart`. Do not put loose chart data directly on a slide or region.\n\n```json\n{\n "title": "Revenue Trend",\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Quarter", "Revenue", "Costs"],\n "rows": [\n ["Q1", 12, 8],\n ["Q2", 18, 11],\n ["Q3", 24, 15]\n ]\n }\n }\n}\n```\n\nInline chart data is tabular by default. Renderers convert `columns` and `rows` into series, axes, legends, and workbook data internally.\n\nAsset-backed data is still table-oriented:\n\n```json\n{\n "chart": {\n "type": "column",\n "data": {\n "src": "asset:revenue-csv",\n "columns": ["Quarter", "Revenue"]\n }\n }\n}\n```\n\n## Table\n\nTable-specific fields are grouped under `table`. Do not put loose `columns` or `rows` directly on a slide or region.\n\n```json\n{\n "title": "Pipeline",\n "table": {\n "columns": ["Stage", "Count", "Value"],\n "rows": [\n ["Qualified", 42, "$1.2M"],\n ["Proposal", 18, "$840K"]\n ]\n }\n}\n```\n\nTable body cells accept strings, numbers, booleans, or `null`. Since core 0.5.0, a cell or column header also accepts the same `TextRun[]` used by rich text:\n\n```json\n{\n "table": {\n "columns": [["Quarter ", {"text": "growth", "bold": true}], "Value"],\n "rows": [\n [["Up ", {"text": "12%", "color": "#008800"}], 12]\n ]\n }\n}\n```\n\nUse core 0.5.0, renderer 0.3.0, editor 0.2.0 and PPTX 0.3.0 together for this form. Core measures run styles when checking overflow and keeps each row intact when paginating. The renderer traces rich cells for the editor\'s existing formatting, typing and undo controls; the exporter emits editable native text runs. Native PPTX table import currently flattens runs to strings, and native PowerPoint visual parity is not yet verified. Per-cell fills, borders, alignment and merged cells are separate work.\n\nThe current development line (core 0.6.0) adds `layoutTable` from `@openpresentation/opf/composition`. It measures scalar and rich cells, keeps short rows compact, and gives wrapped or multiline rows the height they need. When space is constrained it reduces spare row height before shrinking text, and reports overflow when the minimum fitting size cannot fit. Pass the same `scale`, font family, measurement provider and effective `minFontSize` to each consumer. The returned row boxes, cell text boxes and fits are shared by the coordinated SVG and PPTX implementations; rich table cells use uniform line advances to match native cell paragraph spacing. These downstream changes and native rich-table import are not yet published. Native viewer fidelity remains a separate verification boundary.\n\n\n## Code\n\nCode-specific fields are grouped under `code`. A string value is shorthand for `code.source`; use object form when syntax highlighting or a file label matters. In object form, `source` is required.\n\n```json\n{\n "title": "Decision Rule",\n "code": {\n "source": "if risk > threshold:\\n escalate(owner)\\nelse:\\n approve(change)",\n "language": "python",\n "filename": "decision.py"\n }\n}\n```\n\n## Metric\n\nMetric-specific fields are grouped under `metric`. A string or number value is shorthand for `metric.value`; numeric values stay numeric and are formatted by renderers at display time. Use object form when labels, descriptions, units, deltas, or trends matter.\n\n```json\n{\n "title": "Operating Metric",\n "metric": {\n "value": "42%",\n "label": "Review cycle reduction",\n "description": "Median reduction across customer review workflows.",\n "delta": "+11 pts",\n "trend": "up"\n }\n}\n```\n\n## Quote\n\nQuote-specific fields are grouped under `quote`. A string value is shorthand for `quote.text`; use object form when attribution or citation matters.\n\n```json\n{\n "title": "Customer Proof",\n "quote": {\n "text": "The new workflow made exceptions visible before they became escalations.",\n "attribution": "VP Operations, Acme Corp",\n "source": "Customer interview"\n }\n}\n```\n\n## Timeline\n\nTimeline-specific fields are grouped under `timeline`. An array value is shorthand for `timeline.events`; use object form when the timeline needs a name or description. Timeline events use `when`, `what`, and `description`.\n\n```json\n{\n "title": "Rollout Plan",\n "timeline": {\n "name": "Regional Rollout",\n "description": "Major milestones for the rollout.",\n "events": [\n {\n "when": "Q1",\n "what": "Pilot",\n "description": "Launch with one operations team."\n },\n {\n "when": "Q2",\n "what": "Rollout",\n "description": "Expand to all regions."\n }\n ]\n }\n}\n```\n\n## Regions\n\nRegion keys address a 3\xD73 grid of rows (`top`, `middle`, `bottom`) and columns (`left`, `center`, `right`):\n\n```\n left center right\n +--------------------+--------------------+--------------------+\n top | top:left | top:center | top:right |\n +--------------------+--------------------+--------------------+\n middle | middle:left | middle:center | middle:right |\n +--------------------+--------------------+--------------------+\n bottom | bottom:left | bottom:center | bottom:right |\n +--------------------+--------------------+--------------------+\n```\n\n- A bare column key (`left`) spans all three rows; a bare row key (`top`) spans all three columns.\n- `+` spans adjacent rows or columns: `center+right`, `top+middle`.\n- `row:column` combines the two: `top:left`, `middle+bottom:center+right`.\n- Keys on one slide must not overlap, and regions cannot be mixed with root payload fields.\n\nSpans compose into common slide shapes:\n\n```\n "left" + "center+right" "top" + "middle+bottom"\n (sidebar + main) (headline band + body)\n +----------+------------------+ +-------------------------------+\n | | | | top |\n | | | +-------------------------------+\n | left | center+right | | |\n | | | | middle+bottom |\n | | | | |\n +----------+------------------+ +-------------------------------+\n\n "top" + "middle+bottom:left" + "middle+bottom:center+right"\n (headline band, then sidebar + main)\n +---------------------------------------------+\n | top |\n +---------------+-----------------------------+\n | | |\n | middle+bottom | middle+bottom:center+right |\n | :left | |\n | | |\n +---------------+-----------------------------+\n```\n\nThe same payload objects work inside regions \u2014 here, the sidebar-plus-main shape:\n\n```json\n{\n "title": "Operating Snapshot",\n "left": {\n "table": {\n "columns": ["Metric", "Value"],\n "rows": [\n ["Revenue", "$4.2M"],\n ["Gross margin", "68%"]\n ]\n }\n },\n "center+right": {\n "chart": {\n "type": "line",\n "data": {\n "columns": ["Month", "Revenue"],\n "rows": [\n ["Jan", 3.4],\n ["Feb", 3.8],\n ["Mar", 4.2]\n ]\n }\n }\n }\n}\n```\n'
26
26
  },
27
27
  {
28
28
  "slug": "data-import",
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  export { specFileEntries, specFileKinds, specFilePaths } from './chunk-OZULJBSG.js';
2
2
  export { getLayoutPreview, hasLayoutPreview, layoutPreviewIndex, layoutPreviewSlugs, layoutPreviews } from './chunk-PIQXLBBJ.js';
3
3
  export { OPFDataImportError, createDataContent, parseTabularData } from './chunk-SOSOBKQ2.js';
4
- export { OPFPaginationError, paginatePresentation, paginateSlide } from './chunk-YLORKB63.js';
5
- export { OPFValidationError, assertValid, assertValidCatalogRecord, assertValidPresentation, validate, validateCatalogRecord, validatePresentation } from './chunk-PFJE2IW4.js';
6
- export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, wrapText } from './chunk-WG5A4KZG.js';
4
+ export { OPFPaginationError, paginatePresentation, paginateSlide } from './chunk-C6U7LLBT.js';
5
+ export { OPFValidationError, assertValid, assertValidCatalogRecord, assertValidPresentation, validate, validateCatalogRecord, validatePresentation } from './chunk-23MXJL2K.js';
6
+ export { MAX_COMPOSITION_DEPTH, OPFCompositionError, composeSlide, fitList, fitRichText, fitText, measureText, resolveCanvasDimensions, resolveFontFamilies, resolveTextStyle, textWidthMeasurer, wrapText } from './chunk-C7GKRLI6.js';
7
7
  export { audience, chartType, colorScheme, fontScheme, language, layout, narrative, presentation, purpose, schemaEntries, schemaNames, schemas, socialPlatform, theme, tone } from './chunk-KSN6RQG2.js';
8
8
  export { audiences, catalogEntries, catalogIndexes, catalogKinds, catalogSchemaNames, catalogs, chartTypes, colorSchemes, fontSchemes, languages, layouts, narratives, purposes, socialPlatforms, themes, tones } from './chunk-TWMRZ43O.js';
@@ -1,5 +1,5 @@
1
- export { OPFPaginationError, paginatePresentation, paginateSlide } from './chunk-YLORKB63.js';
2
- import './chunk-PFJE2IW4.js';
3
- import './chunk-WG5A4KZG.js';
1
+ export { OPFPaginationError, paginatePresentation, paginateSlide } from './chunk-C6U7LLBT.js';
2
+ import './chunk-23MXJL2K.js';
3
+ import './chunk-C7GKRLI6.js';
4
4
  import './chunk-KSN6RQG2.js';
5
5
  import './chunk-TWMRZ43O.js';
package/dist/validator.js CHANGED
@@ -1,4 +1,4 @@
1
- export { OPFValidationError, assertValid, assertValidCatalogRecord, assertValidPresentation, validate, validateCatalogRecord, validatePresentation } from './chunk-PFJE2IW4.js';
2
- import './chunk-WG5A4KZG.js';
1
+ export { OPFValidationError, assertValid, assertValidCatalogRecord, assertValidPresentation, validate, validateCatalogRecord, validatePresentation } from './chunk-23MXJL2K.js';
2
+ import './chunk-C7GKRLI6.js';
3
3
  import './chunk-KSN6RQG2.js';
4
4
  import './chunk-TWMRZ43O.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Canonical Open Presentation Format schemas, catalogs, TypeScript types, and local validation.",
5
5
  "license": "MIT",
6
6
  "author": "Open Presentation",