@openpresentation/opf-editor 0.26.2 → 0.26.4

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.
@@ -0,0 +1,87 @@
1
+ // Quick fixes for the findings of a YAML or Markdown source (artifacts/source-editor/VALIDATOR-HINTS.md, section 12): "Change to center", "Rename to
2
+ // slides", "Remove property", "Add `id`". The same fixes source-fixes.js writes for JSON, as text edits found with core's `locate`, so each one is a
3
+ // change of the text itself (one undoable change) that keeps the author's comments and layout. Pure functions: `{ id, title, safe, edits: [{ from, to, insert }] }`.
4
+ import { Document } from "yaml";
5
+ import { SLIDE_OPTION_KEYS } from "@openpresentation/opf/source";
6
+ import { replacementValues, segmentsOf } from "./source-messages.js";
7
+ import { locateText } from "./source-ranges.js";
8
+
9
+ const yamlScalar = (value) => {
10
+ if (typeof value !== "string") return String(value);
11
+ try { return new Document(value).toString({ lineWidth: 0 }).trimEnd() || '""'; } catch { return JSON.stringify(value); }
12
+ };
13
+ const yamlKey = (key) => (/^[A-Za-z_$][A-Za-z0-9_.$+-]*(?::[A-Za-z0-9_.$+-]+)*$/.test(key) ? key : yamlScalar(key));
14
+ const BARE = /^[A-Za-z0-9_-]+$/;
15
+
16
+ /** Whether `segments` is an option of a slide's options comment in Markdown (`/slides/0/layout`), whose value is written as `key=value`. */
17
+ const isSlideOption = (format, segments) => format === "md" && segments[0] === "slides" && segments.length === 3 && SLIDE_OPTION_KEYS.includes(segments[2]);
18
+
19
+ /** How `value` is written in place of the token `old`: in the quoting style the author used. */
20
+ function written(format, segments, old, value) {
21
+ if (isSlideOption(format, segments)) return typeof value === "string" && BARE.test(value) ? value : JSON.stringify(value);
22
+ if (old.startsWith('"')) return JSON.stringify(value);
23
+ if (old.startsWith("'")) return `'${String(value).replaceAll("'", "''")}'`;
24
+ return yamlScalar(value);
25
+ }
26
+
27
+ // Only YAML written as block text (and the front matter and fences of Markdown, which are block YAML) can have a whole entry removed or added by lines.
28
+ const lineStart = (text, offset) => text.lastIndexOf("\n", offset - 1) + 1;
29
+ const lineEnd = (text, offset) => { const at = text.indexOf("\n", offset); return at < 0 ? text.length : at; };
30
+
31
+ /** Remove the entry at `segments` (its lines), when it starts its line and is not the first key after a list dash. */
32
+ function removeEntry(format, text, segments) {
33
+ const entry = locateText(format, text, segments, "entry");
34
+ if (!entry?.exact) return null;
35
+ const start = lineStart(text, entry.from);
36
+ if (text.slice(start, entry.from).trim() !== "") return null;
37
+ const end = lineEnd(text, Math.max(entry.from, entry.to - 1));
38
+ return [{ from: start, to: Math.min(text.length, end + 1), insert: "" }];
39
+ }
40
+
41
+ /** Add `key: value` to the mapping at `segments`, after its last line, indented like its first key. */
42
+ function addProperty(format, text, segments, key, value) {
43
+ const parent = segments.length ? locateText(format, text, segments, "value") : null;
44
+ const first = parent?.exact ? locateText(format, text, segments, "open") : null;
45
+ let column = 0;
46
+ let at = text.length;
47
+ if (parent?.exact && first) {
48
+ if (/^[[{]/.test(text.slice(parent.from, parent.from + 1))) return null;
49
+ column = first.from - lineStart(text, first.from);
50
+ at = lineEnd(text, Math.max(parent.from, parent.to - 1));
51
+ } else if (segments.length) return null;
52
+ else if (text.trim() !== "") {
53
+ const root = locateText(format, text, [], "value");
54
+ if (!root) return null;
55
+ at = lineEnd(text, Math.max(root.from, root.to - 1));
56
+ }
57
+ const body = new Document({ [key]: value }).toString({ indent: 2, lineWidth: 0 }).trimEnd().split("\n").map((line) => `${" ".repeat(column)}${line}`).join("\n");
58
+ return [{ from: at, to: at, insert: text.trim() === "" ? `${body}\n` : `\n${body}` }];
59
+ }
60
+
61
+ /**
62
+ * The quick fixes of a schema or reference finding in a YAML or Markdown text. `context` is the one source-messages.js takes plus `{ starter(segments,
63
+ * property) }` (the value `Add` inserts). Returns `[]` for what the text cannot express as a plain edit.
64
+ */
65
+ export function textFixesForFinding(format, finding, text, context = {}) {
66
+ if (!finding.path && finding.validation?.keyword !== "required") return [];
67
+ const segments = segmentsOf(finding.path);
68
+ const keyword = finding.validation?.keyword;
69
+ const fixes = [];
70
+ const push = (id, title, edits) => { if (edits?.length) fixes.push({ id, title, edits, safe: false }); };
71
+ if (keyword === "enum" || finding.ruleId === "opf/unresolved-reference") {
72
+ const where = locateText(format, text, segments, "value");
73
+ if (!where?.exact) return [];
74
+ const old = text.slice(where.from, where.to);
75
+ for (const value of replacementValues(finding, context)) push(`change-to:${value}`, `Change to ${value}`, [{ from: where.from, to: where.to, insert: written(format, segments, old, value) }]);
76
+ } else if (keyword === "additionalProperties" && !isSlideOption(format, segments)) {
77
+ const key = locateText(format, text, segments, "key");
78
+ if (key?.exact) for (const value of replacementValues(finding, context)) push(`rename-to:${value}`, `Rename to ${value}`, [{ from: key.from, to: key.to, insert: yamlKey(value) }]);
79
+ push("remove-property", "Remove property", removeEntry(format, text, segments));
80
+ } else if (keyword === "required") {
81
+ const property = finding.validation.params?.missingProperty;
82
+ const starter = property ? context.starter?.(segments, property) : undefined;
83
+ // In Markdown only the front matter is a mapping this can add to; a slide's fields are written as Markdown.
84
+ if (property && starter !== undefined && (format === "yaml" || segments[0] !== "slides")) push(`add:${property}`, `Add \`${property}\``, addProperty(format, text, segments, property, starter));
85
+ }
86
+ return fixes;
87
+ }
@@ -1,4 +1,5 @@
1
1
  import { parseTree } from "jsonc-parser";
2
+ import { locate } from "@openpresentation/opf/source";
2
3
 
3
4
  // Line lookup for a JSON Pointer in the three source formats of the source pane (RR-98). All functions return a
4
5
  // one-based line number, or null when the path cannot be placed. JSON is exact; YAML and Markdown are best effort:
@@ -197,6 +198,9 @@ export function lineForPath(format, text, path) {
197
198
  const segments = pathSegments(path);
198
199
  if (!segments.length) return 1;
199
200
  try {
201
+ // Core reads the text of every format and knows where a path is; the line heuristics below are for text it cannot place.
202
+ const found = format === "json" ? null : locate(text, format, segments);
203
+ if (found) return found.line;
200
204
  return format === "json" ? jsonLine(text, segments)
201
205
  : format === "yaml" ? yamlLine(text, segments)
202
206
  : markdownLine(text, segments);