OPF
DocsReferenceView source

Content conversions

@openpresentation/opf/convert (RR-26) holds the pure, deterministic converters behind the editor's "Content type" control, the list level keys, grouping, image-to-design and the slide split and merge. They need no renderer, fonts, DOM, network or model: a function takes OPF JSON and returns new OPF JSON.

Every conversion follows one contract:

  • Never invents content. Every word, number and date in the result came from the source. The only generated text is the fixed column headings of a timeline or metric table (When, What, Description, Label, Value, Unit, Delta, Trend, Sentiment), the --- row of a Markdown table, and the title= of a code fence; a heading row can be left off (headings: false).
  • Reports what it cannot carry. A result has lossless and loss, a list of names such as text formatting or list nesting levels. lossless is true exactly when loss is empty. Show loss before applying.
  • Refuses with a reason. A pair with no mapping, or content that does not fit, throws OPFConversionError (code: "not-convertible") with a message that says what to change. contentConversionTargets() returns the same refusal as available: false and reason.
  • Validates its output as OPF. A result that would not validate throws code: "invalid-output".
  • Is pure. The input is never changed and the same input gives the same output. Slide-level functions take and return plain slide or presentation objects; the host turns the difference into a patch, so a conversion is one undoable step (the editor does this in convertBlock).
js
import { contentConversionTargets, convertContent } from "@openpresentation/opf/convert";

const block = { items: ["Plan", { text: "Ship", description: "By June" }] };
contentConversionTargets(block);
// [{ kind: "text", lossless: false, loss: ["list item descriptions (kept as indented lines)"], ... },
//  { kind: "timeline", ... }, { kind: "table", lossless: true, loss: [] }]
const { payload, lossless, loss } = convertContent(block, "table");
// payload: { table: { rows: [["Plan", null], ["Ship", "By June"]] } }

A payload is a block, a slide or region that holds one content field, or a group of metric blocks. id, extensions, type (kept in step with the new kind) and any slide fields (title, design, notes, ...) stay on the result.

Block conversions

