@pixerate/editor 0.14.1 → 0.14.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.
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # @pixerate/editor
2
+
3
+ UI-agnostic core of the Pixerate editor toolkit: a prompt-template grammar with recursive resolution and sourcemaps, plain-text and markdown serializers, headless [TipTap](https://tiptap.dev) extensions, and framework-free controllers for rich text, spreadsheets, images and node-graph canvases.
4
+
5
+ Framework bindings: [`@pixerate/editor-react`](https://www.npmjs.com/package/@pixerate/editor-react) · [`@pixerate/editor-svelte`](https://www.npmjs.com/package/@pixerate/editor-svelte)
6
+
7
+ ```bash
8
+ npm install @pixerate/editor
9
+ ```
10
+
11
+ ## Prompt grammar
12
+
13
+ Prompts use three token types:
14
+
15
+ | Syntax | Token | Example |
16
+ | :--- | :--- | :--- |
17
+ | `{{name}}` | template, replaced by its body (recursively) | `{{golden_hour}}` |
18
+ | `{variable}` | variable, filled from a values map | `{subject}` |
19
+ | `__instruction__` | instruction, a styled directive for the model | `__award_winning__` |
20
+
21
+ ```ts
22
+ import {
23
+ tokenizePrompt,
24
+ resolveTemplates,
25
+ resolveVariables,
26
+ resolveTemplatesWithMapping,
27
+ mapResolvedOffsetToRaw,
28
+ TEMPLATE_LOOP_ERROR,
29
+ } from "@pixerate/editor/grammar";
30
+
31
+ const templates = [
32
+ { name: "mood", body: "cinematic cyber-noir" },
33
+ { name: "scene", body: "rainy street, {{mood}}" },
34
+ ];
35
+
36
+ tokenizePrompt("Shot of {{scene}} with {subject}");
37
+ // [{ type: "text", … }, { type: "template", value: "scene", start: 8, end: 17, … }, …]
38
+
39
+ resolveTemplates("Shot of {{scene}}", templates);
40
+ // "Shot of rainy street, cinematic cyber-noir"
41
+
42
+ resolveVariables("Hello {user}", { user: "Ada" }); // "Hello Ada"
43
+
44
+ // Map offsets in the resolved text back to the source (for error highlighting, carets, …)
45
+ const { text, map } = resolveTemplatesWithMapping("Shot of {{scene}}", templates);
46
+ mapResolvedOffsetToRaw(text.length, map); // 17, the end of "{{scene}}"
47
+ ```
48
+
49
+ - **Cycles and depth.** A tag that would recurse into itself is replaced with `TEMPLATE_LOOP_ERROR` (`"[Template loop detected]"`), and tags nested deeper than `maxDepth` (default `10`) with `TEMPLATE_DEPTH_ERROR`. The rest of the prompt still resolves, so check with `result.includes(TEMPLATE_LOOP_ERROR)`.
50
+ - **Versions.** Templates may carry `versions` and `latestVersion`. Pin specific versions with `resolveTemplates(text, templates, { bindings: { name: "v1" } })`.
51
+
52
+ ## Serializers
53
+
54
+ ```ts
55
+ import {
56
+ plainTextToTipTapHtml,
57
+ htmlToPlainText,
58
+ getEditorText,
59
+ markdownToTipTapHtml,
60
+ getEditorMarkdown,
61
+ extractMentionsFromDoc,
62
+ } from "@pixerate/editor/serializers";
63
+ ```
64
+
65
+ - `plainTextToTipTapHtml` / `htmlToPlainText` round-trip multi-line plain text through TipTap paragraphs without leaking HTML.
66
+ - `markdownToTipTapHtml` renders markdown for TipTap. Raw HTML embedded in the markdown is sanitized against an allowlist (event handlers, `style` and `javascript:` URLs are removed). Pass `{ allowUnsafeHtml: true }` only for fully trusted input.
67
+
68
+ ## Editor controller and extensions
69
+
70
+ `createEditor` wraps a TipTap `Editor` with plain-text / markdown modes, token callbacks and dirty tracking. It works with any view layer, or with none.
71
+
72
+ ```ts
73
+ import { createEditor, createRichTextPreset, GradientText } from "@pixerate/editor";
74
+
75
+ const controller = createEditor({
76
+ element: document.querySelector("#editor"),
77
+ plainTextMode: true,
78
+ content: "A photo of {subject} in {{golden_hour}}",
79
+ extensions: [...createRichTextPreset(), GradientText],
80
+ onTokensChange: (tokens) => console.log(tokens),
81
+ });
82
+
83
+ controller.insertText("__award_winning__ "); // at the selection; literal text, never parsed as HTML
84
+ controller.isDirty(); // true
85
+ ```
86
+
87
+ Extensions (`@pixerate/editor/extensions`): `createRichTextPreset`, `GradientText`, `TemplateSuggestions`, `SlashCommands`, `LoadingNode`, `Mention` / `createMentionExtension`, `Image`, `Markdown`, `SmilieReplacer`, `ColorHighlighter`, `FontSize`.
88
+
89
+ ## Other modules
90
+
91
+ Each module is also available as a subpath import:
92
+
93
+ | Import | Contents |
94
+ | :--- | :--- |
95
+ | `@pixerate/editor/grammar` | Tokenizer, resolver, sourcemaps. No TipTap or DOM dependencies, so it is safe for Node.js, workers and serverless functions. |
96
+ | `@pixerate/editor/serializers` | Plain text, markdown and mention serializers |
97
+ | `@pixerate/editor/extensions` | Headless TipTap extensions |
98
+ | `@pixerate/editor/history` | `HistoryManager`: a command-pattern undo/redo stack |
99
+ | `@pixerate/editor/spreadsheet` | `SpreadsheetController`, formula parser/evaluator and dependency graph |
100
+ | `@pixerate/editor/image-editor` | `ImageEditorController` and Canvas 2D renderer: crop, adjustments, annotations, depth masks and AI hooks. Needs a DOM/canvas environment. |
101
+ | `@pixerate/editor/canvas` | Node-graph layout (dagre), placement, trajectories, displacement, clipboard and streaming layout helpers |
102
+ | `@pixerate/editor/agent` | MCP tool schemas and a planner for agent-driven canvas edits |
103
+
104
+ The root entry (`@pixerate/editor`) re-exports everything, including TipTap. In backends, import from `@pixerate/editor/grammar` to keep the dependency graph small.
105
+
106
+ ## License
107
+
108
+ MIT
package/dist/index.cjs CHANGED
@@ -2698,6 +2698,114 @@ md2.validateLink = (url) => {
2698
2698
  }
2699
2699
  return defaultValidateLink.call(md2, url);
2700
2700
  };
2701
+ var ALLOWED_HTML_TAGS = /* @__PURE__ */ new Set([
2702
+ "a",
2703
+ "abbr",
2704
+ "b",
2705
+ "blockquote",
2706
+ "br",
2707
+ "code",
2708
+ "del",
2709
+ "details",
2710
+ "div",
2711
+ "em",
2712
+ "h1",
2713
+ "h2",
2714
+ "h3",
2715
+ "h4",
2716
+ "h5",
2717
+ "h6",
2718
+ "hr",
2719
+ "i",
2720
+ "img",
2721
+ "input",
2722
+ "kbd",
2723
+ "li",
2724
+ "mark",
2725
+ "ol",
2726
+ "p",
2727
+ "pre",
2728
+ "s",
2729
+ "span",
2730
+ "strong",
2731
+ "sub",
2732
+ "summary",
2733
+ "sup",
2734
+ "table",
2735
+ "tbody",
2736
+ "td",
2737
+ "tfoot",
2738
+ "th",
2739
+ "thead",
2740
+ "tr",
2741
+ "u",
2742
+ "ul"
2743
+ ]);
2744
+ var ALLOWED_HTML_ATTRS = /* @__PURE__ */ new Set([
2745
+ "alt",
2746
+ "align",
2747
+ "checked",
2748
+ "class",
2749
+ "colspan",
2750
+ "disabled",
2751
+ "height",
2752
+ "href",
2753
+ "rel",
2754
+ "rowspan",
2755
+ "src",
2756
+ "start",
2757
+ "target",
2758
+ "title",
2759
+ "type",
2760
+ "width"
2761
+ ]);
2762
+ var HTML_TAG_REGEX = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)((?:\s+[^\s"'>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*)\s*\/?>/g;
2763
+ var HTML_ATTR_REGEX = /([^\s"'>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
2764
+ function sanitizeTag(tagText, isClosing, rawTagName, rawAttrs) {
2765
+ const tagName = rawTagName.toLowerCase();
2766
+ if (!ALLOWED_HTML_TAGS.has(tagName)) return escapeHtml(tagText);
2767
+ if (isClosing) return `</${tagName}>`;
2768
+ if (tagName === "input" && !/\stype\s*=\s*["']?checkbox["']?[\s>/]/i.test(tagText)) {
2769
+ return escapeHtml(tagText);
2770
+ }
2771
+ let attrs = "";
2772
+ let attrMatch;
2773
+ HTML_ATTR_REGEX.lastIndex = 0;
2774
+ while ((attrMatch = HTML_ATTR_REGEX.exec(rawAttrs)) !== null) {
2775
+ const name = attrMatch[1].toLowerCase();
2776
+ if (!ALLOWED_HTML_ATTRS.has(name) && !/^data-[a-z0-9_.-]+$/.test(name)) {
2777
+ continue;
2778
+ }
2779
+ const rawValue = attrMatch[2] ?? attrMatch[3] ?? attrMatch[4];
2780
+ if (rawValue === void 0) {
2781
+ attrs += ` ${name}`;
2782
+ continue;
2783
+ }
2784
+ const value = md2.utils.unescapeAll(rawValue);
2785
+ if ((name === "href" || name === "src") && // Browsers ignore control characters and whitespace inside schemes.
2786
+ !md2.validateLink(value.replace(/[\u0000-\u0020\u007f]/g, ""))) {
2787
+ continue;
2788
+ }
2789
+ attrs += ` ${name}="${escapeHtml(value)}"`;
2790
+ }
2791
+ return `<${tagName}${attrs}>`;
2792
+ }
2793
+ function sanitizeEmbeddedHtml(html) {
2794
+ let result = "";
2795
+ let lastIndex = 0;
2796
+ let match;
2797
+ HTML_TAG_REGEX.lastIndex = 0;
2798
+ while ((match = HTML_TAG_REGEX.exec(html)) !== null) {
2799
+ result += html.slice(lastIndex, match.index).replace(/</g, "&lt;");
2800
+ result += sanitizeTag(match[0], match[1], match[2], match[3]);
2801
+ lastIndex = match.index + match[0].length;
2802
+ }
2803
+ return result + html.slice(lastIndex).replace(/</g, "&lt;");
2804
+ }
2805
+ var defaultHtmlInline = md2.renderer.rules.html_inline;
2806
+ var defaultHtmlBlock = md2.renderer.rules.html_block;
2807
+ md2.renderer.rules.html_inline = (tokens, idx, options, env, self) => env?.allowUnsafeHtml ? defaultHtmlInline(tokens, idx, options, env, self) : sanitizeEmbeddedHtml(tokens[idx].content);
2808
+ md2.renderer.rules.html_block = (tokens, idx, options, env, self) => env?.allowUnsafeHtml ? defaultHtmlBlock(tokens, idx, options, env, self) : sanitizeEmbeddedHtml(tokens[idx].content);
2701
2809
  function preprocessMarkdownFileLinks(content) {
2702
2810
  if (!content) return "";
2703
2811
  return content.replace(
@@ -2705,8 +2813,8 @@ function preprocessMarkdownFileLinks(content) {
2705
2813
  (_match, rawLabel, href) => {
2706
2814
  const trimmed = rawLabel.trim();
2707
2815
  const codeMatch = trimmed.match(/^`([^`]+)`$/);
2708
- const labelContent = codeMatch ? `<code>${codeMatch[1]}</code>` : trimmed;
2709
- return `<a href="${href}" data-file-link="true" title="Click to copy file path">${labelContent}</a>`;
2816
+ const labelContent = codeMatch ? `<code>${escapeHtml(codeMatch[1])}</code>` : escapeHtml(trimmed);
2817
+ return `<a href="${escapeHtml(href)}" data-file-link="true" title="Click to copy file path">${labelContent}</a>`;
2710
2818
  }
2711
2819
  );
2712
2820
  }
@@ -2722,7 +2830,7 @@ function markdownToTipTapHtml(markdown, options) {
2722
2830
  };
2723
2831
  content = preprocessMarkdownMentions(content, mentionOpts);
2724
2832
  }
2725
- return md2.render(content);
2833
+ return md2.render(content, { allowUnsafeHtml: options?.allowUnsafeHtml });
2726
2834
  }
2727
2835
  function getEditorMarkdown(editor) {
2728
2836
  if (!editor) return "";