@openpresentation/opf-editor 0.15.0 → 0.17.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.
@@ -394,6 +394,44 @@ export function readTableStyleOfTable(table) {
394
394
  // Table structure (RR-24): insert, delete, move and sort rows and columns, the header row, cell values.
395
395
  export * from "./table-structure.js";
396
396
 
397
+ // --- text alternative (FA-27) -----------------------------------------------------------------
398
+
399
+ /**
400
+ * The table's text alternative as form state, like a chart's: `alt` is the text (empty when absent or decorative) and `decorative`
401
+ * is the empty alt, a reviewed choice and not a missing one.
402
+ */
403
+ export function readTableAlt(table) {
404
+ return { alt: typeof table?.alt === "string" ? table.alt : "", decorative: table?.alt === "" };
405
+ }
406
+
407
+ /**
408
+ * Compute the patch that changes the table's text alternative. `change` is `{ alt?, decorative? }`: `alt` is trimmed and an empty string
409
+ * or `null` removes it, `decorative: true` writes the empty alt (and wins over `alt`), `decorative: false` removes an empty alt. It works
410
+ * on an inline table and on a table that shows a shared dataset, because `alt` belongs to the table and not to its data. The document
411
+ * is not modified.
412
+ */
413
+ export function prepareTableAlt(presentation, tablePath, change) {
414
+ const before = checkFormat(presentation);
415
+ const parts = splitOpfPath(tablePath);
416
+ const table = getValueAtPath(presentation, parts);
417
+ if (!isObject(table) || (!Array.isArray(table.rows) && typeof table.dataset !== "string")) throw fail("table-not-found", "Choose a table (a path ending in .table).", { tablePath });
418
+ if (!isObject(change)) throw fail("invalid-table-option", "A table alt change is { alt?, decorative? }.", { change });
419
+ let wanted;
420
+ if (change.decorative === true) wanted = "";
421
+ else if (change.alt !== undefined) wanted = typeof change.alt === "string" && change.alt.trim() ? change.alt.trim() : undefined;
422
+ else if (change.decorative === false && table.alt === "") wanted = undefined;
423
+ else wanted = table.alt;
424
+ const pointer = opfPathToJsonPointer([...parts, "alt"]);
425
+ const present = Object.hasOwn(table, "alt");
426
+ const patches = [];
427
+ if (wanted === undefined) {
428
+ if (present) patches.push({ op: "remove", path: pointer });
429
+ } else if (!present) patches.push({ op: "add", path: pointer, value: wanted });
430
+ else if (table.alt !== wanted) patches.push({ op: "replace", path: pointer, value: wanted });
431
+ const next = checkedDocument(presentation, patches, before);
432
+ return { action: "table-alt", tablePath: parts.join("."), changed: patches.length > 0, patches, presentation: clone(next) };
433
+ }
434
+
397
435
  // --- session forms ----------------------------------------------------------------------------
398
436
 
399
437
  function commit(editor, prepared, meta = {}) {
@@ -423,6 +461,11 @@ export function setTableCellStyle(editor, tablePath, cells, style, meta = {}) {
423
461
  checkEditor(editor);
424
462
  return commit(editor, prepareTableCellStyle(editor.presentation, tablePath, cells, style), meta);
425
463
  }
464
+ /** Change the table's text alternative as one undoable transaction. See {@link prepareTableAlt}. */
465
+ export function setTableAlt(editor, tablePath, change, meta = {}) {
466
+ checkEditor(editor);
467
+ return commit(editor, prepareTableAlt(editor.presentation, tablePath, change), meta);
468
+ }
426
469
  /** Apply a table style as one undoable transaction. See {@link prepareTableStyle}. */
427
470
  export function setTableStyle(editor, tablePath, preset, meta = {}) {
428
471
  checkEditor(editor);
@@ -67,7 +67,7 @@ export function createTemplatePanel(container, options) {
67
67
  const builtinList = h(doc, "ul", { class: "opf-template-builtin-list" });
68
68
  const builtinSection = h(doc, "details", { class: "opf-template-builtins", "data-opf-component": "template-builtins" },
69
69
  h(doc, "summary", { text: "Built-in variables" }),
70
- h(doc, "p", { class: "opf-template-help", text: "Read from the presentation's own speaker, organization and deck fields; edit those fields to change them. Use them like any variable: {{speaker.name}} inside text, or var:speaker.photo as a whole image field." }),
70
+ h(doc, "p", { class: "opf-template-help", text: "Read from the presentation's own speaker, organization and deck fields; edit those fields to change them. Use them like any variable: {{speaker.name}} inside text, or var:speaker.photo as a whole image field. Slide number, section and slide count vary per slide: write them as {{slide.number}}, {{slide.section}} and {{deck.slideCount}} inside any text, not as a whole var: field." }),
71
71
  builtinList);
72
72
  const live = h(doc, "p", { class: "opf-template-live", role: "status", "aria-live": "polite" });
73
73
  const applyButton = h(doc, "button", { type: "button", class: "opf-template-apply primary", text: "Fill the presentation" });
@@ -234,10 +234,12 @@ export function createTemplatePanel(container, options) {
234
234
  );
235
235
  builtinSection.hidden = !builtins.length;
236
236
  builtinList.replaceChildren(...builtins.map((entry) => {
237
- const shown = builtinValueText(entry);
238
237
  const uses = entry.uses.length;
239
- return h(doc, "li", { "data-builtin": entry.name, "data-kind": entry.kind, "data-available": entry.available ? "true" : "false" },
240
- h(doc, "code", { text: `{{${entry.name}}}` }), ` ${entry.label}: `, entry.available ? shown : h(doc, "em", { text: "not set" }), uses ? ` (used ${uses} time${uses === 1 ? "" : "s"})` : "");
238
+ // A slide-scoped built-in has no single value: each slide gets its own when it is drawn or exported (FA-31).
239
+ const slideScoped = entry.scope === "slide";
240
+ const shown = slideScoped ? h(doc, "em", { text: "varies per slide" }) : entry.available ? builtinValueText(entry) : h(doc, "em", { text: "not set" });
241
+ return h(doc, "li", { "data-builtin": entry.name, "data-kind": entry.kind, "data-scope": entry.scope, "data-available": entry.available ? "true" : "false" },
242
+ h(doc, "code", { text: `{{${entry.name}}}` }), ` ${entry.label}: `, shown, slideScoped && !entry.available ? " (no slide has one yet)" : "", uses ? ` (used ${uses} time${uses === 1 ? "" : "s"})` : "");
241
243
  }));
242
244
  slideSelect.replaceChildren(...(editor.presentation.slides ?? []).map((slide, index) => h(doc, "option", { value: String(index), text: `${index + 1}. ${slideTitle(slide) || "Untitled"}` })));
243
245
  previewSlide = Math.min(previewSlide, Math.max(0, (editor.presentation.slides?.length ?? 1) - 1));
@@ -109,7 +109,9 @@ export interface BuiltinInfo {
109
109
  name: string;
110
110
  kind: "text" | "image" | "list";
111
111
  label: string;
112
- /** The source value, when the document has one. */
112
+ /** `deck` for a value read once from the document; `slide` for `slide.number`, `slide.section` and `deck.slideCount`, which vary per slide. */
113
+ scope: "deck" | "slide";
114
+ /** The source value, when the document has one. Never set for a slide-scoped built-in. */
113
115
  value?: unknown;
114
116
  available: boolean;
115
117
  uses: { id: string; path: string; form: "token" | "reference" }[];
package/dist/templates.js CHANGED
@@ -8,7 +8,7 @@ import { OPFEditorError, getValueAtPath, opfPathToJsonPointer } from "./index.js
8
8
 
9
9
  const VARIABLE_ID = /^[a-z][a-z0-9-]*$/;
10
10
  // A built-in variable name (core FA-04): `speakers`, or deck/speaker/organization plus one or two dotted segments.
11
- const BUILTIN_NAME = /^(?:speakers|(?:deck|speaker|organization)(?:.[A-Za-z0-9_-]+){1,2})$/;
11
+ const BUILTIN_NAME = /^(?:speakers|(?:deck|slide|speaker|organization)(?:\.[A-Za-z0-9_-]+){1,2})$/;
12
12
  const KINDS = ["color", "text", "number", "date", "image", "url", "list"];
13
13
 
14
14
  /** The form control each variable kind uses. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-editor",
3
- "version": "0.15.0",
3
+ "version": "0.17.0",
4
4
  "description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -226,6 +226,7 @@
226
226
  "test:code-browser": "node test/code-browser.mjs",
227
227
  "test:selection-browser": "node test/selection-browser.mjs",
228
228
  "test:click-entry-browser": "node test/click-entry-browser.mjs",
229
+ "test:canvas-a11y-browser": "node test/canvas-a11y-browser.mjs",
229
230
  "test:metric-browser": "node test/metric-browser.mjs",
230
231
  "test:rich-input-browser": "node test/rich-input-browser.mjs",
231
232
  "test:json": "npm run build && node test/json-source.mjs && node test/json-options.mjs",
@@ -257,11 +258,11 @@
257
258
  "@codemirror/state": "6.7.6",
258
259
  "@codemirror/view": "6.43.13",
259
260
  "@lezer/highlight": "1.2.4",
260
- "@openpresentation/opf": "^0.15.1",
261
+ "@openpresentation/opf": "^0.17.0",
261
262
  "jsonc-parser": "3.3.1"
262
263
  },
263
264
  "peerDependencies": {
264
- "@openpresentation/opf-render": "^0.15.0"
265
+ "@openpresentation/opf-render": "^0.17.0"
265
266
  },
266
267
  "peerDependenciesMeta": {
267
268
  "@openpresentation/opf-render": {
@@ -269,6 +270,12 @@
269
270
  }
270
271
  },
271
272
  "devDependencies": {
273
+ "@expo-google-fonts/arimo": "0.4.3",
274
+ "@expo-google-fonts/caladea": "0.4.2",
275
+ "@expo-google-fonts/cousine": "0.4.3",
276
+ "@expo-google-fonts/gelasio": "0.4.1",
277
+ "@expo-google-fonts/noto-color-emoji": "0.4.6",
278
+ "@expo-google-fonts/noto-emoji": "0.4.7",
272
279
  "@expo-google-fonts/noto-naskh-arabic": "0.4.5",
273
280
  "@expo-google-fonts/noto-nastaliq-urdu": "0.4.2",
274
281
  "@expo-google-fonts/noto-sans": "0.4.2",
@@ -303,15 +310,17 @@
303
310
  "@expo-google-fonts/noto-sans-thai": "0.4.2",
304
311
  "@expo-google-fonts/noto-serif-hebrew": "0.4.2",
305
312
  "@expo-google-fonts/noto-serif-tibetan": "0.4.2",
306
- "@openpresentation/opf-pptx": "^0.15.0",
307
- "@openpresentation/opf-render": "^0.15.0",
313
+ "@expo-google-fonts/roboto": "0.4.3",
314
+ "@expo-google-fonts/roboto-mono": "0.4.2",
315
+ "@expo-google-fonts/stix-two-math": "0.4.0",
316
+ "@expo-google-fonts/tinos": "0.4.2",
317
+ "@openpresentation/opf-pptx": "^0.17.0",
318
+ "@openpresentation/opf-render": "^0.17.0",
308
319
  "axe-core": "4.13.0",
309
320
  "esbuild": "0.28.2",
310
321
  "jszip": "3.10.2",
322
+ "pdf-lib": "1.17.1",
311
323
  "pdfjs-dist": "6.3.289",
312
- "playwright": "1.63.0",
313
- "@expo-google-fonts/noto-color-emoji": "0.4.6",
314
- "@expo-google-fonts/noto-emoji": "0.4.7",
315
- "@expo-google-fonts/stix-two-math": "0.4.0"
324
+ "playwright": "1.63.0"
316
325
  }
317
326
  }