@openpresentation/opf-editor 0.26.1 → 0.26.3

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,81 @@
1
+ // The syntax problems of a JSON text, as findings (artifacts/source-editor/VALIDATOR-HINTS.md, sections 5 and 6): one per cause, each with the range
2
+ // the squiggle covers, a plain-words message, what could come next and the quick fixes. Duplicate keys are found here too.
3
+ import { parse as parseJsonc, parseTree, printParseErrorCode } from "jsonc-parser";
4
+ import { analyzeJsonSyntax } from "@openpresentation/opf/source";
5
+ import { normalizeFinding } from "./source-findings.js";
6
+ import { lineColumn } from "./source-ranges.js";
7
+
8
+ const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
9
+ const ERRORS = { InvalidSymbol: "Invalid symbol", InvalidNumberFormat: "Invalid number", PropertyNameExpected: "Property name expected", ValueExpected: "Value expected", ColonExpected: "Colon expected", CommaExpected: "Comma expected", CloseBraceExpected: "Closing brace expected", CloseBracketExpected: "Closing bracket expected", EndOfFileExpected: "End of file expected", InvalidCommentToken: "Comments are not allowed in JSON", UnexpectedEndOfComment: "Unexpected end of comment", UnexpectedEndOfString: "Unexpected end of string", UnexpectedEndOfNumber: "Unexpected end of number", InvalidUnicode: "Invalid unicode escape", InvalidEscapeCharacter: "Invalid escape character", InvalidCharacter: "Invalid character" };
10
+
11
+ /**
12
+ * The syntax problems of `text` (empty when it is a JSON object): one finding per root cause, `{ ruleId, message, next, range: { from, to },
13
+ * line, column, editFixes }`, by core's recovery analyzer (`@openpresentation/opf/source`).
14
+ */
15
+ export function analyzeSyntax(text) {
16
+ // A valid object is the common case on every keystroke: the engine's parser answers that at once.
17
+ try { if (isObject(JSON.parse(text))) return []; } catch { /* Analyze. */ }
18
+ const found = analyzeJsonSyntax(text).map((problem) => ({
19
+ ruleId: "opf/json-syntax",
20
+ message: problem.message,
21
+ next: problem.next,
22
+ range: { from: problem.range.offset, to: problem.range.offset + problem.range.length },
23
+ line: problem.range.line,
24
+ column: problem.range.column,
25
+ ...(problem.path ? { problemPath: problem.path } : {}),
26
+ editFixes: problem.fixes.map((fix) => ({
27
+ id: fix.id, title: fix.title, safe: fix.safe,
28
+ edits: fix.edits.map((edit) => ({ from: edit.offset, to: edit.offset + edit.length, insert: edit.insert })),
29
+ ...(fix.caret !== undefined ? { caret: fix.caret } : {}),
30
+ ...(fix.action ? { action: fix.action, format: fix.format } : {}),
31
+ })),
32
+ }));
33
+ if (found.length) return found;
34
+ const errors = [];
35
+ parseJsonc(text, errors, { allowTrailingComma: false, disallowComments: true });
36
+ if (!errors.length) return [];
37
+ const first = errors[0];
38
+ return [{ ruleId: "opf/json-syntax", message: `${ERRORS[printParseErrorCode(first.error)] ?? printParseErrorCode(first.error)}.`, range: { from: first.offset, to: first.offset + Math.max(1, first.length) }, line: lineColumn(text, first.offset).line }];
39
+ }
40
+
41
+ /** The edit that removes property `property` of `object`: it and the comma that joined it to its neighbor. */
42
+ function removeProperty(object, property) {
43
+ const list = object.children ?? [], at = list.indexOf(property);
44
+ if (at < list.length - 1) return [{ from: property.offset, to: list[at + 1].offset, insert: "" }];
45
+ if (at > 0) { const before = list[at - 1]; return [{ from: before.offset + before.length, to: property.offset + property.length, insert: "" }]; }
46
+ return [{ from: property.offset, to: property.offset + property.length, insert: "" }];
47
+ }
48
+
49
+ /** Every duplicate object key of a JSON text that parses, as findings that mark the later occurrence (`related` is the earlier one). */
50
+ export function findDuplicateKeys(text) {
51
+ const root = parseTree(text);
52
+ const out = [];
53
+ const walk = (node, path) => {
54
+ if (node.type === "object") {
55
+ const seen = new Map();
56
+ for (const property of node.children ?? []) {
57
+ const key = property.children?.[0];
58
+ if (!key) continue;
59
+ if (seen.has(key.value)) {
60
+ const first = seen.get(key.value);
61
+ const firstKey = first.children[0];
62
+ out.push(normalizeFinding({
63
+ path: `/${[...path, key.value].map((part) => String(part).replaceAll("~", "~0").replaceAll("/", "~1")).join("/")}`,
64
+ message: `Duplicate key "${key.value}". Remove the repeated key.`,
65
+ ruleId: "opf/duplicate-key",
66
+ range: { from: key.offset, to: key.offset + key.length },
67
+ related: [{ from: firstKey.offset, to: firstKey.offset + firstKey.length }],
68
+ editFixes: [
69
+ { id: "remove-first", title: "Remove the first", edits: removeProperty(node, first) },
70
+ { id: "remove-second", title: "Remove the second", edits: removeProperty(node, property) },
71
+ ],
72
+ line: lineColumn(text, key.offset).line,
73
+ }, "syntax"));
74
+ } else seen.set(key.value, property);
75
+ if (property.children?.[1]) walk(property.children[1], [...path, key.value]);
76
+ }
77
+ } else if (node.type === "array") (node.children ?? []).forEach((child, index) => walk(child, [...path, index]));
78
+ };
79
+ if (root) walk(root, []);
80
+ return out;
81
+ }
package/dist/styles.css CHANGED
@@ -3655,6 +3655,54 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
3655
3655
  border-radius: var(--opf-ui-radius-md);
