@panneau/editor 0.0.0-stage → 5.0.0-alpha.11

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.
Files changed (60) hide show
  1. package/README.md +129 -2
  2. package/dist/LinkDialog.d.ts +18 -0
  3. package/dist/LinkDialog.d.ts.map +1 -0
  4. package/dist/LinkDialog.js +220 -0
  5. package/dist/LinkDialog.js.map +1 -0
  6. package/dist/RichTextField.css +257 -0
  7. package/dist/RichTextField.d.ts +43 -0
  8. package/dist/RichTextField.d.ts.map +1 -0
  9. package/dist/RichTextField.js +361 -0
  10. package/dist/RichTextField.js.map +1 -0
  11. package/dist/RichTextField.module.js +28 -0
  12. package/dist/RichTextField.module.js.map +1 -0
  13. package/dist/Toolbar.d.ts +20 -0
  14. package/dist/Toolbar.d.ts.map +1 -0
  15. package/dist/Toolbar.js +203 -0
  16. package/dist/Toolbar.js.map +1 -0
  17. package/dist/buttons.d.ts +23 -0
  18. package/dist/buttons.d.ts.map +1 -0
  19. package/dist/buttons.js +117 -0
  20. package/dist/buttons.js.map +1 -0
  21. package/dist/extensions/attributes.d.ts +44 -0
  22. package/dist/extensions/attributes.d.ts.map +1 -0
  23. package/dist/extensions/attributes.js +138 -0
  24. package/dist/extensions/attributes.js.map +1 -0
  25. package/dist/extensions/index.d.ts +19 -0
  26. package/dist/extensions/index.d.ts.map +1 -0
  27. package/dist/extensions/index.js +96 -0
  28. package/dist/extensions/index.js.map +1 -0
  29. package/dist/extensions/marks.d.ts +4 -0
  30. package/dist/extensions/marks.d.ts.map +1 -0
  31. package/dist/extensions/marks.js +62 -0
  32. package/dist/extensions/marks.js.map +1 -0
  33. package/dist/extensions/paste.d.ts +32 -0
  34. package/dist/extensions/paste.d.ts.map +1 -0
  35. package/dist/extensions/paste.js +131 -0
  36. package/dist/extensions/paste.js.map +1 -0
  37. package/dist/extensions/raw.d.ts +23 -0
  38. package/dist/extensions/raw.d.ts.map +1 -0
  39. package/dist/extensions/raw.js +269 -0
  40. package/dist/extensions/raw.js.map +1 -0
  41. package/dist/html.d.ts +21 -0
  42. package/dist/html.d.ts.map +1 -0
  43. package/dist/html.js +51 -0
  44. package/dist/html.js.map +1 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +7 -0
  48. package/dist/plugin.d.ts +26 -0
  49. package/dist/plugin.d.ts.map +1 -0
  50. package/dist/plugin.js +46 -0
  51. package/dist/plugin.js.map +1 -0
  52. package/dist/settings.d.ts +22 -0
  53. package/dist/settings.d.ts.map +1 -0
  54. package/dist/settings.js +45 -0
  55. package/dist/settings.js.map +1 -0
  56. package/dist/styles.d.ts +16 -0
  57. package/dist/styles.d.ts.map +1 -0
  58. package/dist/styles.js +86 -0
  59. package/dist/styles.js.map +1 -0
  60. package/package.json +71 -5