FromToLossless?What is lost or reported
textlistwhen every line is plain, not blank and not numberedblank lines; list numbering when 1. markers are stripped. Indentation (2 spaces or a tab) and -, *, •, +, 1. markers become nesting levels; formatting is kept
textquotewhen the text is plaintext formatting. A trailing dash line (— Name, Title, –, --, ~, -) is the attribution and a second one the source; a last line “Quote” — Name splits when the quote opens with a quote mark. Never guessed from a lone line, a dashed list or a line over 120 characters
textmetricwhen plainblank lines, text formatting. Refused when the first line is over 24 characters; first line is the value, the second the label, the rest the description
textcodewhen plaintext formatting. One fenced block (``` or ~~~) gives the source, the language (first word of the info string) and the file name (title=, filename=, file=); nothing is guessed
texttimelinewhen plainblank lines, text formatting. Lines like 2024 — Launch, Q1 2026: Pilot, Jan - Kickoff, 2026-03 give when; an indented line is the previous event's description; looseWhen: true also splits Label: text
texttablewhen plainMarkdown pipe tables (the separator row makes the headings), tab-separated lines, or delimiter/header options. Refused without that structure or with uneven rows
listtextwhen plain, no descriptions, levels in orderlist nesting levels (renumbered...) for gaps in levels; list item descriptions (kept as indented lines). Levels are written as 2-space indentation
listtimelineno when the list nests or is richlist nesting levels, text formatting. 2024 — Launch gives when; an item's description is the event's description
listtableyes without nestinglist nesting levels. One column, or text and description when any item has one; no headings are invented
quotetextyestext, then — attribution, role (the role joins the attribution after a comma), then — source (the source line is plain when there is no attribution or role); loses the quote photo
metrictextno with a trend or sentimentmetric trend, metric sentiment
codetextyes with fences: "auto" (default)A fenced block keeps the language and file name; with fences: "never" the loss is code language, code filename
timelinetextyestimeline name, timeline description for the metadata a text cannot hold; when: what, then the description indented
timelinelistyestimeline name, timeline description; when: what is the item text, the description its description
timelinetableyestimeline name, timeline description. Columns When, What, Description only for the fields used
charttablenochart type. Only inline columns and rows, or a dataset; a chart without them is refused
tablechartyesRefused unless every column has a plain label and every value after the first column is a number, a numeric string or empty; type column
tablelistnocolumn headings; table columns beyond the second (joined into the description); cell styles. First column is the item, the other columns the description. Merged cells are refused
tabletimelineyes when the headings are recognisedReads columns by heading (When/Date/Quarter..., What/Event/Milestone..., Description/Notes...) or by columns: { when, what, description }; column "X" for dropped columns, column heading "X" for a heading it did not recognise, text formatting. Refused without headings or an event column
tabletextyes for plain cellstext formatting, cell styles, merged cells, line breaks inside cells, empty rows. Markdown with headings, tab-separated without
tablemetric blocksyes when the headings are recognisedNeeds a Value column; Label, Unit, Delta, Trend, Sentiment, Description are read too; column "X", trend values other than up, down or flat, sentiment values other than positive, negative or neutral, text formatting
group of metric blockstableyesblock ids and extensions, group arrangement (composition) (a group's composition cannot sit on a table; a slide keeps its own). Columns only for the fields used

Images, videos and any other group have no content conversion.

Other helpers

FunctionWhat it doesLossless?
demoteListItems, promoteListItems, shiftListLevelsNest an item one level deeper (Tab) or one level up (Shift+Tab). A level is at most one more than the item above and never below 0; the first item cannot nest. Items under it move with it unless withChildren: false. Level 0 collapses back to the plain formyes; changed: false and a reason when nothing can move
convertListFormitems to bullets and backlist item descriptions when going to bullets
wrapBlocksPut the selected blocks of a slide or group into a new group, optionally with a compositionyes
unwrapGroupReplace a group in a blocks list by its blocksgroup arrangement (composition), group id, group extensions
blocksToRegions, regionsToBlocks, moveRegionMove blocks into named regions (one block each, no overlap), turn regions back into blocks in reading order, move or swap a regionregion placement going to blocks
promoteImageMove an image block into design.background (keeping its alt text, fit, focus, opacity and overlay) or design.watermark (opacity, default 0.1); refuses an occupied slot unless replace: true. Placing an image beside the content is not a move: set the block's placementimage alt text (watermark), the block's other treatments (image shape, image placement, ...), image title/description/mediaType/format, block id
demoteImageMove the background image or watermark back into the content as an image block, with the background's alt text, fit, focus, opacity and overlaybackground tile fit, watermark opacity
splitSlideSplit a slide by blocks (at: [indexes] or each: true); design, layout and the other slide fields are copied, headings repeat (repeatHeadings), the first slide keeps the id and notes, the others get free ids ending --2, --3. Regions split region by regionregion placement for regions
splitSlideOnOverflowSplit a slide that does not fit with the existing pagination (paginate, one slide as slide 1 of 1, host options for fonts and measurement) and return the pages mappingyes
mergeSlidesMerge consecutive slides: the first slide's id, headings and design win, blocks follow each other, notes are joined with a blank lineslide id "x", title of slide N, any other field that differs; regions are refused
unpaginateThe inverse of pagination for slides still as paginate returned them, given its pages: slices of text, items, rows, events and code are joined into the original leavesyes (the pagination readability floor stays in composition.minFontSize); refused when the slices are not consecutive

Slide-level functions return the replacement slides, the replaced range (start, deleteCount) and the new presentation; structure functions return the new slide and the path of what they touched.

Decisions

  • Column headings of timeline and metric tables are the schema's own field names. They are the only generated text and can be omitted; reading a table back needs headings (or explicit columns), because a role is never guessed from position.
  • Text-to-timeline and text-to-quote parse only unambiguous patterns; the rest stays text. Parsing a pattern is not loss, and a wrong guess would be invented structure.
  • Code to text writes a fenced block when the code has a language or file name, so the round trip is lossless. A fence and its title= are the visible cost; fences: "never" writes the bare source and reports the loss.
  • Un-paginate needs the pagination mapping. Without it, mergeSlides concatenates blocks and never glues text together, because pagination's exact slice boundaries are not recoverable from the slides.