3656
3656
  }
3657
3657
 
3658
+ /* The live region next to the code: read by screen readers, never seen. */
3659
+ .opf-source-live {
3660
+ position: absolute;
3661
+ width: 1px;
3662
+ height: 1px;
3663
+ margin: -1px;
3664
+ padding: 0;
3665
+ overflow: hidden;
3666
+ clip-path: inset(50%);
3667
+ white-space: nowrap;
3668
+ border: 0;
3669
+ }
3670
+
3671
+ /* The documentation cards: beside the selected suggestion and over a key or a value. */
3672
+ :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip .opf-doc,
3673
+ .opf-source-code .cm-tooltip .opf-doc {
3674
+ display: flex;
3675
+ flex-direction: column;
3676
+ gap: 4px;
3677
+ max-width: 340px;
3678
+ padding: 2px 0;
3679
+ font: 12px/1.45 var(--opf-ui-font);
3680
+ white-space: normal;
3681
+ }
3682
+ .opf-doc-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px; }
3683
+ .opf-doc-title { font-family: var(--opf-ui-mono); font-weight: 600; }
3684
+ .opf-doc-type { color: var(--opf-ui-muted-fg); }
3685
+ .opf-doc-badge { padding: 0 6px; border-radius: 999px; background: color-mix(in srgb, var(--opf-ui-accent) 14%, transparent); color: var(--opf-ui-accent-strong); font-size: 11px; font-weight: 500; }
3686
+ .opf-doc-text, .opf-doc-note, .opf-doc-example, .opf-doc-source { margin: 0; }
3687
+ .opf-doc-note, .opf-doc-source { color: var(--opf-ui-muted-fg); }
3688
+ .opf-doc-values { display: flex; flex-wrap: wrap; gap: 4px; }
3689
+ .opf-doc-chip { padding: 0 5px; border: 1px solid var(--opf-ui-border); border-radius: var(--opf-ui-radius-sm); font-size: 11px; }
3690
+ .opf-doc-more { color: var(--opf-ui-muted-fg); font-size: 11px; }
3691
+
3692
+ /* A problem's tooltip and the quick fix list. */
3693
+ .cm-tooltip-lint .opf-problem { display: flex; flex-direction: column; gap: 2px; max-width: 360px; white-space: normal; }
3694
+ .opf-problem-next { color: var(--opf-ui-muted-fg); }
3695
+ .cm-tooltip-lint .cm-diagnosticAction { margin: 4px 6px 0 0; padding: 1px 8px; border: 1px solid var(--opf-ui-border); border-radius: var(--opf-ui-radius-sm); background: var(--opf-ui-muted); color: var(--opf-ui-fg); font: 12px var(--opf-ui-font); cursor: pointer; }
3696
+ .cm-tooltip-lint .cm-diagnosticSource { color: var(--opf-ui-muted-fg); }
3697
+ .opf-problem-hint { color: var(--opf-ui-muted-fg); font-size: 11px; }
3698
+ .cm-tooltip .opf-fixes { display: flex; flex-direction: column; min-width: 180px; padding: 3px; font: 12px var(--opf-ui-font); }
3699
+ .opf-fix { padding: 3px 8px; border-radius: var(--opf-ui-radius-sm); white-space: nowrap; }
3700
+ .opf-fix[aria-selected="true"] { background: var(--opf-ui-accent); color: var(--opf-ui-accent-fg); }
3701
+ :root :is(.opf-ui, .opf-json, .opf-code) .cm-tooltip .cm-completionInfo { white-space: normal; }
3702
+
3703
+ /* Ctrl+Space with the list open hides the documentation panel (and shows it again). */
3704
+ .cm-editor.opf-docs-off .cm-completionInfo { display: none; }
3705
+
3658
3706
  :is(.opf-ui, .opf-json, .opf-code) .cm-diagnostic-error {
3659
3707
  border-left-color: var(--opf-ui-danger);
3660
3708
  }