@@ -0,0 +1,96 @@
1
+ import { HtmlAttributes } from "./attributes.js";
2
+ import { markExtensions } from "./marks.js";
3
+ import { HtmlBlock, HtmlInline, HtmlMark } from "./raw.js";
4
+ import { Paste } from "./paste.js";
5
+ import { Link } from "@tiptap/extension-link";
6
+ import { Blockquote } from "@tiptap/extension-blockquote";
7
+ import { Document } from "@tiptap/extension-document";
8
+ import { HardBreak } from "@tiptap/extension-hard-break";
9
+ import { Heading } from "@tiptap/extension-heading";
10
+ import { HorizontalRule } from "@tiptap/extension-horizontal-rule";
11
+ import { BulletList, ListItem, ListKeymap, OrderedList } from "@tiptap/extension-list";
12
+ import { Paragraph } from "@tiptap/extension-paragraph";
13
+ import { Text } from "@tiptap/extension-text";
14
+ import { Dropcursor, Gapcursor, Placeholder, UndoRedo } from "@tiptap/extensions";
15
+ //#region src/extensions/index.ts
16
+ /**
17
+ * Links with their attributes in the order they were written in (CKEditor 5 writes `target` and
18
+ * `rel` before `href`), and new links with `href` first.
19
+ */
20
+ var KeptLink = Link.extend({
21
+ addAttributes() {
22
+ return {
23
+ ...this.parent?.(),
24
+ order: {
25
+ default: null,
26
+ rendered: false,
27
+ parseHTML: (element) => element.getAttributeNames().join(" ") || null
28
+ }
29
+ };
30
+ },
31
+ renderHTML(props) {
32
+ const [tag, attributes] = this.parent?.(props);
33
+ const order = typeof props.mark.attrs.order === "string" ? props.mark.attrs.order : "href";
34
+ return [
35
+ tag,
36
+ {
37
+ ...Object.fromEntries(order.split(" ").filter((name) => attributes[name] !== void 0 && attributes[name] !== null).map((name) => [name, attributes[name]])),
38
+ ...attributes
39
+ },
40
+ 0
41
+ ];
42
+ }
43
+ });
44
+ /**
45
+ * The TipTap extensions of the rich text field. Their schema reads everything the field may hold,
46
+ * whatever its toolbar offers: every heading level, the marks, attributes, and HTML it doesn't
47
+ * edit, kept as it was written.
48
+ */
49
+ function createRichTextExtensions({ extensions = [], placeholder, html = {} } = {}) {
50
+ const builtIn = [
51
+ Document,
52
+ Paragraph,
53
+ Text,
54
+ Heading.configure({ levels: [
55
+ 1,
56
+ 2,
57
+ 3,
58
+ 4,
59
+ 5,
60
+ 6
61
+ ] }),
62
+ Blockquote,
63
+ BulletList,
64
+ OrderedList,
65
+ ListItem,
66
+ ListKeymap,
67
+ HardBreak,
68
+ HorizontalRule,
69
+ ...markExtensions,
70
+ KeptLink.configure({
71
+ autolink: false,
72
+ linkOnPaste: false,
73
+ openOnClick: false,
74
+ HTMLAttributes: {
75
+ target: null,
76
+ rel: null,
77
+ class: null
78
+ }
79
+ }),
80
+ HtmlAttributes,
81
+ HtmlBlock.configure(html),
82
+ HtmlInline.configure(html),
83
+ HtmlMark,
84
+ Paste,
85
+ UndoRedo,
86
+ Dropcursor,
87
+ Gapcursor,
88
+ ...placeholder !== void 0 && placeholder !== "" ? [Placeholder.configure({ placeholder })] : []
89
+ ];
90
+ const replaced = new Set(extensions.map((extension) => extension.name));
91
+ return [...builtIn.filter((extension) => !replaced.has(extension.name)), ...extensions];
92
+ }
93
+ //#endregion
94
+ export { HtmlAttributes, HtmlBlock, HtmlInline, HtmlMark, createRichTextExtensions };
95
+
96
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["AnyExtension","Blockquote","Document","HardBreak","Heading","HorizontalRule","Link","BulletList","ListItem","ListKeymap","OrderedList","Paragraph","Text","Dropcursor","Gapcursor","Placeholder","UndoRedo","HtmlAttributes","markExtensions","Paste","HtmlBlock","HtmlInline","HtmlMark","HtmlNodeOptions","KeptLink","extend","addAttributes","parent","order","default","rendered","parseHTML","element","HTMLElement","getAttributeNames","join","renderHTML","props","tag","attributes","Record","mark","attrs","ordered","Object","fromEntries","split","filter","name","undefined","map","RichTextExtensionsOptions","extensions","placeholder","html","Partial","createRichTextExtensions","builtIn","configure","levels","autolink","linkOnPaste","openOnClick","HTMLAttributes","target","rel","class","replaced","Set","extension","has"],"sources":["../../src/extensions/index.ts"],"sourcesContent":["import type { AnyExtension } from '@tiptap/core';\nimport { Blockquote } from '@tiptap/extension-blockquote';\nimport { Document } from '@tiptap/extension-document';\nimport { HardBreak } from '@tiptap/extension-hard-break';\nimport { Heading } from '@tiptap/extension-heading';\nimport { HorizontalRule } from '@tiptap/extension-horizontal-rule';\nimport { Link } from '@tiptap/extension-link';\nimport { BulletList, ListItem, ListKeymap, OrderedList } from '@tiptap/extension-list';\nimport { Paragraph } from '@tiptap/extension-paragraph';\nimport { Text } from '@tiptap/extension-text';\nimport { Dropcursor, Gapcursor, Placeholder, UndoRedo } from '@tiptap/extensions';\n\nimport { HtmlAttributes } from './attributes';\nimport { markExtensions } from './marks';\nimport { Paste } from './paste';\nimport { HtmlBlock, HtmlInline, HtmlMark, type HtmlNodeOptions } from './raw';\n\n/**\n * Links with their attributes in the order they were written in (CKEditor 5 writes `target` and\n * `rel` before `href`), and new links with `href` first.\n */\nconst KeptLink = Link.extend({\n addAttributes() {\n return {\n ...this.parent?.(),\n order: {\n default: null,\n rendered: false,\n parseHTML: (element: HTMLElement) => element.getAttributeNames().join(' ') || null,\n },\n };\n },\n renderHTML(props) {\n const [tag, attributes] = this.parent?.(props) as [string, Record<string, unknown>, 0];\n const order = typeof props.mark.attrs.order === 'string' ? props.mark.attrs.order : 'href';\n const ordered = Object.fromEntries(\n order\n .split(' ')\n .filter((name) => attributes[name] !== undefined && attributes[name] !== null)\n .map((name) => [name, attributes[name]]),\n );\n return [tag, { ...ordered, ...attributes }, 0];\n },\n});\n\nexport interface RichTextExtensionsOptions {\n /** TipTap extensions of the project. One with the name of a built-in extension replaces it. */\n extensions?: readonly AnyExtension[];\n /** Text shown in the empty editor. */\n placeholder?: string;\n /** How kept HTML shows in the editor. */\n html?: Partial<HtmlNodeOptions>;\n}\n\n/**\n * The TipTap extensions of the rich text field. Their schema reads everything the field may hold,\n * whatever its toolbar offers: every heading level, the marks, attributes, and HTML it doesn't\n * edit, kept as it was written.\n */\nexport function createRichTextExtensions({\n extensions = [],\n placeholder,\n html = {},\n}: RichTextExtensionsOptions = {}): AnyExtension[] {\n const builtIn: AnyExtension[] = [\n Document,\n Paragraph,\n Text,\n Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }),\n Blockquote,\n BulletList,\n OrderedList,\n ListItem,\n ListKeymap,\n HardBreak,\n HorizontalRule,\n ...markExtensions,\n KeptLink.configure({\n // Links are added through the toolbar: typing or pasting a URL keeps it as text.\n autolink: false,\n linkOnPaste: false,\n openOnClick: false,\n // No `target` or `rel` on every link: they are set per link.\n HTMLAttributes: { target: null, rel: null, class: null },\n }),\n HtmlAttributes,\n HtmlBlock.configure(html),\n HtmlInline.configure(html),\n HtmlMark,\n Paste,\n UndoRedo,\n Dropcursor,\n Gapcursor,\n ...(placeholder !== undefined && placeholder !== ''\n ? [Placeholder.configure({ placeholder })]\n : []),\n ];\n const replaced = new Set(extensions.map((extension) => extension.name));\n return [...builtIn.filter((extension) => !replaced.has(extension.name)), ...extensions];\n}\n\nexport { HtmlAttributes, HtmlBlock, HtmlInline, HtmlMark };\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAqBA,IAAMwB,WAAWlB,KAAKmB,OAAO;CACzBC,gBAAgB;EACZ,OAAO;GACH,GAAG,KAAKC,SAAS;GACjBC,OAAO;IACHC,SAAS;IACTC,UAAU;IACVC,YAAYC,YAAyBA,QAAQE,kBAAkB,CAAC,CAACC,KAAK,GAAG,KAAK;GAClF;EACJ;CACJ;CACAC,WAAWC,OAAO;EACd,MAAM,CAACC,KAAKC,cAAc,KAAKZ,SAASU,KAAK;EAC7C,MAAMT,QAAQ,OAAOS,MAAMI,KAAKC,MAAMd,UAAU,WAAWS,MAAMI,KAAKC,MAAMd,QAAQ;EAOpF,OAAO;GAACU;GAAK;IAAE,GANCM,OAAOC,YACnBjB,MACKkB,MAAM,GAAG,CAAC,CACVC,QAAQC,SAAST,WAAWS,UAAUC,KAAAA,KAAaV,WAAWS,UAAU,IAAI,CAAC,CAC7EE,KAAKF,SAAS,CAACA,MAAMT,WAAWS,KAAK,CAAC,CAE7BL;IAAS,GAAGJ;GAAW;GAAG;EAAC;CACjD;AACJ,CAAC;;;;;;AAgBD,SAAgBiB,yBAAyB,EACrCJ,aAAa,CAAA,GACbC,aACAC,OAAO,CAAC,MACmB,CAAC,GAAmB;CAC/C,MAAMG,UAA0B;EAC5BvD;EACAS;EACAC;EACAR,QAAQsD,UAAU,EAAEC,QAAQ;GAAC;GAAG;GAAG;GAAG;GAAG;GAAG;EAAC,EAAE,CAAC;EAChD1D;EACAM;EACAG;EACAF;EACAC;EACAN;EACAE;EACA,GAAGa;EACHM,SAASkC,UAAU;GAEfE,UAAU;GACVC,aAAa;GACbC,aAAa;GAEbC,gBAAgB;IAAEC,QAAQ;IAAMC,KAAK;IAAMC,OAAO;GAAK;EAC3D,CAAC;EACDjD;EACAG,UAAUsC,UAAUJ,IAAI;EACxBjC,WAAWqC,UAAUJ,IAAI;EACzBhC;EACAH;EACAH;EACAH;EACAC;EACA,GAAIuC,gBAAgBJ,KAAAA,KAAaI,gBAAgB,KAC3C,CAACtC,YAAY2C,UAAU,EAAEL,YAAY,CAAC,CAAC,IACvC,CAAA;CAAG;CAEb,MAAMc,WAAW,IAAIC,IAAIhB,WAAWF,KAAKmB,cAAcA,UAAUrB,IAAI,CAAC;CACtE,OAAO,CAAC,GAAGS,QAAQV,QAAQsB,cAAc,CAACF,SAASG,IAAID,UAAUrB,IAAI,CAAC,GAAG,GAAGI,UAAU;AAC1F"}
@@ -0,0 +1,4 @@
1
+ import { type Mark } from '@tiptap/core';
2
+ /** The marks of the field: bold, italic, underline, strikethrough, code, subscript, superscript. */
3
+ export declare const markExtensions: Mark<import("@tiptap/extension-bold").BoldOptions, any>[];
4
+ //# sourceMappingURL=marks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"marks.d.ts","sourceRoot":"","sources":["../../src/extensions/marks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,EAAmB,MAAM,cAAc,CAAC;AA4C1D,oGAAoG;AACpG,eAAO,MAAM,cAAc,2DAY1B,CAAC"}
@@ -0,0 +1,62 @@
1
+ import { mergeAttributes } from "@tiptap/core";
2
+ import { Bold } from "@tiptap/extension-bold";
3
+ import { Code } from "@tiptap/extension-code";
4
+ import { Italic } from "@tiptap/extension-italic";
5
+ import { Strike } from "@tiptap/extension-strike";
6
+ import { Subscript } from "@tiptap/extension-subscript";
7
+ import { Superscript } from "@tiptap/extension-superscript";
8
+ import { Underline } from "@tiptap/extension-underline";
9
+ //#region src/extensions/marks.ts
10
+ /**
11
+ * A mark that is written with the element it was read from: `<b>` stays `<b>` and `<i>` stays
12
+ * `<i>` (CKEditor 5 writes italics as `<i>`), and new marks use `tag`. Only elements are read:
13
+ * styles (`font-weight: bold` on a `<span>`) stay on their element.
14
+ */
15
+ function keepTag(mark, tag, rules) {
16
+ return mark.extend({
17
+ addAttributes() {
18
+ return {
19
+ ...this.parent?.(),
20
+ tag: {
21
+ default: null,
22
+ rendered: false,
23
+ parseHTML: (element) => {
24
+ const name = element.tagName.toLowerCase();
25
+ return name === tag ? null : name;
26
+ }
27
+ }
28
+ };
29
+ },
30
+ parseHTML() {
31
+ return [...rules];
32
+ },
33
+ renderHTML({ mark: node, HTMLAttributes }) {
34
+ return [
35
+ typeof node.attrs.tag === "string" ? node.attrs.tag : tag,
36
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
37
+ 0
38
+ ];
39
+ }
40
+ });
41
+ }
42
+ /** The marks of the field: bold, italic, underline, strikethrough, code, subscript, superscript. */
43
+ var markExtensions = [
44
+ keepTag(Bold, "strong", [{ tag: "strong" }, {
45
+ tag: "b",
46
+ getAttrs: (node) => node.style.fontWeight !== "normal" && null
47
+ }]),
48
+ keepTag(Italic, "em", [{ tag: "em" }, { tag: "i" }]),
49
+ keepTag(Underline, "u", [{ tag: "u" }]),
50
+ keepTag(Strike, "s", [
51
+ { tag: "s" },
52
+ { tag: "del" },
53
+ { tag: "strike" }
54
+ ]),
55
+ keepTag(Code, "code", [{ tag: "code" }]),
56
+ keepTag(Subscript, "sub", [{ tag: "sub" }]),
57
+ keepTag(Superscript, "sup", [{ tag: "sup" }])
58
+ ];
59
+ //#endregion
60
+ export { markExtensions };
61
+
62
+ //# sourceMappingURL=marks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"marks.js","names":["Mark","mergeAttributes","Bold","Code","Italic","Strike","Subscript","Superscript","Underline","TagParseRule","keepTag","HTMLAttributes","Record","mark","Options","tag","rules","extend","addAttributes","parent","default","rendered","parseHTML","element","HTMLElement","name","tagName","toLowerCase","renderHTML","node","attrs","options","markExtensions","getAttrs","style","fontWeight"],"sources":["../../src/extensions/marks.ts"],"sourcesContent":["import { type Mark, mergeAttributes } from '@tiptap/core';\nimport { Bold } from '@tiptap/extension-bold';\nimport { Code } from '@tiptap/extension-code';\nimport { Italic } from '@tiptap/extension-italic';\nimport { Strike } from '@tiptap/extension-strike';\nimport { Subscript } from '@tiptap/extension-subscript';\nimport { Superscript } from '@tiptap/extension-superscript';\nimport { Underline } from '@tiptap/extension-underline';\nimport type { TagParseRule } from '@tiptap/pm/model';\n\n/**\n * A mark that is written with the element it was read from: `<b>` stays `<b>` and `<i>` stays\n * `<i>` (CKEditor 5 writes italics as `<i>`), and new marks use `tag`. Only elements are read:\n * styles (`font-weight: bold` on a `<span>`) stay on their element.\n */\nfunction keepTag<Options extends { HTMLAttributes: Record<string, unknown> }>(\n mark: Mark<Options>,\n tag: string,\n rules: readonly TagParseRule[],\n): Mark<Options> {\n return mark.extend({\n addAttributes() {\n return {\n ...this.parent?.(),\n tag: {\n default: null,\n rendered: false,\n parseHTML: (element: HTMLElement) => {\n const name = element.tagName.toLowerCase();\n return name === tag ? null : name;\n },\n },\n };\n },\n parseHTML() {\n return [...rules];\n },\n renderHTML({ mark: node, HTMLAttributes }) {\n const name = typeof node.attrs.tag === 'string' ? node.attrs.tag : tag;\n return [name, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];\n },\n });\n}\n\n/** The marks of the field: bold, italic, underline, strikethrough, code, subscript, superscript. */\nexport const markExtensions = [\n keepTag(Bold, 'strong', [\n { tag: 'strong' },\n // Google Docs wraps pasted content in `<b style=\"font-weight: normal\">`.\n { tag: 'b', getAttrs: (node) => node.style.fontWeight !== 'normal' && null },\n ]),\n keepTag(Italic, 'em', [{ tag: 'em' }, { tag: 'i' }]),\n keepTag(Underline, 'u', [{ tag: 'u' }]),\n keepTag(Strike, 's', [{ tag: 's' }, { tag: 'del' }, { tag: 'strike' }]),\n keepTag(Code, 'code', [{ tag: 'code' }]),\n keepTag(Subscript, 'sub', [{ tag: 'sub' }]),\n keepTag(Superscript, 'sup', [{ tag: 'sup' }]),\n];\n"],"mappings":";;;;;;;;;;;;;;AAeA,SAASU,QACLG,MACAE,KACAC,OACa;CACb,OAAOH,KAAKI,OAAO;EACfC,gBAAgB;GACZ,OAAO;IACH,GAAG,KAAKC,SAAS;IACjBJ,KAAK;KACDK,SAAS;KACTC,UAAU;KACVC,YAAYC,YAAyB;MACjC,MAAME,OAAOF,QAAQG,QAAQC,YAAY;MACzC,OAAOF,SAASV,MAAM,OAAOU;KACjC;IACJ;GACJ;EACJ;EACAH,YAAY;GACR,OAAO,CAAC,GAAGN,KAAK;EACpB;EACAY,WAAW,EAAEf,MAAMgB,MAAMlB,kBAAkB;GAEvC,OAAO;IADM,OAAOkB,KAAKC,MAAMf,QAAQ,WAAWc,KAAKC,MAAMf,MAAMA;IACrDd,gBAAgB,KAAK8B,QAAQpB,gBAAgBA,cAAc;IAAG;GAAC;EACjF;CACJ,CAAC;AACL;;AAGA,IAAaqB,iBAAiB;CAC1BtB,QAAQR,MAAM,UAAU,CACpB,EAAEa,KAAK,SAAS,GAEhB;EAAEA,KAAK;EAAKkB,WAAWJ,SAASA,KAAKK,MAAMC,eAAe,YAAY;CAAK,CAAC,CAC/E;CACDzB,QAAQN,QAAQ,MAAM,CAAC,EAAEW,KAAK,KAAK,GAAG,EAAEA,KAAK,IAAI,CAAC,CAAC;CACnDL,QAAQF,WAAW,KAAK,CAAC,EAAEO,KAAK,IAAI,CAAC,CAAC;CACtCL,QAAQL,QAAQ,KAAK;EAAC,EAAEU,KAAK,IAAI;EAAG,EAAEA,KAAK,MAAM;EAAG,EAAEA,KAAK,SAAS;CAAC,CAAC;CACtEL,QAAQP,MAAM,QAAQ,CAAC,EAAEY,KAAK,OAAO,CAAC,CAAC;CACvCL,QAAQJ,WAAW,OAAO,CAAC,EAAES,KAAK,MAAM,CAAC,CAAC;CAC1CL,QAAQH,aAAa,OAAO,CAAC,EAAEQ,KAAK,MAAM,CAAC,CAAC;AAAC"}
@@ -0,0 +1,32 @@
1
+ import { Extension } from '@tiptap/core';
2
+ import { DOMParser, DOMSerializer, type Schema } from '@tiptap/pm/model';
3
+ /**
4
+ * The token that the rich text fields of this page put on what they copy: random, so that no
5
+ * other page or document can claim to come from them.
6
+ */
7
+ export declare function getCopyToken(): string;
8
+ /** The serializer of copied content: `getCopyToken()` on its elements. */
9
+ export declare function createCopySerializer(schema: Schema): DOMSerializer;
10
+ /**
11
+ * The parser of pasted content: what the toolbar can make, without the attributes and the HTML
12
+ * that loaded content keeps, so that a page or a document pasted brings its text, its structure
13
+ * and its marks, not its styles (`<span style="font-size: 11pt">` of Google Docs). Bold and
14
+ * italic written as styles become marks. Content copied from a rich text field of this page
15
+ * (`getCopyToken()`), and only that, is read in full: kept HTML can't be brought in from
16
+ * elsewhere, even by HTML that claims to come from a ProseMirror editor (`data-pm-slice`).
17
+ * Attributes that would run a script are dropped in both cases.
18
+ */
19
+ export declare function createPasteParser(schema: Schema): DOMParser;
20
+ /**
21
+ * Pasted HTML that wasn't copied from a rich text field of this page, without the slice data of
22
+ * ProseMirror (`data-pm-slice`): its context is JSON that ProseMirror makes nodes of after the
23
+ * parser, attributes included, so any page could write it.
24
+ */
25
+ export declare function withoutForeignSlice(html: string): string;
26
+ /**
27
+ * Pasted content brings what the toolbar can make, not the styles of its page
28
+ * (`createPasteParser()`), and only content copied from a rich text field of this page brings
29
+ * its context (`withoutForeignSlice()`).
30
+ */
31
+ export declare const Paste: Extension<any, any>;
32
+ //# sourceMappingURL=paste.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paste.d.ts","sourceRoot":"","sources":["../../src/extensions/paste.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EACH,SAAS,EACT,aAAa,EAIb,KAAK,MAAM,EAGd,MAAM,kBAAkB,CAAC;AAQ1B;;;GAGG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAMrC;AAgCD,0EAA0E;AAC1E,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,CAGlE;AA8CD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAuB3D;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAaxD;AAED;;;;GAIG;AACH,eAAO,MAAM,KAAK,qBAehB,CAAC"}
@@ -0,0 +1,131 @@
1
+ import { CLIPBOARD_SLICE, COPIED_FROM } from "./attributes.js";
2
+ import { restoreKeptHtml } from "./raw.js";
3
+ import { Extension } from "@tiptap/core";
4
+ import { DOMParser, DOMSerializer } from "@tiptap/pm/model";
5
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
6
+ //#region src/extensions/paste.ts
7
+ var copyToken = null;
8
+ /**
9
+ * The token that the rich text fields of this page put on what they copy: random, so that no
10
+ * other page or document can claim to come from them.
11
+ */
12
+ function getCopyToken() {
13
+ copyToken ??= Array.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16)), (byte) => byte.toString(16).padStart(2, "0")).join("");
14
+ return copyToken;
15
+ }
16
+ /** Whether pasted content was copied from a rich text field of this page. */
17
+ function isCopiedHere(dom) {
18
+ if (!(dom instanceof Element)) return false;
19
+ const selector = `[${COPIED_FROM}="${getCopyToken()}"]`;
20
+ return dom.matches(selector) || dom.querySelector(selector) !== null;
21
+ }
22
+ /** Writes copied content as the field does, with the token of this page on its elements. */
23
+ var CopySerializer = class extends DOMSerializer {
24
+ serializeFragment(fragment, options = {}, target) {
25
+ const result = super.serializeFragment(fragment, options, target);
26
+ if (target === void 0) {
27
+ restoreKeptHtml(result);
28
+ result.childNodes.forEach((child) => {
29
+ if (child.nodeType === 1) child.setAttribute(COPIED_FROM, getCopyToken());
30
+ });
31
+ }
32
+ return result;
33
+ }
34
+ };
35
+ /** The serializer of copied content: `getCopyToken()` on its elements. */
36
+ function createCopySerializer(schema) {
37
+ const { nodes, marks } = DOMSerializer.fromSchema(schema);
38
+ return new CopySerializer(nodes, marks);
39
+ }
40
+ /** What the field keeps as it was written, which pasted content doesn't bring. */
41
+ var KEPT_AS_WRITTEN = /* @__PURE__ */ new Set([
42
+ "htmlBlock",
43
+ "htmlInline",
44
+ "htmlMark"
45
+ ]);
46
+ /** The attributes of a rule's result, without the attributes kept from the HTML. */
47
+ function withoutHtmlAttributes(rule) {
48
+ if (!("tag" in rule)) return rule;
49
+ const tagRule = rule;
50
+ const { getAttrs } = tagRule;
51
+ if (getAttrs === void 0) return rule;
52
+ return {
53
+ ...tagRule,
54
+ getAttrs: (node) => {
55
+ const attrs = getAttrs(node);
56
+ if (attrs === null || attrs === false) return attrs;
57
+ const { htmlAttributes: _htmlAttributes, ...others } = attrs;
58
+ return others;
59
+ }
60
+ };
61
+ }
62
+ /** Pasted content copied from a rich text field of this page is read in full. */
63
+ var PasteParser = class extends DOMParser {
64
+ full;
65
+ constructor(schema, rules, full) {
66
+ super(schema, rules);
67
+ this.full = full;
68
+ }
69
+ parseSlice(dom, options) {
70
+ return isCopiedHere(dom) ? this.full.parseSlice(dom, options) : super.parseSlice(dom, options);
71
+ }
72
+ };
73
+ /**
74
+ * The parser of pasted content: what the toolbar can make, without the attributes and the HTML
75
+ * that loaded content keeps, so that a page or a document pasted brings its text, its structure
76
+ * and its marks, not its styles (`<span style="font-size: 11pt">` of Google Docs). Bold and
77
+ * italic written as styles become marks. Content copied from a rich text field of this page
78
+ * (`getCopyToken()`), and only that, is read in full: kept HTML can't be brought in from
79
+ * elsewhere, even by HTML that claims to come from a ProseMirror editor (`data-pm-slice`).
80
+ * Attributes that would run a script are dropped in both cases.
81
+ */
82
+ function createPasteParser(schema) {
83
+ const full = DOMParser.fromSchema(schema);
84
+ const rules = full.rules.filter((rule) => !KEPT_AS_WRITTEN.has(("node" in rule ? rule.node : void 0) ?? rule.mark ?? "")).map(withoutHtmlAttributes);
85
+ const styles = [...schema.marks.bold !== void 0 ? [{
86
+ style: "font-weight",
87
+ mark: "bold",
88
+ getAttrs: (value) => /^(bold(er)?|[5-9]\d{2,})$/.test(value) && null
89
+ }] : [], ...schema.marks.italic !== void 0 ? [{
90
+ style: "font-style=italic",
91
+ mark: "italic"
92
+ }] : []];
93
+ return new PasteParser(schema, [...rules, ...styles], full);
94
+ }
95
+ /**
96
+ * Pasted HTML that wasn't copied from a rich text field of this page, without the slice data of
97
+ * ProseMirror (`data-pm-slice`): its context is JSON that ProseMirror makes nodes of after the
98
+ * parser, attributes included, so any page could write it.
99
+ */
100
+ function withoutForeignSlice(html) {
101
+ const template = document.createElement("template");
102
+ template.innerHTML = html;
103
+ const { content } = template;
104
+ if (content.querySelector(`[data-panneau-copy="${getCopyToken()}"]`) !== null) return html;
105
+ const slices = content.querySelectorAll(`[${CLIPBOARD_SLICE}]`);
106
+ if (slices.length === 0) return html;
107
+ slices.forEach((element) => element.removeAttribute(CLIPBOARD_SLICE));
108
+ return template.innerHTML;
109
+ }
110
+ /**
111
+ * Pasted content brings what the toolbar can make, not the styles of its page
112
+ * (`createPasteParser()`), and only content copied from a rich text field of this page brings
113
+ * its context (`withoutForeignSlice()`).
114
+ */
115
+ var Paste = Extension.create({
116
+ name: "panneauPaste",
117
+ addProseMirrorPlugins() {
118
+ return [new Plugin({
119
+ key: new PluginKey("panneauPaste"),
120
+ props: {
121
+ transformPastedHTML: withoutForeignSlice,
122
+ clipboardParser: createPasteParser(this.editor.schema),
123
+ clipboardSerializer: createCopySerializer(this.editor.schema)
124
+ }
125
+ })];
126
+ }
127
+ });
128
+ //#endregion
129
+ export { Paste, createCopySerializer, createPasteParser, getCopyToken, withoutForeignSlice };
130
+
131
+ //# sourceMappingURL=paste.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paste.js","names":["Extension","DOMParser","DOMSerializer","Fragment","ParseOptions","ParseRule","Schema","Slice","TagParseRule","Plugin","PluginKey","CLIPBOARD_SLICE","COPIED_FROM","restoreKeptHtml","copyToken","getCopyToken","Array","from","crypto","getRandomValues","Uint8Array","byte","toString","padStart","join","isCopiedHere","dom","globalThis","Node","Element","selector","matches","querySelector","CopySerializer","serializeFragment","fragment","options","document","Document","target","HTMLElement","DocumentFragment","result","undefined","childNodes","forEach","child","nodeType","setAttribute","createCopySerializer","schema","nodes","marks","fromSchema","KEPT_AS_WRITTEN","Set","withoutHtmlAttributes","rule","tagRule","getAttrs","next","node","attrs","htmlAttributes","_htmlAttributes","others","PasteParser","constructor","rules","full","parseSlice","createPasteParser","filter","has","mark","map","styles","bold","style","value","test","italic","withoutForeignSlice","html","template","createElement","innerHTML","content","slices","querySelectorAll","length","element","removeAttribute","Paste","create","name","addProseMirrorPlugins","key","props","transformPastedHTML","clipboardParser","editor","clipboardSerializer"],"sources":["../../src/extensions/paste.ts"],"sourcesContent":["import { Extension } from '@tiptap/core';\nimport {\n DOMParser,\n DOMSerializer,\n type Fragment,\n type ParseOptions,\n type ParseRule,\n type Schema,\n type Slice,\n type TagParseRule,\n} from '@tiptap/pm/model';\nimport { Plugin, PluginKey } from '@tiptap/pm/state';\n\nimport { CLIPBOARD_SLICE, COPIED_FROM } from './attributes';\nimport { restoreKeptHtml } from './raw';\n\nlet copyToken: string | null = null;\n\n/**\n * The token that the rich text fields of this page put on what they copy: random, so that no\n * other page or document can claim to come from them.\n */\nexport function getCopyToken(): string {\n // `randomUUID()` only exists in secure contexts: an admin can be served over plain HTTP\n copyToken ??= Array.from(crypto.getRandomValues(new Uint8Array(16)), (byte) =>\n byte.toString(16).padStart(2, '0'),\n ).join('');\n return copyToken;\n}\n\n/** Whether pasted content was copied from a rich text field of this page. */\nfunction isCopiedHere(dom: globalThis.Node): boolean {\n if (!(dom instanceof Element)) {\n return false;\n }\n const selector = `[${COPIED_FROM}=\"${getCopyToken()}\"]`;\n return dom.matches(selector) || dom.querySelector(selector) !== null;\n}\n\n/** Writes copied content as the field does, with the token of this page on its elements. */\nclass CopySerializer extends DOMSerializer {\n override serializeFragment(\n fragment: Fragment,\n options: { document?: Document } = {},\n target?: HTMLElement | DocumentFragment,\n ): HTMLElement | DocumentFragment {\n const result = super.serializeFragment(fragment, options, target);\n if (target === undefined) {\n // ProseMirror writes the clipboard in an inert document\n restoreKeptHtml(result);\n result.childNodes.forEach((child) => {\n if (child.nodeType === 1) {\n (child as Element).setAttribute(COPIED_FROM, getCopyToken());\n }\n });\n }\n return result;\n }\n}\n\n/** The serializer of copied content: `getCopyToken()` on its elements. */\nexport function createCopySerializer(schema: Schema): DOMSerializer {\n const { nodes, marks } = DOMSerializer.fromSchema(schema);\n return new CopySerializer(nodes, marks);\n}\n\n/** What the field keeps as it was written, which pasted content doesn't bring. */\nconst KEPT_AS_WRITTEN = new Set(['htmlBlock', 'htmlInline', 'htmlMark']);\n\n/** The attributes of a rule's result, without the attributes kept from the HTML. */\nfunction withoutHtmlAttributes(rule: ParseRule): ParseRule {\n if (!('tag' in rule)) {\n return rule;\n }\n const tagRule = rule as TagParseRule;\n const { getAttrs } = tagRule;\n if (getAttrs === undefined) {\n return rule;\n }\n const next: TagParseRule = {\n ...tagRule,\n getAttrs: (node: HTMLElement) => {\n const attrs = getAttrs(node);\n if (attrs === null || attrs === false) {\n return attrs;\n }\n const { htmlAttributes: _htmlAttributes, ...others } = attrs;\n return others;\n },\n };\n return next;\n}\n\n/** Pasted content copied from a rich text field of this page is read in full. */\nclass PasteParser extends DOMParser {\n constructor(\n schema: Schema,\n rules: readonly ParseRule[],\n private readonly full: DOMParser,\n ) {\n super(schema, rules);\n }\n\n override parseSlice(dom: globalThis.Node, options?: ParseOptions): Slice {\n return isCopiedHere(dom)\n ? this.full.parseSlice(dom, options)\n : super.parseSlice(dom, options);\n }\n}\n\n/**\n * The parser of pasted content: what the toolbar can make, without the attributes and the HTML\n * that loaded content keeps, so that a page or a document pasted brings its text, its structure\n * and its marks, not its styles (`<span style=\"font-size: 11pt\">` of Google Docs). Bold and\n * italic written as styles become marks. Content copied from a rich text field of this page\n * (`getCopyToken()`), and only that, is read in full: kept HTML can't be brought in from\n * elsewhere, even by HTML that claims to come from a ProseMirror editor (`data-pm-slice`).\n * Attributes that would run a script are dropped in both cases.\n */\nexport function createPasteParser(schema: Schema): DOMParser {\n const full = DOMParser.fromSchema(schema);\n const rules = full.rules\n .filter(\n (rule) =>\n !KEPT_AS_WRITTEN.has(('node' in rule ? rule.node : undefined) ?? rule.mark ?? ''),\n )\n .map(withoutHtmlAttributes);\n const styles: ParseRule[] = [\n ...(schema.marks.bold !== undefined\n ? [\n {\n style: 'font-weight',\n mark: 'bold',\n getAttrs: (value: string) => /^(bold(er)?|[5-9]\\d{2,})$/.test(value) && null,\n },\n ]\n : []),\n ...(schema.marks.italic !== undefined\n ? [{ style: 'font-style=italic', mark: 'italic' }]\n : []),\n ];\n return new PasteParser(schema, [...rules, ...styles], full);\n}\n\n/**\n * Pasted HTML that wasn't copied from a rich text field of this page, without the slice data of\n * ProseMirror (`data-pm-slice`): its context is JSON that ProseMirror makes nodes of after the\n * parser, attributes included, so any page could write it.\n */\nexport function withoutForeignSlice(html: string): string {\n const template = document.createElement('template');\n template.innerHTML = html;\n const { content } = template;\n if (content.querySelector(`[${COPIED_FROM}=\"${getCopyToken()}\"]`) !== null) {\n return html;\n }\n const slices = content.querySelectorAll(`[${CLIPBOARD_SLICE}]`);\n if (slices.length === 0) {\n return html;\n }\n slices.forEach((element) => element.removeAttribute(CLIPBOARD_SLICE));\n return template.innerHTML;\n}\n\n/**\n * Pasted content brings what the toolbar can make, not the styles of its page\n * (`createPasteParser()`), and only content copied from a rich text field of this page brings\n * its context (`withoutForeignSlice()`).\n */\nexport const Paste = Extension.create({\n // TipTap's core has an extension named `paste`\n name: 'panneauPaste',\n addProseMirrorPlugins() {\n return [\n new Plugin({\n key: new PluginKey('panneauPaste'),\n props: {\n transformPastedHTML: withoutForeignSlice,\n clipboardParser: createPasteParser(this.editor.schema),\n clipboardSerializer: createCopySerializer(this.editor.schema),\n },\n }),\n ];\n },\n});\n"],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { Mark, Node } from '@tiptap/core';
2
+ export interface HtmlNodeOptions {
3
+ /** Names kept HTML in the editor, for assistive technologies: "HTML". */
4
+ label: string;
5
+ /** Class name of the element that stands for kept HTML in the editor. */
6
+ className: string;
7
+ }
8
+ /**
9
+ * Replaces the placeholders of kept HTML with the HTML they hold, in content written outside the
10
+ * page: an inert document, or a fragment of one.
11
+ */
12
+ export declare function restoreKeptHtml(root: ParentNode): void;
13
+ /** A block of HTML that the field keeps as it was written. */
14
+ export declare const HtmlBlock: Node<HtmlNodeOptions, any>;
15
+ /** An element of HTML in the text that the field keeps as it was written: an image, an iframe. */
16
+ export declare const HtmlInline: Node<HtmlNodeOptions, any>;
17
+ /**
18
+ * An element around text that the field keeps with its attributes: `<span style="…">`, `<abbr
19
+ * title="…">`, an anchor without `href`. A `<span>` without attributes is dropped, as browsers
20
+ * render it the same.
21
+ */
22
+ export declare const HtmlMark: Mark<any, any>;
23
+ //# sourceMappingURL=raw.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"raw.d.ts","sourceRoot":"","sources":["../../src/extensions/raw.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AA4F1C,MAAM,WAAW,eAAe;IAC5B,yEAAyE;IACzE,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,SAAS,EAAE,MAAM,CAAC;CACrB;AAYD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAOtD;AA4ED,8DAA8D;AAC9D,eAAO,MAAM,SAAS,4BAiCpB,CAAC;AAEH,kGAAkG;AAClG,eAAO,MAAM,UAAU,4BA8BrB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,QAAQ,gBAwCnB,CAAC"}