@@ -4169,13 +4217,17 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4169
4217
  .opf-source-findings-toggle[aria-expanded="false"] > .opf-source-icon { transform: rotate(-90deg); }
4170
4218
  .opf-source-findings-icon { display: inline-grid; place-items: center; }
4171
4219
  .opf-source-findings-list { display: flex; flex-direction: column; gap: 2px; margin: 0; padding: 0 8px 8px; list-style: none; overflow: auto; }
4220
+ .opf-source-finding-item { display: flex; flex-direction: column; }
4172
4221
  .opf-source-finding {
4173
- display: flex;
4222
+ display: grid;
4223
+ grid-template-columns: 14px minmax(0, 1fr) auto;
4224
+ grid-template-areas: "icon head where" "icon next rule";
4174
4225
  align-items: baseline;
4175
- gap: 8px;
4226
+ column-gap: 8px;
4227
+ row-gap: 1px;
4176
4228
  width: 100%;
4177
4229
  min-height: calc(var(--_h) - 4px);
4178
- padding: 3px 6px;
4230
+ padding: 4px 6px;
4179
4231
  border: 0;
4180
4232
  border-radius: var(--opf-ui-radius-sm);
4181
4233
  background: transparent;
@@ -4185,6 +4237,12 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4185
4237
  transition: background-color var(--opf-ui-ease);
4186
4238
  }
4187
4239
  .opf-source-finding:hover { background: var(--opf-ui-muted); }
4240
+ /* The severity is a shape as well as a color; the word is for screen readers. */
4241
+ .opf-source-finding::before { grid-area: icon; align-self: start; color: var(--opf-ui-danger); font-size: 11px; line-height: 1.7; content: "\2716"; }
4242
+ .opf-source-finding[data-severity="warning"]::before { color: var(--opf-ui-warn); content: "\25B2"; }
4243
+ .opf-source-finding[data-severity="info"]::before { color: var(--opf-ui-muted-fg); content: "\25CF"; }
4244
+ .opf-source-finding-severity { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
4245
+ .opf-source-finding-head { grid-area: head; display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; min-width: 0; }
4188
4246
  .opf-source-finding-path {
4189
4247
  flex: none;
4190
4248
  padding: 1px 5px;
@@ -4195,8 +4253,21 @@ input.opf-lx-input[data-field="max"] { font-family: var(--opf-ui-mono); font-siz
4195
4253
  }
4196
4254
  .opf-source-finding[data-severity="warning"] .opf-source-finding-path { background: color-mix(in srgb, var(--opf-ui-warn) 12%, transparent); color: var(--opf-ui-warn); }
4197
4255
  .opf-source-finding[data-severity="info"] .opf-source-finding-path { background: var(--opf-ui-muted); color: var(--opf-ui-muted-fg); }
4198
- .opf-source-finding-message { min-width: 0; color: var(--opf-ui-muted-fg); overflow-wrap: anywhere; }
4199
- .opf-source-finding-line { flex: none; margin-left: auto; color: var(--opf-ui-muted-fg); font: 11px var(--opf-ui-mono); }
4256
+ .opf-source-finding-message { min-width: 0; color: var(--opf-ui-fg); overflow-wrap: anywhere; }
4257
+ .opf-source-finding-next { grid-area: next; min-width: 0; color: var(--opf-ui-muted-fg); overflow-wrap: anywhere; }
4258
+ .opf-source-finding-line { grid-area: where; justify-self: end; color: var(--opf-ui-muted-fg); font: 11px var(--opf-ui-mono); white-space: nowrap; }
4259
+ .opf-source-finding-rule { grid-area: rule; justify-self: end; color: var(--opf-ui-muted-fg); font: 10.5px var(--opf-ui-mono); }
4260
+ .opf-source-finding-fixes { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 6px 4px 28px; }
4261
+ .opf-source-finding-fix {
4262
+ min-height: 22px;
4263
+ padding: 0 8px;
4264
+ border: 1px solid var(--opf-ui-border);
4265
+ border-radius: var(--opf-ui-radius-sm);
4266
+ background: var(--opf-ui-surface);
4267
+ font-size: 12px;
4268
+ cursor: pointer;
4269
+ }
4270
+ .opf-source-finding-fix:hover { background: var(--opf-ui-muted); }
4200
4271
  .opf-source-finding-more { padding: 3px 6px; color: var(--opf-ui-muted-fg); font-size: 12px; }
4201
4272
 
4202
4273
  .opf-source-status { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-editor",
3
- "version": "0.26.1",
3
+ "version": "0.26.3",
4
4
  "description": "Embeddable local OPF editor primitives built on the deterministic renderer.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -283,10 +283,10 @@
283
283
  "@lezer/highlight": "1.2.5",
284
284
  "jsonc-parser": "3.3.1",
285
285
  "yaml": "^2.9.1",
286
- "@openpresentation/opf": "^0.26.1"
286
+ "@openpresentation/opf": "^0.26.3"
287
287
  },
288
288
  "peerDependencies": {
289
- "@openpresentation/opf-render": "^0.26.1"
289
+ "@openpresentation/opf-render": "^0.26.3"
290
290
  },
291
291
  "peerDependenciesMeta": {
292
292
  "@openpresentation/opf-render": {
@@ -345,9 +345,9 @@
345
345
  "pdf-lib": "1.17.1",
346
346
  "pdfjs-dist": "6.4.299",
347
347
  "playwright": "1.64.0",
348
- "@openpresentation/opf-render": "0.26.1",
349
- "@openpresentation/opf-pptx": "0.26.1",
350
- "@openpresentation/gallery": "0.26.1"
348
+ "@openpresentation/gallery": "0.26.3",
349
+ "@openpresentation/opf-pptx": "0.26.3",
350
+ "@openpresentation/opf-render": "0.26.3"
351
351
  },
352
352
  "scripts": {
353
353
  "build": "node scripts/build.mjs",
@@ -385,6 +385,7 @@
385
385
  "test:review-hook-browser": "node test/review-hook-browser.mjs",
386
386
  "test:source-pane-browser": "node test/source-pane-browser.mjs",
387
387
  "test:source-keyboard-browser": "node test/source-keyboard-browser.mjs",
388
+ "test:source-keyboard-json-browser": "node test/source-keyboard-json-browser.mjs",
388
389
  "test:layout-editor-browser": "node test/layout-editor-browser.mjs",
389
390
  "test:export-license-browser": "node test/export-license-browser.mjs",
390
391
  "test:chart-options-browser": "node test/chart-options-browser.mjs",
@@ -394,5 +395,5 @@
394
395
  "test:export-menu-browser": "node test/export-menu-browser.mjs",
395
396
  "test:agent-handoff-browser": "node test/agent-handoff-browser.mjs"
396
397
  },
397
- "gitHead": "792e7c6d348011aa6f398989702fc8b8a80ec691"
398
+ "gitHead": "570ebe0c803e9613f6b9f9fcca8160b38b16a826"
398
399
  }