@doist/typist 13.0.1 → 14.0.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.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,36 @@
1
+ ## [14.0.0](https://github.com/Doist/typist/compare/v13.0.1...v14.0.0) (2026-06-12)
2
+
3
+ ### ⚠ BREAKING CHANGES
4
+
5
+ * Tables are now enabled by default in the RichTextKit
6
+ (disable with `table: false`), which means Markdown content with GFM
7
+ pipe-table syntax now renders as native tables in the editor.
8
+ Additionally, the `PasteHTMLTableAsString` extension is no longer
9
+ registered by the RichTextKit (pasted tables become native tables),
10
+ and as such the `pasteHTMLTableAsString` option was removed from
11
+ `RichTextKitOptions` (the extension is still registered by the
12
+ PlainTextKit, where its option remains available).
13
+
14
+ * fix: do not register table extensions when the paragraph extension is disabled
15
+
16
+ * fix: restore hard breaks nested within styled text in table cells
17
+
18
+ * fix: skip the table cell hard break rule for schemas without hard breaks
19
+
20
+ * fix: escape text that looks like a br element in the Markdown output
21
+
22
+ * fix: disable the table toolbar button when the cursor is inside a table
23
+
24
+ * test: cover the table extensions exclusion in singleline documents
25
+
26
+ * fix: add a heading row to pasted tables without one
27
+
28
+ * fix: keep table extensions registered when the paragraph option is disabled
29
+
30
+ ### Features
31
+
32
+ * add GFM table support to the rich-text editor ([#1373](https://github.com/Doist/typist/issues/1373)) ([bdcab2d](https://github.com/Doist/typist/commit/bdcab2d7184daefd8a6fdb430be9eb0b7624b431))
33
+
1
34
  ## [13.0.1](https://github.com/Doist/typist/compare/v13.0.0...v13.0.1) (2026-06-11)
2
35
 
3
36
  ### Bug Fixes
@@ -5,6 +5,7 @@ import { RichTextDocumentOptions } from "./rich-text-document.js";
5
5
  import { RichTextLinkOptions } from "./rich-text-link.js";
6
6
  import { RichTextOrderedListOptions } from "./rich-text-ordered-list.js";
7
7
  import { RichTextStrikethroughOptions } from "./rich-text-strikethrough.js";
8
+ import { RichTextTableOptions } from "./rich-text-table.js";
8
9
  import { Extension } from "@tiptap/core";
9
10
  import { HistoryOptions } from "@tiptap/extension-history";
10
11
  import { ParagraphOptions } from "@tiptap/extension-paragraph";
@@ -112,14 +113,19 @@ type RichTextKitOptions = {
112
113
  * Set to `false` to disable the `PasteSinglelineText` extension.
113
114
  */
114
115
  pasteSinglelineText: false;
115
- /**
116
- * Set to `false` to disable the `PasteHTMLTableAsString` extension.
117
- */
118
- pasteHTMLTableAsString: false;
119
116
  /**
120
117
  * Set options for the `Strike` extension, or `false` to disable.
121
118
  */
122
119
  strike: Partial<RichTextStrikethroughOptions> | false;
120
+ /**
121
+ * Set options for the `Table` extension, or `false` to disable.
122
+ *
123
+ * Note that table cells are restricted to a single paragraph of content, since the GFM table
124
+ * syntax cannot represent multiple blocks within a cell (which also means that tables require
125
+ * a `paragraph` node in the editor schema). Additionally, tables are always disabled in
126
+ * singleline documents, since they serialize to multiple lines of Markdown.
127
+ */
128
+ table: Partial<RichTextTableOptions> | false;
123
129
  /**
124
130
  * Set to `false` to disable the `Text` extension.
125
131
  */
@@ -1 +1 @@
1
- {"version":3,"file":"rich-text-kit.d.ts","names":[],"sources":["../../../src/extensions/rich-text/rich-text-kit.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAoD6E;KAKxE,kBAAA;;;;EAID,UAAA,EAAY,OAAA,CAAQ,iBAAA;EAKd;;;EAAN,IAAA,EAAM,OAAA,CAAQ,WAAA;EAUR;;;EALN,UAAA,EAAY,OAAA,CAAQ,yBAAA;EAeV;;;EAVV,IAAA,EAAM,OAAA,CAAQ,WAAA;EAyBH;;;EApBX,SAAA,EAAW,OAAA,CAAQ,gBAAA;EA8BV;;;EAzBT,QAAA,EAAU,OAAA,CAAQ,uBAAA;EAmCX;;;EA9BP,UAAA,EAAY,OAAA,CAAQ,iBAAA;EAwCd;;;EAnCN,SAAA;EA6CY;;;EAxCZ,SAAA,EAAW,OAAA,CAAQ,gBAAA;EAkDR;;;EA7CX,OAAA,EAAS,OAAA,CAAQ,sBAAA;EAsEF;;;EAjEf,OAAA,EAAS,OAAA,CAAQ,cAAA;EA7CjB;;;EAkDA,cAAA,EAAgB,OAAA,CAAQ,qBAAA;EA7CZ;;;EAkDZ,KAAA,EAAO,OAAA,CAAQ,oBAAA;EA7CD;;;EAkDd,MAAA,EAAQ,OAAA,CAAQ,aAAA;EAxChB;;;EA6CA,IAAA,EAAM,OAAA,CAAQ,mBAAA;EAxCF;;;EA6CZ,QAAA,EAAU,OAAA,CAAQ,eAAA;EAnCP;;;EAwCX,UAAA,EAAY,OAAA,CAAQ,iBAAA;EAnCH;;;EAwCjB,WAAA,EAAa,OAAA,CAAQ,0BAAA;EA9BrB;;;EAmCA,SAAA,EAAW,OAAA,CAAQ,gBAAA;EA9BZ;;;EAmCP,WAAA;EA9BgB;;;EAmChB,aAAA;EAzBA;;;EA8BA,mBAAA;EAzBY;;;EA8BZ,sBAAA;EAzBqB;;;EA8BrB,MAAA,EAAQ,OAAA,CAAQ,4BAAA;EApBhB;;;EAyBA,IAAA;EALA;;;EAUA,UAAA;AAAA;;AAAU;AAAA;;;cAQR,WAAA,EAAW,SAAA,CAAA,kBAAA"}
1
+ {"version":3,"file":"rich-text-kit.d.ts","names":[],"sources":["../../../src/extensions/rich-text/rich-text-kit.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAwD6D;KAKxD,kBAAA;;;;EAID,UAAA,EAAY,OAAA,CAAQ,iBAAA;EAKd;;;EAAN,IAAA,EAAM,OAAA,CAAQ,WAAA;EAUR;;;EALN,UAAA,EAAY,OAAA,CAAQ,yBAAA;EAeV;;;EAVV,IAAA,EAAM,OAAA,CAAQ,WAAA;EAyBH;;;EApBX,SAAA,EAAW,OAAA,CAAQ,gBAAA;EA8BV;;;EAzBT,QAAA,EAAU,OAAA,CAAQ,uBAAA;EAmCX;;;EA9BP,UAAA,EAAY,OAAA,CAAQ,iBAAA;EAwCd;;;EAnCN,SAAA;EA6CY;;;EAxCZ,SAAA,EAAW,OAAA,CAAQ,gBAAA;EAkDR;;;EA7CX,OAAA,EAAS,OAAA,CAAQ,sBAAA;EA2EV;;;EAtEP,OAAA,EAAS,OAAA,CAAQ,cAAA;EAlDL;;;EAuDZ,cAAA,EAAgB,OAAA,CAAQ,qBAAA;EAlDV;;;EAuDd,KAAA,EAAO,OAAA,CAAQ,oBAAA;EA7Cf;;;EAkDA,MAAA,EAAQ,OAAA,CAAQ,aAAA;EA7CL;;;EAkDX,IAAA,EAAM,OAAA,CAAQ,mBAAA;EA7CI;;;EAkDlB,QAAA,EAAU,OAAA,CAAQ,eAAA;EAxClB;;;EA6CA,UAAA,EAAY,OAAA,CAAQ,iBAAA;EAnCpB;;;EAwCA,WAAA,EAAa,OAAA,CAAQ,0BAAA;EAnCZ;;;EAwCT,SAAA,EAAW,OAAA,CAAQ,gBAAA;EAnCK;;;EAwCxB,WAAA;EA9BA;;;EAmCA,aAAA;EA9BM;;;EAmCN,mBAAA;EA9BkB;;;EAmClB,MAAA,EAAQ,OAAA,CAAQ,4BAAA;EAzBhB;;;;;;;;EAmCA,KAAA,EAAO,OAAA,CAAQ,oBAAA;EAVf;;;EAeA,IAAA;EALO;;;EAUP,UAAA;AAAA;AAAU;AAAA;;;;AAAA,cAQR,WAAA,EAAW,SAAA,CAAA,kBAAA"}
@@ -1,6 +1,5 @@
1
1
  import "../../constants/extension-priorities.js";
2
2
  import { CopyMarkdownSource } from "../shared/copy-markdown-source.js";
3
- import { PasteHTMLTableAsString } from "../shared/paste-html-table-as-string.js";
4
3
  import { PasteSinglelineText } from "../shared/paste-singleline-text.js";
5
4
  import { BoldAndItalics } from "./bold-and-italics.js";
6
5
  import { CurvenoteCodemark } from "./curvenote-codemark.js";
@@ -14,6 +13,7 @@ import { RichTextImage } from "./rich-text-image.js";
14
13
  import { RichTextLink } from "./rich-text-link.js";
15
14
  import { RichTextOrderedList } from "./rich-text-ordered-list.js";
16
15
  import { RichTextStrikethrough } from "./rich-text-strikethrough.js";
16
+ import { RichTextTable } from "./rich-text-table.js";
17
17
  import { Extension } from "@tiptap/core";
18
18
  import { History } from "@tiptap/extension-history";
19
19
  import { Text } from "@tiptap/extension-text";
@@ -29,6 +29,9 @@ import { HorizontalRule } from "@tiptap/extension-horizontal-rule";
29
29
  import { Italic } from "@tiptap/extension-italic";
30
30
  import { ListItem } from "@tiptap/extension-list-item";
31
31
  import { ListKeymap } from "@tiptap/extension-list-keymap";
32
+ import { TableCell } from "@tiptap/extension-table-cell";
33
+ import { TableHeader } from "@tiptap/extension-table-header";
34
+ import { TableRow } from "@tiptap/extension-table-row";
32
35
  //#region src/extensions/rich-text/rich-text-kit.ts
33
36
  /**
34
37
  * The `RichTextKit` extension is a collection of the minimal required extensions to have a full
@@ -49,7 +52,6 @@ const RichTextKit = Extension.create({
49
52
  if (this.options?.pasteEmojis !== false) extensions.push(PasteEmojis);
50
53
  if (this.options?.pasteMarkdown !== false) extensions.push(PasteMarkdown);
51
54
  if (this.options?.document?.multiline === false && this.options?.pasteSinglelineText !== false) extensions.push(PasteSinglelineText);
52
- if (this.options?.pasteHTMLTableAsString !== false) extensions.push(PasteHTMLTableAsString);
53
55
  }
54
56
  if (this.options.dropCursor !== false) extensions.push(Dropcursor.configure(this.options?.dropCursor));
55
57
  if (this.options.gapCursor !== false) extensions.push(Gapcursor);
@@ -66,6 +68,8 @@ const RichTextKit = Extension.create({
66
68
  if (this.options.orderedList !== false) extensions.push(RichTextOrderedList.configure(this.options?.orderedList));
67
69
  if (this.options.paragraph !== false) extensions.push(Paragraph.configure(this.options?.paragraph));
68
70
  if (this.options.strike !== false) extensions.push(RichTextStrikethrough.configure(this.options?.strike));
71
+ const isSinglelineDocument = this.options.document !== false && this.options.document?.multiline === false;
72
+ if (this.options.table !== false && !isSinglelineDocument) extensions.push(RichTextTable.configure(this.options?.table), TableRow, TableHeader.extend({ content: "paragraph" }), TableCell.extend({ content: "paragraph" }));
69
73
  if (this.options.text !== false) extensions.push(Text);
70
74
  if (this.options.typography !== false) extensions.push(Typography);
71
75
  return extensions;
@@ -1 +1 @@
1
- {"version":3,"file":"rich-text-kit.js","names":[],"sources":["../../../src/extensions/rich-text/rich-text-kit.ts"],"sourcesContent":["import { Extension } from '@tiptap/core'\nimport { Blockquote } from '@tiptap/extension-blockquote'\nimport { Bold } from '@tiptap/extension-bold'\nimport { CodeBlock } from '@tiptap/extension-code-block'\nimport { Dropcursor } from '@tiptap/extension-dropcursor'\nimport { Gapcursor } from '@tiptap/extension-gapcursor'\nimport { HardBreak } from '@tiptap/extension-hard-break'\nimport { History } from '@tiptap/extension-history'\nimport { HorizontalRule } from '@tiptap/extension-horizontal-rule'\nimport { Italic } from '@tiptap/extension-italic'\nimport { ListItem } from '@tiptap/extension-list-item'\nimport { ListKeymap } from '@tiptap/extension-list-keymap'\nimport { Paragraph } from '@tiptap/extension-paragraph'\nimport { Text } from '@tiptap/extension-text'\nimport { Typography } from '@tiptap/extension-typography'\n\nimport { BLOCKQUOTE_EXTENSION_PRIORITY } from '../../constants/extension-priorities'\nimport { CopyMarkdownSource } from '../shared/copy-markdown-source'\nimport { PasteHTMLTableAsString } from '../shared/paste-html-table-as-string'\nimport { PasteSinglelineText } from '../shared/paste-singleline-text'\n\nimport { BoldAndItalics } from './bold-and-italics'\nimport { CurvenoteCodemark } from './curvenote-codemark'\nimport { PasteEmojis } from './paste-emojis'\nimport { PasteMarkdown } from './paste-markdown'\nimport { RichTextBulletList } from './rich-text-bullet-list'\nimport { RichTextCode } from './rich-text-code'\nimport { RichTextDocument } from './rich-text-document'\nimport { RichTextHeading, RichTextHeadingOptions } from './rich-text-heading'\nimport { RichTextImage } from './rich-text-image'\nimport { RichTextLink } from './rich-text-link'\nimport { RichTextOrderedList } from './rich-text-ordered-list'\nimport { RichTextStrikethrough } from './rich-text-strikethrough'\n\nimport type { Extensions } from '@tiptap/core'\nimport type { BlockquoteOptions } from '@tiptap/extension-blockquote'\nimport type { BoldOptions } from '@tiptap/extension-bold'\nimport type { CodeOptions } from '@tiptap/extension-code'\nimport type { CodeBlockOptions } from '@tiptap/extension-code-block'\nimport type { DropcursorOptions } from '@tiptap/extension-dropcursor'\nimport type { HardBreakOptions } from '@tiptap/extension-hard-break'\nimport type { HistoryOptions } from '@tiptap/extension-history'\nimport type { HorizontalRuleOptions } from '@tiptap/extension-horizontal-rule'\nimport type { ItalicOptions } from '@tiptap/extension-italic'\nimport type { ListItemOptions } from '@tiptap/extension-list-item'\nimport type { ListKeymapOptions } from '@tiptap/extension-list-keymap'\nimport type { ParagraphOptions } from '@tiptap/extension-paragraph'\nimport type { RichTextBulletListOptions } from './rich-text-bullet-list'\nimport type { RichTextDocumentOptions } from './rich-text-document'\nimport type { RichTextImageOptions } from './rich-text-image'\nimport type { RichTextLinkOptions } from './rich-text-link'\nimport type { RichTextOrderedListOptions } from './rich-text-ordered-list'\nimport type { RichTextStrikethroughOptions } from './rich-text-strikethrough'\n\n/**\n * The options available to customize the `RichTextKit` extension.\n */\ntype RichTextKitOptions = {\n /**\n * Set options for the `Blockquote` extension, or `false` to disable.\n */\n blockquote: Partial<BlockquoteOptions> | false\n\n /**\n * Set options for the `Bold` extension, or `false` to disable.\n */\n bold: Partial<BoldOptions> | false\n\n /**\n * Set options for the `BulletList` extension, or `false` to disable.\n */\n bulletList: Partial<RichTextBulletListOptions> | false\n\n /**\n * Set options for the `Code` extension, or `false` to disable.\n */\n code: Partial<CodeOptions> | false\n\n /**\n * Set options for the `CodeBlock` extension, or `false` to disable.\n */\n codeBlock: Partial<CodeBlockOptions> | false\n\n /**\n * Set options for the `Document` extension, or `false` to disable.\n */\n document: Partial<RichTextDocumentOptions> | false\n\n /**\n * Set options for the `Dropcursor` extension, or `false` to disable.\n */\n dropCursor: Partial<DropcursorOptions> | false\n\n /**\n * Set to `false` to disable the `Gapcursor` extension.\n */\n gapCursor: false\n\n /**\n * Set options for the `HardBreak` extension, or `false` to disable.\n */\n hardBreak: Partial<HardBreakOptions> | false\n\n /**\n * Set options for the `Heading` extension, or `false` to disable.\n */\n heading: Partial<RichTextHeadingOptions> | false\n\n /**\n * Set options for the `History` extension, or `false` to disable.\n */\n history: Partial<HistoryOptions> | false\n\n /**\n * Set options for the `HorizontalRule` extension, or `false` to disable.\n */\n horizontalRule: Partial<HorizontalRuleOptions> | false\n\n /**\n * Set options for the `Image` extension, or `false` to disable.\n */\n image: Partial<RichTextImageOptions> | false\n\n /**\n * Set options for the `Italic` extension, or `false` to disable.\n */\n italic: Partial<ItalicOptions> | false\n\n /**\n * Set options for the `Link` extension, or `false` to disable.\n */\n link: Partial<RichTextLinkOptions> | false\n\n /**\n * Set options for the `ListItem` extension, or `false` to disable.\n */\n listItem: Partial<ListItemOptions> | false\n\n /**\n * Set options for the `ListKeymap` extension, or `false` to disable.\n */\n listKeymap: Partial<ListKeymapOptions> | false\n\n /**\n * Set options for the `OrderedList` extension, or `false` to disable.\n */\n orderedList: Partial<RichTextOrderedListOptions> | false\n\n /**\n * Set options for the `Paragraph` extension, or `false` to disable.\n */\n paragraph: Partial<ParagraphOptions> | false\n\n /**\n * Set to `false` to disable the `PasteEmojis` extension.\n */\n pasteEmojis: false\n\n /**\n * Set to `false` to disable the `PasteMarkdown` extension.\n */\n pasteMarkdown: false\n\n /**\n * Set to `false` to disable the `PasteSinglelineText` extension.\n */\n pasteSinglelineText: false\n\n /**\n * Set to `false` to disable the `PasteHTMLTableAsString` extension.\n */\n pasteHTMLTableAsString: false\n\n /**\n * Set options for the `Strike` extension, or `false` to disable.\n */\n strike: Partial<RichTextStrikethroughOptions> | false\n\n /**\n * Set to `false` to disable the `Text` extension.\n */\n text: false\n\n /**\n * Set to `false` to disable the `Typography` extension.\n */\n typography: false\n}\n\n/**\n * The `RichTextKit` extension is a collection of the minimal required extensions to have a full\n * WYSIWYG text editor working. This extension is based on the official `StarterKit` extension\n * implementation, allowing almost every extension to be customized or disabled.\n */\nconst RichTextKit = Extension.create<RichTextKitOptions>({\n name: 'richTextKit',\n addExtensions() {\n const extensions: Extensions = []\n\n if (this.options.blockquote !== false) {\n extensions.push(\n Blockquote.extend({\n priority: BLOCKQUOTE_EXTENSION_PRIORITY,\n }).configure(this.options?.blockquote),\n )\n }\n\n if (this.options.bold !== false) {\n extensions.push(Bold.configure(this.options?.bold))\n }\n\n if (this.options.bulletList !== false) {\n extensions.push(RichTextBulletList.configure(this.options?.bulletList))\n }\n\n if (this.options.code !== false) {\n extensions.push(\n RichTextCode.configure(this.options?.code),\n\n // Enhances the Code extension capabilities with additional features\n CurvenoteCodemark,\n )\n }\n\n if (this.options.codeBlock !== false) {\n extensions.push(CodeBlock.configure(this.options?.codeBlock))\n }\n\n if (this.options.document !== false) {\n extensions.push(\n RichTextDocument.configure(this.options?.document),\n\n // Supports copying the underlying Markdown source to the clipboard\n CopyMarkdownSource.configure({\n keyboardShortcut: 'Mod-Shift-c',\n }),\n )\n\n if (this.options?.pasteEmojis !== false) {\n // Supports pasting HTML image emojis as unicode characters\n extensions.push(PasteEmojis)\n }\n\n if (this.options?.pasteMarkdown !== false) {\n // Supports pasting Markdown content as HTML into the editor\n extensions.push(PasteMarkdown)\n }\n\n if (\n this.options?.document?.multiline === false &&\n this.options?.pasteSinglelineText !== false\n ) {\n // Supports pasting multiple lines into a singleline editor, by joining all the\n // pasted lines together\n extensions.push(PasteSinglelineText)\n }\n\n if (this.options?.pasteHTMLTableAsString !== false) {\n // Supports pasting tables (from spreadsheets and websites) into the editor\n extensions.push(PasteHTMLTableAsString)\n }\n }\n\n if (this.options.dropCursor !== false) {\n extensions.push(Dropcursor.configure(this.options?.dropCursor))\n }\n\n if (this.options.gapCursor !== false) {\n extensions.push(Gapcursor)\n }\n\n if (this.options.hardBreak !== false) {\n extensions.push(HardBreak.configure(this.options?.hardBreak))\n }\n\n if (this.options.heading !== false) {\n extensions.push(RichTextHeading.configure(this.options?.heading))\n }\n\n if (this.options.history !== false) {\n extensions.push(History.configure(this.options?.history))\n }\n\n if (this.options.horizontalRule !== false) {\n extensions.push(HorizontalRule.configure(this.options?.horizontalRule))\n }\n\n if (this.options.image !== false) {\n extensions.push(RichTextImage.configure(this.options?.image))\n }\n\n if (this.options.italic !== false) {\n extensions.push(Italic.configure(this.options?.italic))\n }\n\n if (this.options.bold !== false && this.options.italic !== false) {\n extensions.push(BoldAndItalics)\n }\n\n if (this.options.link !== false) {\n extensions.push(RichTextLink.configure(this.options?.link))\n }\n\n if (this.options.listItem !== false) {\n extensions.push(ListItem.configure(this.options?.listItem))\n }\n\n if (this.options.listKeymap !== false) {\n extensions.push(ListKeymap.configure(this.options?.listKeymap))\n }\n\n if (this.options.orderedList !== false) {\n extensions.push(RichTextOrderedList.configure(this.options?.orderedList))\n }\n\n if (this.options.paragraph !== false) {\n extensions.push(Paragraph.configure(this.options?.paragraph))\n }\n\n if (this.options.strike !== false) {\n extensions.push(RichTextStrikethrough.configure(this.options?.strike))\n }\n\n if (this.options.text !== false) {\n extensions.push(Text)\n }\n\n if (this.options.typography !== false) {\n extensions.push(Typography)\n }\n\n return extensions\n },\n})\n\nexport { RichTextKit }\n\nexport type { RichTextKitOptions }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkMA,MAAM,cAAc,UAAU,OAA2B;CACrD,MAAM;CACN,gBAAgB;EACZ,MAAM,aAAyB,CAAC;EAEhC,IAAI,KAAK,QAAQ,eAAe,OAC5B,WAAW,KACP,WAAW,OAAO,EACd,UAAA,IACJ,CAAC,CAAC,CAAC,UAAU,KAAK,SAAS,UAAU,CACzC;EAGJ,IAAI,KAAK,QAAQ,SAAS,OACtB,WAAW,KAAK,KAAK,UAAU,KAAK,SAAS,IAAI,CAAC;EAGtD,IAAI,KAAK,QAAQ,eAAe,OAC5B,WAAW,KAAK,mBAAmB,UAAU,KAAK,SAAS,UAAU,CAAC;EAG1E,IAAI,KAAK,QAAQ,SAAS,OACtB,WAAW,KACP,aAAa,UAAU,KAAK,SAAS,IAAI,GAGzC,iBACJ;EAGJ,IAAI,KAAK,QAAQ,cAAc,OAC3B,WAAW,KAAK,UAAU,UAAU,KAAK,SAAS,SAAS,CAAC;EAGhE,IAAI,KAAK,QAAQ,aAAa,OAAO;GACjC,WAAW,KACP,iBAAiB,UAAU,KAAK,SAAS,QAAQ,GAGjD,mBAAmB,UAAU,EACzB,kBAAkB,cACtB,CAAC,CACL;GAEA,IAAI,KAAK,SAAS,gBAAgB,OAE9B,WAAW,KAAK,WAAW;GAG/B,IAAI,KAAK,SAAS,kBAAkB,OAEhC,WAAW,KAAK,aAAa;GAGjC,IACI,KAAK,SAAS,UAAU,cAAc,SACtC,KAAK,SAAS,wBAAwB,OAItC,WAAW,KAAK,mBAAmB;GAGvC,IAAI,KAAK,SAAS,2BAA2B,OAEzC,WAAW,KAAK,sBAAsB;EAE9C;EAEA,IAAI,KAAK,QAAQ,eAAe,OAC5B,WAAW,KAAK,WAAW,UAAU,KAAK,SAAS,UAAU,CAAC;EAGlE,IAAI,KAAK,QAAQ,cAAc,OAC3B,WAAW,KAAK,SAAS;EAG7B,IAAI,KAAK,QAAQ,cAAc,OAC3B,WAAW,KAAK,UAAU,UAAU,KAAK,SAAS,SAAS,CAAC;EAGhE,IAAI,KAAK,QAAQ,YAAY,OACzB,WAAW,KAAK,gBAAgB,UAAU,KAAK,SAAS,OAAO,CAAC;EAGpE,IAAI,KAAK,QAAQ,YAAY,OACzB,WAAW,KAAK,QAAQ,UAAU,KAAK,SAAS,OAAO,CAAC;EAG5D,IAAI,KAAK,QAAQ,mBAAmB,OAChC,WAAW,KAAK,eAAe,UAAU,KAAK,SAAS,cAAc,CAAC;EAG1E,IAAI,KAAK,QAAQ,UAAU,OACvB,WAAW,KAAK,cAAc,UAAU,KAAK,SAAS,KAAK,CAAC;EAGhE,IAAI,KAAK,QAAQ,WAAW,OACxB,WAAW,KAAK,OAAO,UAAU,KAAK,SAAS,MAAM,CAAC;EAG1D,IAAI,KAAK,QAAQ,SAAS,SAAS,KAAK,QAAQ,WAAW,OACvD,WAAW,KAAK,cAAc;EAGlC,IAAI,KAAK,QAAQ,SAAS,OACtB,WAAW,KAAK,aAAa,UAAU,KAAK,SAAS,IAAI,CAAC;EAG9D,IAAI,KAAK,QAAQ,aAAa,OAC1B,WAAW,KAAK,SAAS,UAAU,KAAK,SAAS,QAAQ,CAAC;EAG9D,IAAI,KAAK,QAAQ,eAAe,OAC5B,WAAW,KAAK,WAAW,UAAU,KAAK,SAAS,UAAU,CAAC;EAGlE,IAAI,KAAK,QAAQ,gBAAgB,OAC7B,WAAW,KAAK,oBAAoB,UAAU,KAAK,SAAS,WAAW,CAAC;EAG5E,IAAI,KAAK,QAAQ,cAAc,OAC3B,WAAW,KAAK,UAAU,UAAU,KAAK,SAAS,SAAS,CAAC;EAGhE,IAAI,KAAK,QAAQ,WAAW,OACxB,WAAW,KAAK,sBAAsB,UAAU,KAAK,SAAS,MAAM,CAAC;EAGzE,IAAI,KAAK,QAAQ,SAAS,OACtB,WAAW,KAAK,IAAI;EAGxB,IAAI,KAAK,QAAQ,eAAe,OAC5B,WAAW,KAAK,UAAU;EAG9B,OAAO;CACX;AACJ,CAAC"}
1
+ {"version":3,"file":"rich-text-kit.js","names":[],"sources":["../../../src/extensions/rich-text/rich-text-kit.ts"],"sourcesContent":["import { Extension } from '@tiptap/core'\nimport { Blockquote } from '@tiptap/extension-blockquote'\nimport { Bold } from '@tiptap/extension-bold'\nimport { CodeBlock } from '@tiptap/extension-code-block'\nimport { Dropcursor } from '@tiptap/extension-dropcursor'\nimport { Gapcursor } from '@tiptap/extension-gapcursor'\nimport { HardBreak } from '@tiptap/extension-hard-break'\nimport { History } from '@tiptap/extension-history'\nimport { HorizontalRule } from '@tiptap/extension-horizontal-rule'\nimport { Italic } from '@tiptap/extension-italic'\nimport { ListItem } from '@tiptap/extension-list-item'\nimport { ListKeymap } from '@tiptap/extension-list-keymap'\nimport { Paragraph } from '@tiptap/extension-paragraph'\nimport { TableCell } from '@tiptap/extension-table-cell'\nimport { TableHeader } from '@tiptap/extension-table-header'\nimport { TableRow } from '@tiptap/extension-table-row'\nimport { Text } from '@tiptap/extension-text'\nimport { Typography } from '@tiptap/extension-typography'\n\nimport { BLOCKQUOTE_EXTENSION_PRIORITY } from '../../constants/extension-priorities'\nimport { CopyMarkdownSource } from '../shared/copy-markdown-source'\nimport { PasteSinglelineText } from '../shared/paste-singleline-text'\n\nimport { BoldAndItalics } from './bold-and-italics'\nimport { CurvenoteCodemark } from './curvenote-codemark'\nimport { PasteEmojis } from './paste-emojis'\nimport { PasteMarkdown } from './paste-markdown'\nimport { RichTextBulletList } from './rich-text-bullet-list'\nimport { RichTextCode } from './rich-text-code'\nimport { RichTextDocument } from './rich-text-document'\nimport { RichTextHeading, RichTextHeadingOptions } from './rich-text-heading'\nimport { RichTextImage } from './rich-text-image'\nimport { RichTextLink } from './rich-text-link'\nimport { RichTextOrderedList } from './rich-text-ordered-list'\nimport { RichTextStrikethrough } from './rich-text-strikethrough'\nimport { RichTextTable } from './rich-text-table'\n\nimport type { Extensions } from '@tiptap/core'\nimport type { BlockquoteOptions } from '@tiptap/extension-blockquote'\nimport type { BoldOptions } from '@tiptap/extension-bold'\nimport type { CodeOptions } from '@tiptap/extension-code'\nimport type { CodeBlockOptions } from '@tiptap/extension-code-block'\nimport type { DropcursorOptions } from '@tiptap/extension-dropcursor'\nimport type { HardBreakOptions } from '@tiptap/extension-hard-break'\nimport type { HistoryOptions } from '@tiptap/extension-history'\nimport type { HorizontalRuleOptions } from '@tiptap/extension-horizontal-rule'\nimport type { ItalicOptions } from '@tiptap/extension-italic'\nimport type { ListItemOptions } from '@tiptap/extension-list-item'\nimport type { ListKeymapOptions } from '@tiptap/extension-list-keymap'\nimport type { ParagraphOptions } from '@tiptap/extension-paragraph'\nimport type { RichTextBulletListOptions } from './rich-text-bullet-list'\nimport type { RichTextDocumentOptions } from './rich-text-document'\nimport type { RichTextImageOptions } from './rich-text-image'\nimport type { RichTextLinkOptions } from './rich-text-link'\nimport type { RichTextOrderedListOptions } from './rich-text-ordered-list'\nimport type { RichTextStrikethroughOptions } from './rich-text-strikethrough'\nimport type { RichTextTableOptions } from './rich-text-table'\n\n/**\n * The options available to customize the `RichTextKit` extension.\n */\ntype RichTextKitOptions = {\n /**\n * Set options for the `Blockquote` extension, or `false` to disable.\n */\n blockquote: Partial<BlockquoteOptions> | false\n\n /**\n * Set options for the `Bold` extension, or `false` to disable.\n */\n bold: Partial<BoldOptions> | false\n\n /**\n * Set options for the `BulletList` extension, or `false` to disable.\n */\n bulletList: Partial<RichTextBulletListOptions> | false\n\n /**\n * Set options for the `Code` extension, or `false` to disable.\n */\n code: Partial<CodeOptions> | false\n\n /**\n * Set options for the `CodeBlock` extension, or `false` to disable.\n */\n codeBlock: Partial<CodeBlockOptions> | false\n\n /**\n * Set options for the `Document` extension, or `false` to disable.\n */\n document: Partial<RichTextDocumentOptions> | false\n\n /**\n * Set options for the `Dropcursor` extension, or `false` to disable.\n */\n dropCursor: Partial<DropcursorOptions> | false\n\n /**\n * Set to `false` to disable the `Gapcursor` extension.\n */\n gapCursor: false\n\n /**\n * Set options for the `HardBreak` extension, or `false` to disable.\n */\n hardBreak: Partial<HardBreakOptions> | false\n\n /**\n * Set options for the `Heading` extension, or `false` to disable.\n */\n heading: Partial<RichTextHeadingOptions> | false\n\n /**\n * Set options for the `History` extension, or `false` to disable.\n */\n history: Partial<HistoryOptions> | false\n\n /**\n * Set options for the `HorizontalRule` extension, or `false` to disable.\n */\n horizontalRule: Partial<HorizontalRuleOptions> | false\n\n /**\n * Set options for the `Image` extension, or `false` to disable.\n */\n image: Partial<RichTextImageOptions> | false\n\n /**\n * Set options for the `Italic` extension, or `false` to disable.\n */\n italic: Partial<ItalicOptions> | false\n\n /**\n * Set options for the `Link` extension, or `false` to disable.\n */\n link: Partial<RichTextLinkOptions> | false\n\n /**\n * Set options for the `ListItem` extension, or `false` to disable.\n */\n listItem: Partial<ListItemOptions> | false\n\n /**\n * Set options for the `ListKeymap` extension, or `false` to disable.\n */\n listKeymap: Partial<ListKeymapOptions> | false\n\n /**\n * Set options for the `OrderedList` extension, or `false` to disable.\n */\n orderedList: Partial<RichTextOrderedListOptions> | false\n\n /**\n * Set options for the `Paragraph` extension, or `false` to disable.\n */\n paragraph: Partial<ParagraphOptions> | false\n\n /**\n * Set to `false` to disable the `PasteEmojis` extension.\n */\n pasteEmojis: false\n\n /**\n * Set to `false` to disable the `PasteMarkdown` extension.\n */\n pasteMarkdown: false\n\n /**\n * Set to `false` to disable the `PasteSinglelineText` extension.\n */\n pasteSinglelineText: false\n\n /**\n * Set options for the `Strike` extension, or `false` to disable.\n */\n strike: Partial<RichTextStrikethroughOptions> | false\n\n /**\n * Set options for the `Table` extension, or `false` to disable.\n *\n * Note that table cells are restricted to a single paragraph of content, since the GFM table\n * syntax cannot represent multiple blocks within a cell (which also means that tables require\n * a `paragraph` node in the editor schema). Additionally, tables are always disabled in\n * singleline documents, since they serialize to multiple lines of Markdown.\n */\n table: Partial<RichTextTableOptions> | false\n\n /**\n * Set to `false` to disable the `Text` extension.\n */\n text: false\n\n /**\n * Set to `false` to disable the `Typography` extension.\n */\n typography: false\n}\n\n/**\n * The `RichTextKit` extension is a collection of the minimal required extensions to have a full\n * WYSIWYG text editor working. This extension is based on the official `StarterKit` extension\n * implementation, allowing almost every extension to be customized or disabled.\n */\nconst RichTextKit = Extension.create<RichTextKitOptions>({\n name: 'richTextKit',\n addExtensions() {\n const extensions: Extensions = []\n\n if (this.options.blockquote !== false) {\n extensions.push(\n Blockquote.extend({\n priority: BLOCKQUOTE_EXTENSION_PRIORITY,\n }).configure(this.options?.blockquote),\n )\n }\n\n if (this.options.bold !== false) {\n extensions.push(Bold.configure(this.options?.bold))\n }\n\n if (this.options.bulletList !== false) {\n extensions.push(RichTextBulletList.configure(this.options?.bulletList))\n }\n\n if (this.options.code !== false) {\n extensions.push(\n RichTextCode.configure(this.options?.code),\n\n // Enhances the Code extension capabilities with additional features\n CurvenoteCodemark,\n )\n }\n\n if (this.options.codeBlock !== false) {\n extensions.push(CodeBlock.configure(this.options?.codeBlock))\n }\n\n if (this.options.document !== false) {\n extensions.push(\n RichTextDocument.configure(this.options?.document),\n\n // Supports copying the underlying Markdown source to the clipboard\n CopyMarkdownSource.configure({\n keyboardShortcut: 'Mod-Shift-c',\n }),\n )\n\n if (this.options?.pasteEmojis !== false) {\n // Supports pasting HTML image emojis as unicode characters\n extensions.push(PasteEmojis)\n }\n\n if (this.options?.pasteMarkdown !== false) {\n // Supports pasting Markdown content as HTML into the editor\n extensions.push(PasteMarkdown)\n }\n\n if (\n this.options?.document?.multiline === false &&\n this.options?.pasteSinglelineText !== false\n ) {\n // Supports pasting multiple lines into a singleline editor, by joining all the\n // pasted lines together\n extensions.push(PasteSinglelineText)\n }\n }\n\n if (this.options.dropCursor !== false) {\n extensions.push(Dropcursor.configure(this.options?.dropCursor))\n }\n\n if (this.options.gapCursor !== false) {\n extensions.push(Gapcursor)\n }\n\n if (this.options.hardBreak !== false) {\n extensions.push(HardBreak.configure(this.options?.hardBreak))\n }\n\n if (this.options.heading !== false) {\n extensions.push(RichTextHeading.configure(this.options?.heading))\n }\n\n if (this.options.history !== false) {\n extensions.push(History.configure(this.options?.history))\n }\n\n if (this.options.horizontalRule !== false) {\n extensions.push(HorizontalRule.configure(this.options?.horizontalRule))\n }\n\n if (this.options.image !== false) {\n extensions.push(RichTextImage.configure(this.options?.image))\n }\n\n if (this.options.italic !== false) {\n extensions.push(Italic.configure(this.options?.italic))\n }\n\n if (this.options.bold !== false && this.options.italic !== false) {\n extensions.push(BoldAndItalics)\n }\n\n if (this.options.link !== false) {\n extensions.push(RichTextLink.configure(this.options?.link))\n }\n\n if (this.options.listItem !== false) {\n extensions.push(ListItem.configure(this.options?.listItem))\n }\n\n if (this.options.listKeymap !== false) {\n extensions.push(ListKeymap.configure(this.options?.listKeymap))\n }\n\n if (this.options.orderedList !== false) {\n extensions.push(RichTextOrderedList.configure(this.options?.orderedList))\n }\n\n if (this.options.paragraph !== false) {\n extensions.push(Paragraph.configure(this.options?.paragraph))\n }\n\n if (this.options.strike !== false) {\n extensions.push(RichTextStrikethrough.configure(this.options?.strike))\n }\n\n // Tables are not supported in singleline documents because, although a table is a single\n // block node (which would technically fit the singleline schema), it serializes to\n // multiple lines of Markdown, breaking the singleline contract\n const isSinglelineDocument =\n this.options.document !== false && this.options.document?.multiline === false\n\n if (this.options.table !== false && !isSinglelineDocument) {\n extensions.push(\n RichTextTable.configure(this.options?.table),\n TableRow,\n\n // Restrict cells to a single paragraph (instead of one or more blocks), since the\n // GFM table syntax cannot represent multiple blocks within a cell, ensuring the\n // editor cannot produce tables that lose content when serialized to Markdown\n TableHeader.extend({ content: 'paragraph' }),\n TableCell.extend({ content: 'paragraph' }),\n )\n }\n\n if (this.options.text !== false) {\n extensions.push(Text)\n }\n\n if (this.options.typography !== false) {\n extensions.push(Typography)\n }\n\n return extensions\n },\n})\n\nexport { RichTextKit }\n\nexport type { RichTextKitOptions }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2MA,MAAM,cAAc,UAAU,OAA2B;CACrD,MAAM;CACN,gBAAgB;EACZ,MAAM,aAAyB,CAAC;EAEhC,IAAI,KAAK,QAAQ,eAAe,OAC5B,WAAW,KACP,WAAW,OAAO,EACd,UAAA,IACJ,CAAC,CAAC,CAAC,UAAU,KAAK,SAAS,UAAU,CACzC;EAGJ,IAAI,KAAK,QAAQ,SAAS,OACtB,WAAW,KAAK,KAAK,UAAU,KAAK,SAAS,IAAI,CAAC;EAGtD,IAAI,KAAK,QAAQ,eAAe,OAC5B,WAAW,KAAK,mBAAmB,UAAU,KAAK,SAAS,UAAU,CAAC;EAG1E,IAAI,KAAK,QAAQ,SAAS,OACtB,WAAW,KACP,aAAa,UAAU,KAAK,SAAS,IAAI,GAGzC,iBACJ;EAGJ,IAAI,KAAK,QAAQ,cAAc,OAC3B,WAAW,KAAK,UAAU,UAAU,KAAK,SAAS,SAAS,CAAC;EAGhE,IAAI,KAAK,QAAQ,aAAa,OAAO;GACjC,WAAW,KACP,iBAAiB,UAAU,KAAK,SAAS,QAAQ,GAGjD,mBAAmB,UAAU,EACzB,kBAAkB,cACtB,CAAC,CACL;GAEA,IAAI,KAAK,SAAS,gBAAgB,OAE9B,WAAW,KAAK,WAAW;GAG/B,IAAI,KAAK,SAAS,kBAAkB,OAEhC,WAAW,KAAK,aAAa;GAGjC,IACI,KAAK,SAAS,UAAU,cAAc,SACtC,KAAK,SAAS,wBAAwB,OAItC,WAAW,KAAK,mBAAmB;EAE3C;EAEA,IAAI,KAAK,QAAQ,eAAe,OAC5B,WAAW,KAAK,WAAW,UAAU,KAAK,SAAS,UAAU,CAAC;EAGlE,IAAI,KAAK,QAAQ,cAAc,OAC3B,WAAW,KAAK,SAAS;EAG7B,IAAI,KAAK,QAAQ,cAAc,OAC3B,WAAW,KAAK,UAAU,UAAU,KAAK,SAAS,SAAS,CAAC;EAGhE,IAAI,KAAK,QAAQ,YAAY,OACzB,WAAW,KAAK,gBAAgB,UAAU,KAAK,SAAS,OAAO,CAAC;EAGpE,IAAI,KAAK,QAAQ,YAAY,OACzB,WAAW,KAAK,QAAQ,UAAU,KAAK,SAAS,OAAO,CAAC;EAG5D,IAAI,KAAK,QAAQ,mBAAmB,OAChC,WAAW,KAAK,eAAe,UAAU,KAAK,SAAS,cAAc,CAAC;EAG1E,IAAI,KAAK,QAAQ,UAAU,OACvB,WAAW,KAAK,cAAc,UAAU,KAAK,SAAS,KAAK,CAAC;EAGhE,IAAI,KAAK,QAAQ,WAAW,OACxB,WAAW,KAAK,OAAO,UAAU,KAAK,SAAS,MAAM,CAAC;EAG1D,IAAI,KAAK,QAAQ,SAAS,SAAS,KAAK,QAAQ,WAAW,OACvD,WAAW,KAAK,cAAc;EAGlC,IAAI,KAAK,QAAQ,SAAS,OACtB,WAAW,KAAK,aAAa,UAAU,KAAK,SAAS,IAAI,CAAC;EAG9D,IAAI,KAAK,QAAQ,aAAa,OAC1B,WAAW,KAAK,SAAS,UAAU,KAAK,SAAS,QAAQ,CAAC;EAG9D,IAAI,KAAK,QAAQ,eAAe,OAC5B,WAAW,KAAK,WAAW,UAAU,KAAK,SAAS,UAAU,CAAC;EAGlE,IAAI,KAAK,QAAQ,gBAAgB,OAC7B,WAAW,KAAK,oBAAoB,UAAU,KAAK,SAAS,WAAW,CAAC;EAG5E,IAAI,KAAK,QAAQ,cAAc,OAC3B,WAAW,KAAK,UAAU,UAAU,KAAK,SAAS,SAAS,CAAC;EAGhE,IAAI,KAAK,QAAQ,WAAW,OACxB,WAAW,KAAK,sBAAsB,UAAU,KAAK,SAAS,MAAM,CAAC;EAMzE,MAAM,uBACF,KAAK,QAAQ,aAAa,SAAS,KAAK,QAAQ,UAAU,cAAc;EAE5E,IAAI,KAAK,QAAQ,UAAU,SAAS,CAAC,sBACjC,WAAW,KACP,cAAc,UAAU,KAAK,SAAS,KAAK,GAC3C,UAKA,YAAY,OAAO,EAAE,SAAS,YAAY,CAAC,GAC3C,UAAU,OAAO,EAAE,SAAS,YAAY,CAAC,CAC7C;EAGJ,IAAI,KAAK,QAAQ,SAAS,OACtB,WAAW,KAAK,IAAI;EAGxB,IAAI,KAAK,QAAQ,eAAe,OAC5B,WAAW,KAAK,UAAU;EAG9B,OAAO;CACX;AACJ,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { TableOptions } from "@tiptap/extension-table";
2
+
3
+ //#region src/extensions/rich-text/rich-text-table.d.ts
4
+ /**
5
+ * The options available to customize the `Table` extension, omitting all resizing-related options
6
+ * since column widths cannot be represented in the GFM table syntax, and would otherwise be
7
+ * silently lost when the editor content is serialized to Markdown.
8
+ */
9
+ type RichTextTableOptions = Omit<TableOptions, 'resizable' | 'renderWrapper' | 'handleWidth' | 'cellMinWidth' | 'View' | 'lastColumnResizable'>;
10
+ /**
11
+ * Custom extension that extends the built-in `Table` extension with an input rule to insert a
12
+ * table by typing two or more consecutive pipe characters (i.e., the Markdown table column
13
+ * delimiter) followed by a space. The number of typed delimiters defines the number of columns
14
+ * (e.g., `||| ` inserts a table with two columns), and the table is inserted with a heading row
15
+ * and a single body row. New rows can then be created by pressing the `Tab` key at the last cell
16
+ * (a built-in capability of the `Table` extension).
17
+ *
18
+ * Additionally, this extension makes sure that tables pasted without a heading row (e.g., from
19
+ * spreadsheet apps or as heading-less Markdown tables) get an empty one, since the GFM table
20
+ * syntax requires it.
21
+ */
22
+ //#endregion
23
+ export type { RichTextTableOptions };
24
+ //# sourceMappingURL=rich-text-table.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rich-text-table.d.ts","names":[],"sources":["../../../src/extensions/rich-text/rich-text-table.ts"],"mappings":";;;;;AAM2D;;;KAoBtD,oBAAA,GAAuB,IAAI,CAC5B,YAAA;AAAY"}
@@ -0,0 +1,97 @@
1
+ import { parseHtmlToElement } from "../../helpers/dom.js";
2
+ import { InputRule } from "@tiptap/core";
3
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
4
+ import { Table } from "@tiptap/extension-table";
5
+ //#region src/extensions/rich-text/rich-text-table.ts
6
+ /**
7
+ * The regular expression to match a line that looks like a Markdown table row (i.e. a line
8
+ * starting and ending with a pipe character).
9
+ */
10
+ const TABLE_ROW_REGEX = /^\s*\|.*\|\s*$/;
11
+ /**
12
+ * The regular expression to match a Markdown table delimiter row (i.e. `| --- | :-- |`, which
13
+ * separates the heading row from the table body).
14
+ */
15
+ const TABLE_DELIMITER_ROW_REGEX = /^\s*\|(?:\s*:?-+:?\s*\|)+\s*$/;
16
+ /**
17
+ * Transforms pasted HTML by adding an empty heading row to tables that don't have one (e.g.,
18
+ * tables copied from spreadsheet apps are composed of data cells only). The GFM table syntax
19
+ * requires tables to have a heading row, so adding one at paste time keeps what is seen in the
20
+ * editor consistent with the serialized Markdown (which would otherwise gain the empty heading
21
+ * row when the content is reloaded).
22
+ */
23
+ function transformPastedHTML(html) {
24
+ const body = parseHtmlToElement(html);
25
+ const tables = body.querySelectorAll("table");
26
+ if (tables.length === 0) return html;
27
+ for (const table of Array.from(tables)) {
28
+ const firstRow = table.rows[0];
29
+ if (!firstRow || firstRow.cells.length === 0) continue;
30
+ if (Array.from(firstRow.cells).every((cell) => cell.tagName === "TH")) continue;
31
+ const headingRow = document.createElement("tr");
32
+ for (let index = 0; index < firstRow.cells.length; index++) headingRow.appendChild(document.createElement("th"));
33
+ firstRow.before(headingRow);
34
+ }
35
+ return body.innerHTML;
36
+ }
37
+ /**
38
+ * Transforms pasted plain text by adding an empty heading row (followed by the required delimiter
39
+ * row) to Markdown tables that don't have one. The GFM table syntax requires a heading row to
40
+ * parse as a table at all, so without this transform, pasted heading-less Markdown tables would
41
+ * be inserted as plain text paragraphs.
42
+ */
43
+ function transformPastedText(text, _plain, view) {
44
+ if (view.state.selection.$from.parent.type.spec.code) return text;
45
+ const lines = text.split("\n");
46
+ const output = [];
47
+ for (let index = 0; index < lines.length; index++) {
48
+ const line = lines[index];
49
+ const isFirstRowOfTable = !TABLE_ROW_REGEX.test(lines[index - 1] || "");
50
+ if (TABLE_ROW_REGEX.test(line) && isFirstRowOfTable && !TABLE_DELIMITER_ROW_REGEX.test(lines[index + 1] || "")) {
51
+ const columnsCount = line.trim().split(/(?<!\\)\|/).length - 2;
52
+ output.push(`|${" |".repeat(columnsCount)}`, `|${" --- |".repeat(columnsCount)}`);
53
+ }
54
+ output.push(line);
55
+ }
56
+ return output.join("\n");
57
+ }
58
+ /**
59
+ * Custom extension that extends the built-in `Table` extension with an input rule to insert a
60
+ * table by typing two or more consecutive pipe characters (i.e., the Markdown table column
61
+ * delimiter) followed by a space. The number of typed delimiters defines the number of columns
62
+ * (e.g., `||| ` inserts a table with two columns), and the table is inserted with a heading row
63
+ * and a single body row. New rows can then be created by pressing the `Tab` key at the last cell
64
+ * (a built-in capability of the `Table` extension).
65
+ *
66
+ * Additionally, this extension makes sure that tables pasted without a heading row (e.g., from
67
+ * spreadsheet apps or as heading-less Markdown tables) get an empty one, since the GFM table
68
+ * syntax requires it.
69
+ */
70
+ const RichTextTable = Table.extend({
71
+ addInputRules() {
72
+ return [new InputRule({
73
+ find: /^(\|{2,})\s$/,
74
+ handler: ({ range, match, chain }) => {
75
+ if (this.editor.isActive("table")) return null;
76
+ chain().deleteRange(range).insertTable({
77
+ rows: 2,
78
+ cols: match[1].length - 1,
79
+ withHeaderRow: true
80
+ }).run();
81
+ }
82
+ })];
83
+ },
84
+ addProseMirrorPlugins() {
85
+ return [...this.parent?.() || [], new Plugin({
86
+ key: new PluginKey("pasteTableHeadingRow"),
87
+ props: {
88
+ transformPastedHTML,
89
+ transformPastedText
90
+ }
91
+ })];
92
+ }
93
+ });
94
+ //#endregion
95
+ export { RichTextTable };
96
+
97
+ //# sourceMappingURL=rich-text-table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rich-text-table.js","names":[],"sources":["../../../src/extensions/rich-text/rich-text-table.ts"],"sourcesContent":["import { InputRule } from '@tiptap/core'\nimport { Table } from '@tiptap/extension-table'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nimport { parseHtmlToElement } from '../../helpers/dom'\n\nimport type { TableOptions } from '@tiptap/extension-table'\nimport type { EditorView } from '@tiptap/pm/view'\n\n/**\n * The regular expression to match a line that looks like a Markdown table row (i.e. a line\n * starting and ending with a pipe character).\n */\nconst TABLE_ROW_REGEX = /^\\s*\\|.*\\|\\s*$/\n\n/**\n * The regular expression to match a Markdown table delimiter row (i.e. `| --- | :-- |`, which\n * separates the heading row from the table body).\n */\nconst TABLE_DELIMITER_ROW_REGEX = /^\\s*\\|(?:\\s*:?-+:?\\s*\\|)+\\s*$/\n\n/**\n * The options available to customize the `Table` extension, omitting all resizing-related options\n * since column widths cannot be represented in the GFM table syntax, and would otherwise be\n * silently lost when the editor content is serialized to Markdown.\n */\ntype RichTextTableOptions = Omit<\n TableOptions,\n 'resizable' | 'renderWrapper' | 'handleWidth' | 'cellMinWidth' | 'View' | 'lastColumnResizable'\n>\n\n/**\n * Transforms pasted HTML by adding an empty heading row to tables that don't have one (e.g.,\n * tables copied from spreadsheet apps are composed of data cells only). The GFM table syntax\n * requires tables to have a heading row, so adding one at paste time keeps what is seen in the\n * editor consistent with the serialized Markdown (which would otherwise gain the empty heading\n * row when the content is reloaded).\n */\nfunction transformPastedHTML(html: string): string {\n const body = parseHtmlToElement(html)\n const tables = body.querySelectorAll('table')\n\n if (tables.length === 0) {\n return html\n }\n\n for (const table of Array.from(tables)) {\n const firstRow = table.rows[0]\n\n if (!firstRow || firstRow.cells.length === 0) {\n continue\n }\n\n const isHeadingRow = Array.from(firstRow.cells).every((cell) => cell.tagName === 'TH')\n\n if (isHeadingRow) {\n continue\n }\n\n const headingRow = document.createElement('tr')\n\n for (let index = 0; index < firstRow.cells.length; index++) {\n headingRow.appendChild(document.createElement('th'))\n }\n\n firstRow.before(headingRow)\n }\n\n return body.innerHTML\n}\n\n/**\n * Transforms pasted plain text by adding an empty heading row (followed by the required delimiter\n * row) to Markdown tables that don't have one. The GFM table syntax requires a heading row to\n * parse as a table at all, so without this transform, pasted heading-less Markdown tables would\n * be inserted as plain text paragraphs.\n */\nfunction transformPastedText(text: string, _plain: boolean, view: EditorView): string {\n // Do not transform text pasted into a code block (it must be inserted as-is)\n if (view.state.selection.$from.parent.type.spec.code) {\n return text\n }\n\n const lines = text.split('\\n')\n const output: string[] = []\n\n for (let index = 0; index < lines.length; index++) {\n const line = lines[index]\n const isFirstRowOfTable = !TABLE_ROW_REGEX.test(lines[index - 1] || '')\n\n // Add the empty heading row (followed by the required delimiter row) before the first\n // row of a table that is not followed by a delimiter row (i.e. a heading-less table)\n if (\n TABLE_ROW_REGEX.test(line) &&\n isFirstRowOfTable &&\n !TABLE_DELIMITER_ROW_REGEX.test(lines[index + 1] || '')\n ) {\n const columnsCount = line.trim().split(/(?<!\\\\)\\|/).length - 2\n\n output.push(`|${' |'.repeat(columnsCount)}`, `|${' --- |'.repeat(columnsCount)}`)\n }\n\n output.push(line)\n }\n\n return output.join('\\n')\n}\n\n/**\n * Custom extension that extends the built-in `Table` extension with an input rule to insert a\n * table by typing two or more consecutive pipe characters (i.e., the Markdown table column\n * delimiter) followed by a space. The number of typed delimiters defines the number of columns\n * (e.g., `||| ` inserts a table with two columns), and the table is inserted with a heading row\n * and a single body row. New rows can then be created by pressing the `Tab` key at the last cell\n * (a built-in capability of the `Table` extension).\n *\n * Additionally, this extension makes sure that tables pasted without a heading row (e.g., from\n * spreadsheet apps or as heading-less Markdown tables) get an empty one, since the GFM table\n * syntax requires it.\n */\nconst RichTextTable = Table.extend({\n addInputRules() {\n return [\n new InputRule({\n find: /^(\\|{2,})\\s$/,\n handler: ({ range, match, chain }) => {\n // Tables cannot be nested, so do not apply the input rule within a table\n if (this.editor.isActive('table')) {\n return null\n }\n\n chain()\n .deleteRange(range)\n .insertTable({\n rows: 2,\n cols: match[1].length - 1,\n withHeaderRow: true,\n })\n .run()\n },\n }),\n ]\n },\n addProseMirrorPlugins() {\n return [\n // The built-in plugins (e.g., for cell selection handling) must be preserved\n ...(this.parent?.() || []),\n\n new Plugin({\n key: new PluginKey('pasteTableHeadingRow'),\n props: {\n transformPastedHTML,\n transformPastedText,\n },\n }),\n ]\n },\n})\n\nexport { RichTextTable }\n\nexport type { RichTextTableOptions }\n"],"mappings":";;;;;;;;;AAaA,MAAM,kBAAkB;;;;;AAMxB,MAAM,4BAA4B;;;;;;;;AAmBlC,SAAS,oBAAoB,MAAsB;CAC/C,MAAM,OAAO,mBAAmB,IAAI;CACpC,MAAM,SAAS,KAAK,iBAAiB,OAAO;CAE5C,IAAI,OAAO,WAAW,GAClB,OAAO;CAGX,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,GAAG;EACpC,MAAM,WAAW,MAAM,KAAK;EAE5B,IAAI,CAAC,YAAY,SAAS,MAAM,WAAW,GACvC;EAKJ,IAFqB,MAAM,KAAK,SAAS,KAAK,CAAC,CAAC,OAAO,SAAS,KAAK,YAAY,IAElE,GACX;EAGJ,MAAM,aAAa,SAAS,cAAc,IAAI;EAE9C,KAAK,IAAI,QAAQ,GAAG,QAAQ,SAAS,MAAM,QAAQ,SAC/C,WAAW,YAAY,SAAS,cAAc,IAAI,CAAC;EAGvD,SAAS,OAAO,UAAU;CAC9B;CAEA,OAAO,KAAK;AAChB;;;;;;;AAQA,SAAS,oBAAoB,MAAc,QAAiB,MAA0B;CAElF,IAAI,KAAK,MAAM,UAAU,MAAM,OAAO,KAAK,KAAK,MAC5C,OAAO;CAGX,MAAM,QAAQ,KAAK,MAAM,IAAI;CAC7B,MAAM,SAAmB,CAAC;CAE1B,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS;EAC/C,MAAM,OAAO,MAAM;EACnB,MAAM,oBAAoB,CAAC,gBAAgB,KAAK,MAAM,QAAQ,MAAM,EAAE;EAItE,IACI,gBAAgB,KAAK,IAAI,KACzB,qBACA,CAAC,0BAA0B,KAAK,MAAM,QAAQ,MAAM,EAAE,GACxD;GACE,MAAM,eAAe,KAAK,KAAK,CAAC,CAAC,MAAM,WAAW,CAAC,CAAC,SAAS;GAE7D,OAAO,KAAK,IAAI,MAAM,OAAO,YAAY,KAAK,IAAI,SAAS,OAAO,YAAY,GAAG;EACrF;EAEA,OAAO,KAAK,IAAI;CACpB;CAEA,OAAO,OAAO,KAAK,IAAI;AAC3B;;;;;;;;;;;;;AAcA,MAAM,gBAAgB,MAAM,OAAO;CAC/B,gBAAgB;EACZ,OAAO,CACH,IAAI,UAAU;GACV,MAAM;GACN,UAAU,EAAE,OAAO,OAAO,YAAY;IAElC,IAAI,KAAK,OAAO,SAAS,OAAO,GAC5B,OAAO;IAGX,MAAM,CAAC,CACF,YAAY,KAAK,CAAC,CAClB,YAAY;KACT,MAAM;KACN,MAAM,MAAM,EAAE,CAAC,SAAS;KACxB,eAAe;IACnB,CAAC,CAAC,CACD,IAAI;GACb;EACJ,CAAC,CACL;CACJ;CACA,wBAAwB;EACpB,OAAO,CAEH,GAAI,KAAK,SAAS,KAAK,CAAC,GAExB,IAAI,OAAO;GACP,KAAK,IAAI,UAAU,sBAAsB;GACzC,OAAO;IACH;IACA;GACJ;EACJ,CAAC,CACL;CACJ;AACJ,CAAC"}
@@ -28,11 +28,10 @@ function transformPastedHTML(html) {
28
28
  /**
29
29
  * The `PasteHTMLTableAsString` extension adds the ability to paste a table copied from a spreadsheet
30
30
  * web app (e.g., Google Sheets, Microsoft Excel), along with tables rendered by GitHub Flavored
31
- * Markdown (GFM), into the editor.
31
+ * Markdown (GFM), into a plain-text editor.
32
32
  *
33
- * Since Typist does not yet support tables, this extension simply pastes the table as a string of
34
- * paragraphs (one paragraph per row), with each cell separated by a space character. However,
35
- * whenever we do add support for tables, this extension will need to be completely rewritten.
33
+ * Since plain-text documents cannot represent tables, this extension simply pastes the table as a
34
+ * string of paragraphs (one paragraph per row), with each cell separated by a space character.
36
35
  *
37
36
  * Lastly, please note that formatting is lost when the copied table comes from Google Sheets or
38
37
  * Microsoft Excel, because unfortunately, these apps style the cell contents using CSS.
@@ -1 +1 @@
1
- {"version":3,"file":"paste-html-table-as-string.js","names":[],"sources":["../../../src/extensions/shared/paste-html-table-as-string.ts"],"sourcesContent":["import { Extension } from '@tiptap/core'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nimport { PASTE_HTML_TABLE_AS_STRING_EXTENSION_PRIORITY } from '../../constants/extension-priorities'\nimport { parseHtmlToElement } from '../../helpers/dom'\n\n/**\n * Transforms pasted HTML by converting tables to paragraphs while preserving surrounding content.\n */\nfunction transformPastedHTML(html: string): string {\n const body = parseHtmlToElement(html)\n const tables = body.querySelectorAll('table')\n\n if (tables.length === 0) {\n return html\n }\n\n for (const table of Array.from(tables)) {\n if (!table.rows) {\n continue\n }\n\n // Convert table rows to paragraphs (using innerHTML to preserve formatting)\n const paragraphs = Array.from(table.rows)\n .map((row) =>\n Array.from(row.cells)\n .map((cell) => {\n // Unwrap paragraphs but preserve inline formatting\n const cellParagraphs = cell.querySelectorAll('p')\n\n for (const p of Array.from(cellParagraphs)) {\n p.replaceWith(...Array.from(p.childNodes))\n }\n\n return cell.innerHTML\n })\n .join(' '),\n )\n .filter((row) => row.trim().length > 0)\n .map((row) => {\n const p = document.createElement('p')\n p.innerHTML = row\n return p\n })\n\n table.replaceWith(...paragraphs)\n }\n\n return body.innerHTML\n}\n\n/**\n * The `PasteHTMLTableAsString` extension adds the ability to paste a table copied from a spreadsheet\n * web app (e.g., Google Sheets, Microsoft Excel), along with tables rendered by GitHub Flavored\n * Markdown (GFM), into the editor.\n *\n * Since Typist does not yet support tables, this extension simply pastes the table as a string of\n * paragraphs (one paragraph per row), with each cell separated by a space character. However,\n * whenever we do add support for tables, this extension will need to be completely rewritten.\n *\n * Lastly, please note that formatting is lost when the copied table comes from Google Sheets or\n * Microsoft Excel, because unfortunately, these apps style the cell contents using CSS.\n */\nconst PasteHTMLTableAsString = Extension.create({\n name: 'pasteHTMLTableAsString',\n priority: PASTE_HTML_TABLE_AS_STRING_EXTENSION_PRIORITY,\n addProseMirrorPlugins() {\n return [\n new Plugin({\n key: new PluginKey('pasteHTMLTableAsString'),\n props: {\n transformPastedHTML,\n },\n }),\n ]\n },\n})\n\nexport { PasteHTMLTableAsString, transformPastedHTML }\n"],"mappings":";;;;;;;;AASA,SAAS,oBAAoB,MAAsB;CAC/C,MAAM,OAAO,mBAAmB,IAAI;CACpC,MAAM,SAAS,KAAK,iBAAiB,OAAO;CAE5C,IAAI,OAAO,WAAW,GAClB,OAAO;CAGX,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,GAAG;EACpC,IAAI,CAAC,MAAM,MACP;EAIJ,MAAM,aAAa,MAAM,KAAK,MAAM,IAAI,CAAC,CACpC,KAAK,QACF,MAAM,KAAK,IAAI,KAAK,CAAC,CAChB,KAAK,SAAS;GAEX,MAAM,iBAAiB,KAAK,iBAAiB,GAAG;GAEhD,KAAK,MAAM,KAAK,MAAM,KAAK,cAAc,GACrC,EAAE,YAAY,GAAG,MAAM,KAAK,EAAE,UAAU,CAAC;GAG7C,OAAO,KAAK;EAChB,CAAC,CAAC,CACD,KAAK,GAAG,CACjB,CAAC,CACA,QAAQ,QAAQ,IAAI,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,CACtC,KAAK,QAAQ;GACV,MAAM,IAAI,SAAS,cAAc,GAAG;GACpC,EAAE,YAAY;GACd,OAAO;EACX,CAAC;EAEL,MAAM,YAAY,GAAG,UAAU;CACnC;CAEA,OAAO,KAAK;AAChB;;;;;;;;;;;;;AAcA,MAAM,yBAAyB,UAAU,OAAO;CAC5C,MAAM;CACN,UAAU;CACV,wBAAwB;EACpB,OAAO,CACH,IAAI,OAAO;GACP,KAAK,IAAI,UAAU,wBAAwB;GAC3C,OAAO,EACH,oBACJ;EACJ,CAAC,CACL;CACJ;AACJ,CAAC"}
1
+ {"version":3,"file":"paste-html-table-as-string.js","names":[],"sources":["../../../src/extensions/shared/paste-html-table-as-string.ts"],"sourcesContent":["import { Extension } from '@tiptap/core'\nimport { Plugin, PluginKey } from '@tiptap/pm/state'\n\nimport { PASTE_HTML_TABLE_AS_STRING_EXTENSION_PRIORITY } from '../../constants/extension-priorities'\nimport { parseHtmlToElement } from '../../helpers/dom'\n\n/**\n * Transforms pasted HTML by converting tables to paragraphs while preserving surrounding content.\n */\nfunction transformPastedHTML(html: string): string {\n const body = parseHtmlToElement(html)\n const tables = body.querySelectorAll('table')\n\n if (tables.length === 0) {\n return html\n }\n\n for (const table of Array.from(tables)) {\n if (!table.rows) {\n continue\n }\n\n // Convert table rows to paragraphs (using innerHTML to preserve formatting)\n const paragraphs = Array.from(table.rows)\n .map((row) =>\n Array.from(row.cells)\n .map((cell) => {\n // Unwrap paragraphs but preserve inline formatting\n const cellParagraphs = cell.querySelectorAll('p')\n\n for (const p of Array.from(cellParagraphs)) {\n p.replaceWith(...Array.from(p.childNodes))\n }\n\n return cell.innerHTML\n })\n .join(' '),\n )\n .filter((row) => row.trim().length > 0)\n .map((row) => {\n const p = document.createElement('p')\n p.innerHTML = row\n return p\n })\n\n table.replaceWith(...paragraphs)\n }\n\n return body.innerHTML\n}\n\n/**\n * The `PasteHTMLTableAsString` extension adds the ability to paste a table copied from a spreadsheet\n * web app (e.g., Google Sheets, Microsoft Excel), along with tables rendered by GitHub Flavored\n * Markdown (GFM), into a plain-text editor.\n *\n * Since plain-text documents cannot represent tables, this extension simply pastes the table as a\n * string of paragraphs (one paragraph per row), with each cell separated by a space character.\n *\n * Lastly, please note that formatting is lost when the copied table comes from Google Sheets or\n * Microsoft Excel, because unfortunately, these apps style the cell contents using CSS.\n */\nconst PasteHTMLTableAsString = Extension.create({\n name: 'pasteHTMLTableAsString',\n priority: PASTE_HTML_TABLE_AS_STRING_EXTENSION_PRIORITY,\n addProseMirrorPlugins() {\n return [\n new Plugin({\n key: new PluginKey('pasteHTMLTableAsString'),\n props: {\n transformPastedHTML,\n },\n }),\n ]\n },\n})\n\nexport { PasteHTMLTableAsString, transformPastedHTML }\n"],"mappings":";;;;;;;;AASA,SAAS,oBAAoB,MAAsB;CAC/C,MAAM,OAAO,mBAAmB,IAAI;CACpC,MAAM,SAAS,KAAK,iBAAiB,OAAO;CAE5C,IAAI,OAAO,WAAW,GAClB,OAAO;CAGX,KAAK,MAAM,SAAS,MAAM,KAAK,MAAM,GAAG;EACpC,IAAI,CAAC,MAAM,MACP;EAIJ,MAAM,aAAa,MAAM,KAAK,MAAM,IAAI,CAAC,CACpC,KAAK,QACF,MAAM,KAAK,IAAI,KAAK,CAAC,CAChB,KAAK,SAAS;GAEX,MAAM,iBAAiB,KAAK,iBAAiB,GAAG;GAEhD,KAAK,MAAM,KAAK,MAAM,KAAK,cAAc,GACrC,EAAE,YAAY,GAAG,MAAM,KAAK,EAAE,UAAU,CAAC;GAG7C,OAAO,KAAK;EAChB,CAAC,CAAC,CACD,KAAK,GAAG,CACjB,CAAC,CACA,QAAQ,QAAQ,IAAI,KAAK,CAAC,CAAC,SAAS,CAAC,CAAC,CACtC,KAAK,QAAQ;GACV,MAAM,IAAI,SAAS,cAAc,GAAG;GACpC,EAAE,YAAY;GACd,OAAO;EACX,CAAC;EAEL,MAAM,YAAY,GAAG,UAAU;CACnC;CAEA,OAAO,KAAK;AAChB;;;;;;;;;;;;AAaA,MAAM,yBAAyB,UAAU,OAAO;CAC5C,MAAM;CACN,UAAU;CACV,wBAAwB;EACpB,OAAO,CACH,IAAI,OAAO;GACP,KAAK,IAAI,UAAU,wBAAwB;GAC3C,OAAO,EACH,oBACJ;EACJ,CAAC,CACL;CACJ;AACJ,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"html.d.ts","names":[],"sources":["../../../src/serializers/html/html.ts"],"mappings":";;;;;AAsB8C;KAKzC,wBAAA;;;AAQ2B;AAAA;;;;EAA5B,SAAA,GAAY,QAAgB;AAAA;;;AAqDuC;AAAA;;;;;;iBAA9D,oBAAA,CAAqB,MAAA,EAAQ,MAAA,GAAS,wBAAwB;;AA+FtB;;;;;;iBAAxC,yBAAA,CAA0B,MAAA,EAAQ,MAAA,GAAM,wBAAA"}
1
+ {"version":3,"file":"html.d.ts","names":[],"sources":["../../../src/serializers/html/html.ts"],"mappings":";;;;;AAwB8C;KAKzC,wBAAA;;;AAQ2B;AAAA;;;;EAA5B,SAAA,GAAY,QAAgB;AAAA;;;AAqDuC;AAAA;;;;;;iBAA9D,oBAAA,CAAqB,MAAA,EAAQ,MAAA,GAAS,wBAAwB;;AA2GtB;;;;;;iBAAxC,yBAAA,CAA0B,MAAA,EAAQ,MAAA,GAAM,wBAAA"}
@@ -3,10 +3,12 @@ import { buildSuggestionSchemaInfo, computeSuggestionTriggerCharsId } from "../.
3
3
  import { rehypeCodeBlock } from "./plugins/rehype-code-block.js";
4
4
  import { rehypeImage } from "./plugins/rehype-image.js";
5
5
  import { rehypeSuggestions } from "./plugins/rehype-suggestions.js";
6
+ import { rehypeTable } from "./plugins/rehype-table.js";
6
7
  import { rehypeTaskList } from "./plugins/rehype-task-list.js";
7
8
  import { remarkAutolinkLiteral } from "./plugins/remark-autolink-literal.js";
8
9
  import { remarkDisableConstructs } from "./plugins/remark-disable-constructs.js";
9
10
  import { remarkStrikethrough } from "./plugins/remark-strikethrough.js";
11
+ import { remarkTable } from "./plugins/remark-table.js";
10
12
  import { escape } from "lodash-es";
11
13
  import rehypeMinifyWhitespace from "rehype-minify-whitespace";
12
14
  import rehypeStringify from "rehype-stringify";
@@ -46,9 +48,11 @@ function createHTMLSerializer(schema) {
46
48
  if (schema.nodes.hardBreak) unifiedProcessor.use(remarkBreaks);
47
49
  if (schema.marks.strike) unifiedProcessor.use(remarkStrikethrough, { singleTilde: false });
48
50
  if (schema.marks.link) unifiedProcessor.use(remarkAutolinkLiteral);
51
+ if (schema.nodes.table) unifiedProcessor.use(remarkTable);
49
52
  unifiedProcessor.use(remarkRehype, { allowDangerousHtml: true }).use(rehypeMinifyWhitespace, { newlines: true });
50
53
  if (schema.nodes.codeBlock) unifiedProcessor.use(rehypeCodeBlock);
51
54
  if (schema.nodes.paragraph && schema.nodes.image) unifiedProcessor.use(rehypeImage, schema);
55
+ if (schema.nodes.table && schema.nodes.hardBreak) unifiedProcessor.use(rehypeTable);
52
56
  if (schema.nodes.taskList && schema.nodes.taskItem) unifiedProcessor.use(rehypeTaskList);
53
57
  unifiedProcessor.use(rehypeSuggestions, schema);
54
58
  unifiedProcessor.use(rehypeStringify, { characterReferences: { useNamedReferences: true } });
@@ -1 +1 @@
1
- {"version":3,"file":"html.js","names":[],"sources":["../../../src/serializers/html/html.ts"],"sourcesContent":["import { escape } from 'lodash-es'\nimport rehypeMinifyWhitespace from 'rehype-minify-whitespace'\nimport rehypeStringify from 'rehype-stringify'\nimport remarkBreaks from 'remark-breaks'\nimport remarkParse from 'remark-parse'\nimport remarkRehype from 'remark-rehype'\nimport { unified } from 'unified'\n\nimport { computeSchemaId, isPlainTextDocument } from '../../helpers/schema'\nimport {\n buildSuggestionSchemaInfo,\n computeSuggestionTriggerCharsId,\n} from '../../helpers/serializer'\n\nimport { rehypeCodeBlock } from './plugins/rehype-code-block'\nimport { rehypeImage } from './plugins/rehype-image'\nimport { rehypeSuggestions } from './plugins/rehype-suggestions'\nimport { rehypeTaskList } from './plugins/rehype-task-list'\nimport { remarkAutolinkLiteral } from './plugins/remark-autolink-literal'\nimport { remarkDisableConstructs } from './plugins/remark-disable-constructs'\nimport { remarkStrikethrough } from './plugins/remark-strikethrough'\n\nimport type { Schema } from '@tiptap/pm/model'\n\n/**\n * The return type for the `createHTMLSerializer` function.\n */\ntype HTMLSerializerReturnType = {\n /**\n * Serializes an input Markdown string to an output HTML string.\n *\n * @param markdown The Markdown string to serialize.\n *\n * @returns The serialized HTML.\n */\n serialize: (markdown: string) => string\n}\n\n/**\n * The type for the object that holds multiple HTML serializer instances.\n */\ntype HTMLSerializerInstanceById = {\n [id: string]: HTMLSerializerReturnType\n}\n\n/**\n * Create a custom Markdown to HTML serializer for plain-text editors only.\n *\n * @param schema The editor schema to be used for nodes and marks detection.\n *\n * @returns A normalized object for the HTML serializer.\n */\nfunction createHTMLSerializerForPlainTextEditor(schema: Schema) {\n return {\n serialize(markdown: string) {\n // Converts special characters (i.e. `&`, `<`, `>`, `\"`, and `'`) to their corresponding\n // HTML entities because we need to output the full content as valid HTML (i.e. the\n // editor should not drop invalid HTML).\n let htmlResult = escape(markdown)\n\n // Serialize all suggestion links if any suggestion node exists in the schema\n const suggestionSchemaInfo = buildSuggestionSchemaInfo(schema)\n\n if (suggestionSchemaInfo) {\n for (const [linkSchema, triggerChar] of suggestionSchemaInfo.triggerCharByScheme) {\n htmlResult = htmlResult.replace(\n new RegExp(`\\\\[([^\\\\[]+)\\\\]\\\\((?:${linkSchema}):\\\\/\\\\/([^\\\\s)]+)\\\\)`, 'gm'),\n (_, label, id) =>\n `<span data-${linkSchema} data-id=\"${id}\" data-label=\"${label}\">${triggerChar}${label}</span>`,\n )\n }\n }\n\n // Return the serialized HTML with every line wrapped in a paragraph element\n return htmlResult.replace(/^([^\\n]+)\\n?|\\n+/gm, `<p>$1</p>`)\n },\n }\n}\n\n/**\n * Create a Markdown to HTML serializer with the unified ecosystem for a rich-text editor, or use a\n * custom serializer for a plain-text editor. The editor schema is used to detect which nodes and\n * marks are available in the editor, and only parses the input with the minimal required plugins.\n *\n * @param schema The editor schema to be used for nodes and marks detection.\n *\n * @returns A normalized object for the HTML serializer.\n */\nfunction createHTMLSerializer(schema: Schema): HTMLSerializerReturnType {\n // Returns a custom HTML serializer for plain-text editors\n if (isPlainTextDocument(schema)) {\n return createHTMLSerializerForPlainTextEditor(schema)\n }\n\n // Initialize a unified processor with a remark plugin for parsing Markdown\n const unifiedProcessor = unified().use(remarkParse)\n\n // Configure the unified processor to use a custom plugin to disable constructs based on the\n // supported extensions that are enabled in the editor schema\n unifiedProcessor.use(remarkDisableConstructs, schema)\n\n // Configure the unified processor to use a third-party plugin to turn soft line endings into\n // hard breaks (i.e. `<br>`), which will display user content closer to how it was authored\n // (although not CommonMark compliant, this resembles the behaviour we always supported)\n if (schema.nodes.hardBreak) {\n unifiedProcessor.use(remarkBreaks)\n }\n\n // Configure the unified processor to use a custom plugin to add support for the strikethrough\n // extension from the GitHub Flavored Markdown (GFM) specification\n if (schema.marks.strike) {\n unifiedProcessor.use(remarkStrikethrough, { singleTilde: false })\n }\n\n // Configure the unified processor to use a custom plugin to add support for the autolink\n // literals extension from the GitHub Flavored Markdown (GFM) specification\n if (schema.marks.link) {\n unifiedProcessor.use(remarkAutolinkLiteral)\n }\n\n // Configure the unified processor with an official plugin to convert Markdown into HTML to\n // support rehype (a tool that transforms HTML with plugins), followed by another official\n // plugin to minify whitespace between tags (prevents line feeds from appearing as blank)\n unifiedProcessor\n .use(remarkRehype, {\n // Persist raw HTML (disables support for custom elements/tags)\n // ref: https://github.com/Doist/Issues/issues/5689\n allowDangerousHtml: true,\n })\n // This must come before all rehype plugins that transform the HTML output\n .use(rehypeMinifyWhitespace, {\n // Preserve line breaks when collapsing whitespace (e.g., line feeds)\n newlines: true,\n })\n\n // Configure the unified processor with a custom plugin to remove the trailing newline from code\n // blocks (i.e. the newline between the last code line and `</code></pre>`)\n if (schema.nodes.codeBlock) {\n unifiedProcessor.use(rehypeCodeBlock)\n }\n\n // Configure the unified processor with a custom plugin to remove the wrapping paragraph from\n // images and to remove all inline images based on inline images support in the editor schema\n if (schema.nodes.paragraph && schema.nodes.image) {\n unifiedProcessor.use(rehypeImage, schema)\n }\n\n // Configure the unified processor with a custom plugin to add support Tiptap task lists\n if (schema.nodes.taskList && schema.nodes.taskItem) {\n unifiedProcessor.use(rehypeTaskList)\n }\n\n // Configure the unified processor with a custom plugin to add support for suggestions nodes\n unifiedProcessor.use(rehypeSuggestions, schema)\n\n // Configure the unified processor with an official plugin that defines how to take a syntax\n // tree as input and turn it into serialized HTML\n unifiedProcessor.use(rehypeStringify, {\n characterReferences: {\n // Compatibility with the previous implementation in Marked\n useNamedReferences: true,\n },\n })\n\n return {\n serialize(markdown: string) {\n return unifiedProcessor.processSync(markdown).toString()\n },\n }\n}\n\n/**\n * Object that holds multiple HTML serializer instances based on a given ID.\n */\nconst htmlSerializerInstanceById: HTMLSerializerInstanceById = {}\n\n/**\n * Returns a singleton instance of a HTML serializer based on the provided editor schema.\n *\n * @param schema The editor schema connected to the HTML serializer instance.\n *\n * @returns The HTML serializer instance for the given editor schema.\n */\nfunction getHTMLSerializerInstance(schema: Schema) {\n const schemaId = computeSchemaId(schema)\n const triggerCharsId = computeSuggestionTriggerCharsId(schema)\n\n const id = [schemaId, triggerCharsId].join('|')\n\n if (!htmlSerializerInstanceById[id]) {\n htmlSerializerInstanceById[id] = createHTMLSerializer(schema)\n }\n\n return htmlSerializerInstanceById[id]\n}\n\nexport { createHTMLSerializer, getHTMLSerializerInstance }\n\nexport type { HTMLSerializerReturnType }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAoDA,SAAS,uCAAuC,QAAgB;CAC5D,OAAO,EACH,UAAU,UAAkB;EAIxB,IAAI,aAAa,OAAO,QAAQ;EAGhC,MAAM,uBAAuB,0BAA0B,MAAM;EAE7D,IAAI,sBACA,KAAK,MAAM,CAAC,YAAY,gBAAgB,qBAAqB,qBACzD,aAAa,WAAW,QACpB,IAAI,OAAO,wBAAwB,WAAW,wBAAwB,IAAI,IACzE,GAAG,OAAO,OACP,cAAc,WAAW,YAAY,GAAG,gBAAgB,MAAM,IAAI,cAAc,MAAM,QAC9F;EAKR,OAAO,WAAW,QAAQ,sBAAsB,WAAW;CAC/D,EACJ;AACJ;;;;;;;;;;AAWA,SAAS,qBAAqB,QAA0C;CAEpE,IAAI,oBAAoB,MAAM,GAC1B,OAAO,uCAAuC,MAAM;CAIxD,MAAM,mBAAmB,QAAQ,CAAC,CAAC,IAAI,WAAW;CAIlD,iBAAiB,IAAI,yBAAyB,MAAM;CAKpD,IAAI,OAAO,MAAM,WACb,iBAAiB,IAAI,YAAY;CAKrC,IAAI,OAAO,MAAM,QACb,iBAAiB,IAAI,qBAAqB,EAAE,aAAa,MAAM,CAAC;CAKpE,IAAI,OAAO,MAAM,MACb,iBAAiB,IAAI,qBAAqB;CAM9C,iBACK,IAAI,cAAc,EAGf,oBAAoB,KACxB,CAAC,CAAC,CAED,IAAI,wBAAwB,EAEzB,UAAU,KACd,CAAC;CAIL,IAAI,OAAO,MAAM,WACb,iBAAiB,IAAI,eAAe;CAKxC,IAAI,OAAO,MAAM,aAAa,OAAO,MAAM,OACvC,iBAAiB,IAAI,aAAa,MAAM;CAI5C,IAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UACtC,iBAAiB,IAAI,cAAc;CAIvC,iBAAiB,IAAI,mBAAmB,MAAM;CAI9C,iBAAiB,IAAI,iBAAiB,EAClC,qBAAqB,EAEjB,oBAAoB,KACxB,EACJ,CAAC;CAED,OAAO,EACH,UAAU,UAAkB;EACxB,OAAO,iBAAiB,YAAY,QAAQ,CAAC,CAAC,SAAS;CAC3D,EACJ;AACJ;;;;AAKA,MAAM,6BAAyD,CAAC;;;;;;;;AAShE,SAAS,0BAA0B,QAAgB;CAI/C,MAAM,KAAK,CAHM,gBAAgB,MAGd,GAFI,gCAAgC,MAEpB,CAAC,CAAC,CAAC,KAAK,GAAG;CAE9C,IAAI,CAAC,2BAA2B,KAC5B,2BAA2B,MAAM,qBAAqB,MAAM;CAGhE,OAAO,2BAA2B;AACtC"}
1
+ {"version":3,"file":"html.js","names":[],"sources":["../../../src/serializers/html/html.ts"],"sourcesContent":["import { escape } from 'lodash-es'\nimport rehypeMinifyWhitespace from 'rehype-minify-whitespace'\nimport rehypeStringify from 'rehype-stringify'\nimport remarkBreaks from 'remark-breaks'\nimport remarkParse from 'remark-parse'\nimport remarkRehype from 'remark-rehype'\nimport { unified } from 'unified'\n\nimport { computeSchemaId, isPlainTextDocument } from '../../helpers/schema'\nimport {\n buildSuggestionSchemaInfo,\n computeSuggestionTriggerCharsId,\n} from '../../helpers/serializer'\n\nimport { rehypeCodeBlock } from './plugins/rehype-code-block'\nimport { rehypeImage } from './plugins/rehype-image'\nimport { rehypeSuggestions } from './plugins/rehype-suggestions'\nimport { rehypeTable } from './plugins/rehype-table'\nimport { rehypeTaskList } from './plugins/rehype-task-list'\nimport { remarkAutolinkLiteral } from './plugins/remark-autolink-literal'\nimport { remarkDisableConstructs } from './plugins/remark-disable-constructs'\nimport { remarkStrikethrough } from './plugins/remark-strikethrough'\nimport { remarkTable } from './plugins/remark-table'\n\nimport type { Schema } from '@tiptap/pm/model'\n\n/**\n * The return type for the `createHTMLSerializer` function.\n */\ntype HTMLSerializerReturnType = {\n /**\n * Serializes an input Markdown string to an output HTML string.\n *\n * @param markdown The Markdown string to serialize.\n *\n * @returns The serialized HTML.\n */\n serialize: (markdown: string) => string\n}\n\n/**\n * The type for the object that holds multiple HTML serializer instances.\n */\ntype HTMLSerializerInstanceById = {\n [id: string]: HTMLSerializerReturnType\n}\n\n/**\n * Create a custom Markdown to HTML serializer for plain-text editors only.\n *\n * @param schema The editor schema to be used for nodes and marks detection.\n *\n * @returns A normalized object for the HTML serializer.\n */\nfunction createHTMLSerializerForPlainTextEditor(schema: Schema) {\n return {\n serialize(markdown: string) {\n // Converts special characters (i.e. `&`, `<`, `>`, `\"`, and `'`) to their corresponding\n // HTML entities because we need to output the full content as valid HTML (i.e. the\n // editor should not drop invalid HTML).\n let htmlResult = escape(markdown)\n\n // Serialize all suggestion links if any suggestion node exists in the schema\n const suggestionSchemaInfo = buildSuggestionSchemaInfo(schema)\n\n if (suggestionSchemaInfo) {\n for (const [linkSchema, triggerChar] of suggestionSchemaInfo.triggerCharByScheme) {\n htmlResult = htmlResult.replace(\n new RegExp(`\\\\[([^\\\\[]+)\\\\]\\\\((?:${linkSchema}):\\\\/\\\\/([^\\\\s)]+)\\\\)`, 'gm'),\n (_, label, id) =>\n `<span data-${linkSchema} data-id=\"${id}\" data-label=\"${label}\">${triggerChar}${label}</span>`,\n )\n }\n }\n\n // Return the serialized HTML with every line wrapped in a paragraph element\n return htmlResult.replace(/^([^\\n]+)\\n?|\\n+/gm, `<p>$1</p>`)\n },\n }\n}\n\n/**\n * Create a Markdown to HTML serializer with the unified ecosystem for a rich-text editor, or use a\n * custom serializer for a plain-text editor. The editor schema is used to detect which nodes and\n * marks are available in the editor, and only parses the input with the minimal required plugins.\n *\n * @param schema The editor schema to be used for nodes and marks detection.\n *\n * @returns A normalized object for the HTML serializer.\n */\nfunction createHTMLSerializer(schema: Schema): HTMLSerializerReturnType {\n // Returns a custom HTML serializer for plain-text editors\n if (isPlainTextDocument(schema)) {\n return createHTMLSerializerForPlainTextEditor(schema)\n }\n\n // Initialize a unified processor with a remark plugin for parsing Markdown\n const unifiedProcessor = unified().use(remarkParse)\n\n // Configure the unified processor to use a custom plugin to disable constructs based on the\n // supported extensions that are enabled in the editor schema\n unifiedProcessor.use(remarkDisableConstructs, schema)\n\n // Configure the unified processor to use a third-party plugin to turn soft line endings into\n // hard breaks (i.e. `<br>`), which will display user content closer to how it was authored\n // (although not CommonMark compliant, this resembles the behaviour we always supported)\n if (schema.nodes.hardBreak) {\n unifiedProcessor.use(remarkBreaks)\n }\n\n // Configure the unified processor to use a custom plugin to add support for the strikethrough\n // extension from the GitHub Flavored Markdown (GFM) specification\n if (schema.marks.strike) {\n unifiedProcessor.use(remarkStrikethrough, { singleTilde: false })\n }\n\n // Configure the unified processor to use a custom plugin to add support for the autolink\n // literals extension from the GitHub Flavored Markdown (GFM) specification\n if (schema.marks.link) {\n unifiedProcessor.use(remarkAutolinkLiteral)\n }\n\n // Configure the unified processor to use a custom plugin to add support for the table\n // extension from the GitHub Flavored Markdown (GFM) specification\n if (schema.nodes.table) {\n unifiedProcessor.use(remarkTable)\n }\n\n // Configure the unified processor with an official plugin to convert Markdown into HTML to\n // support rehype (a tool that transforms HTML with plugins), followed by another official\n // plugin to minify whitespace between tags (prevents line feeds from appearing as blank)\n unifiedProcessor\n .use(remarkRehype, {\n // Persist raw HTML (disables support for custom elements/tags)\n // ref: https://github.com/Doist/Issues/issues/5689\n allowDangerousHtml: true,\n })\n // This must come before all rehype plugins that transform the HTML output\n .use(rehypeMinifyWhitespace, {\n // Preserve line breaks when collapsing whitespace (e.g., line feeds)\n newlines: true,\n })\n\n // Configure the unified processor with a custom plugin to remove the trailing newline from code\n // blocks (i.e. the newline between the last code line and `</code></pre>`)\n if (schema.nodes.codeBlock) {\n unifiedProcessor.use(rehypeCodeBlock)\n }\n\n // Configure the unified processor with a custom plugin to remove the wrapping paragraph from\n // images and to remove all inline images based on inline images support in the editor schema\n if (schema.nodes.paragraph && schema.nodes.image) {\n unifiedProcessor.use(rehypeImage, schema)\n }\n\n // Configure the unified processor with a custom plugin to restore raw `<br>` elements within\n // table cells into actual hard break elements\n if (schema.nodes.table && schema.nodes.hardBreak) {\n unifiedProcessor.use(rehypeTable)\n }\n\n // Configure the unified processor with a custom plugin to add support Tiptap task lists\n if (schema.nodes.taskList && schema.nodes.taskItem) {\n unifiedProcessor.use(rehypeTaskList)\n }\n\n // Configure the unified processor with a custom plugin to add support for suggestions nodes\n unifiedProcessor.use(rehypeSuggestions, schema)\n\n // Configure the unified processor with an official plugin that defines how to take a syntax\n // tree as input and turn it into serialized HTML\n unifiedProcessor.use(rehypeStringify, {\n characterReferences: {\n // Compatibility with the previous implementation in Marked\n useNamedReferences: true,\n },\n })\n\n return {\n serialize(markdown: string) {\n return unifiedProcessor.processSync(markdown).toString()\n },\n }\n}\n\n/**\n * Object that holds multiple HTML serializer instances based on a given ID.\n */\nconst htmlSerializerInstanceById: HTMLSerializerInstanceById = {}\n\n/**\n * Returns a singleton instance of a HTML serializer based on the provided editor schema.\n *\n * @param schema The editor schema connected to the HTML serializer instance.\n *\n * @returns The HTML serializer instance for the given editor schema.\n */\nfunction getHTMLSerializerInstance(schema: Schema) {\n const schemaId = computeSchemaId(schema)\n const triggerCharsId = computeSuggestionTriggerCharsId(schema)\n\n const id = [schemaId, triggerCharsId].join('|')\n\n if (!htmlSerializerInstanceById[id]) {\n htmlSerializerInstanceById[id] = createHTMLSerializer(schema)\n }\n\n return htmlSerializerInstanceById[id]\n}\n\nexport { createHTMLSerializer, getHTMLSerializerInstance }\n\nexport type { HTMLSerializerReturnType }\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,SAAS,uCAAuC,QAAgB;CAC5D,OAAO,EACH,UAAU,UAAkB;EAIxB,IAAI,aAAa,OAAO,QAAQ;EAGhC,MAAM,uBAAuB,0BAA0B,MAAM;EAE7D,IAAI,sBACA,KAAK,MAAM,CAAC,YAAY,gBAAgB,qBAAqB,qBACzD,aAAa,WAAW,QACpB,IAAI,OAAO,wBAAwB,WAAW,wBAAwB,IAAI,IACzE,GAAG,OAAO,OACP,cAAc,WAAW,YAAY,GAAG,gBAAgB,MAAM,IAAI,cAAc,MAAM,QAC9F;EAKR,OAAO,WAAW,QAAQ,sBAAsB,WAAW;CAC/D,EACJ;AACJ;;;;;;;;;;AAWA,SAAS,qBAAqB,QAA0C;CAEpE,IAAI,oBAAoB,MAAM,GAC1B,OAAO,uCAAuC,MAAM;CAIxD,MAAM,mBAAmB,QAAQ,CAAC,CAAC,IAAI,WAAW;CAIlD,iBAAiB,IAAI,yBAAyB,MAAM;CAKpD,IAAI,OAAO,MAAM,WACb,iBAAiB,IAAI,YAAY;CAKrC,IAAI,OAAO,MAAM,QACb,iBAAiB,IAAI,qBAAqB,EAAE,aAAa,MAAM,CAAC;CAKpE,IAAI,OAAO,MAAM,MACb,iBAAiB,IAAI,qBAAqB;CAK9C,IAAI,OAAO,MAAM,OACb,iBAAiB,IAAI,WAAW;CAMpC,iBACK,IAAI,cAAc,EAGf,oBAAoB,KACxB,CAAC,CAAC,CAED,IAAI,wBAAwB,EAEzB,UAAU,KACd,CAAC;CAIL,IAAI,OAAO,MAAM,WACb,iBAAiB,IAAI,eAAe;CAKxC,IAAI,OAAO,MAAM,aAAa,OAAO,MAAM,OACvC,iBAAiB,IAAI,aAAa,MAAM;CAK5C,IAAI,OAAO,MAAM,SAAS,OAAO,MAAM,WACnC,iBAAiB,IAAI,WAAW;CAIpC,IAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UACtC,iBAAiB,IAAI,cAAc;CAIvC,iBAAiB,IAAI,mBAAmB,MAAM;CAI9C,iBAAiB,IAAI,iBAAiB,EAClC,qBAAqB,EAEjB,oBAAoB,KACxB,EACJ,CAAC;CAED,OAAO,EACH,UAAU,UAAkB;EACxB,OAAO,iBAAiB,YAAY,QAAQ,CAAC,CAAC,SAAS;CAC3D,EACJ;AACJ;;;;AAKA,MAAM,6BAAyD,CAAC;;;;;;;;AAShE,SAAS,0BAA0B,QAAgB;CAI/C,MAAM,KAAK,CAHM,gBAAgB,MAGd,GAFI,gCAAgC,MAEpB,CAAC,CAAC,CAAC,KAAK,GAAG;CAE9C,IAAI,CAAC,2BAA2B,KAC5B,2BAA2B,MAAM,qBAAqB,MAAM;CAGhE,OAAO,2BAA2B;AACtC"}
@@ -0,0 +1,32 @@
1
+ import { isHastElementNode } from "../../../helpers/unified.js";
2
+ import { visit } from "unist-util-visit";
3
+ //#region src/serializers/html/plugins/rehype-table.ts
4
+ /**
5
+ * A rehype plugin to add support for hard breaks within table cells.
6
+ *
7
+ * Since the GFM table syntax cannot represent multiple lines within a cell, the Markdown
8
+ * serializer outputs hard breaks inside table cells as literal `<br>` elements (which GFM renders as line
9
+ * breaks). However, the HTML serializer persists raw HTML as escaped text (i.e. raw HTML is not
10
+ * interpreted), and thus this plugin restores raw `<br>` elements found within table cells into
11
+ * actual hard breaks.
12
+ */
13
+ function rehypeTable() {
14
+ return (...[tree]) => {
15
+ visit(tree, "element", (node) => {
16
+ if (!isHastElementNode(node, "th") && !isHastElementNode(node, "td")) return;
17
+ visit(node, "raw", (raw, index, parent) => {
18
+ if (parent && typeof index === "number" && "value" in raw && typeof raw.value === "string" && /^<br\s*\/?>$/.test(raw.value)) parent.children[index] = {
19
+ type: "element",
20
+ tagName: "br",
21
+ properties: {},
22
+ children: []
23
+ };
24
+ });
25
+ });
26
+ return tree;
27
+ };
28
+ }
29
+ //#endregion
30
+ export { rehypeTable };
31
+
32
+ //# sourceMappingURL=rehype-table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rehype-table.js","names":[],"sources":["../../../../src/serializers/html/plugins/rehype-table.ts"],"sourcesContent":["import { visit } from 'unist-util-visit'\n\nimport { isHastElementNode } from '../../../helpers/unified'\n\nimport type { Node as HastNode } from 'hast'\nimport type { Transformer } from 'unified'\n\n/**\n * A rehype plugin to add support for hard breaks within table cells.\n *\n * Since the GFM table syntax cannot represent multiple lines within a cell, the Markdown\n * serializer outputs hard breaks inside table cells as literal `<br>` elements (which GFM renders as line\n * breaks). However, the HTML serializer persists raw HTML as escaped text (i.e. raw HTML is not\n * interpreted), and thus this plugin restores raw `<br>` elements found within table cells into\n * actual hard breaks.\n */\nfunction rehypeTable(): Transformer {\n return (...[tree]: Parameters<Transformer>): ReturnType<Transformer> => {\n visit(tree, 'element', (node: HastNode) => {\n if (!isHastElementNode(node, 'th') && !isHastElementNode(node, 'td')) {\n return\n }\n\n // Replace raw `<br>` nodes at any depth within the cell (which would otherwise be\n // output as escaped text) with actual hard break elements (e.g., a hard break inside\n // styled text is nested within the mark element), mutating only the matching nodes\n visit(node, 'raw', (raw, index, parent) => {\n if (\n parent &&\n typeof index === 'number' &&\n 'value' in raw &&\n typeof raw.value === 'string' &&\n /^<br\\s*\\/?>$/.test(raw.value)\n ) {\n parent.children[index] = {\n type: 'element',\n tagName: 'br',\n properties: {},\n children: [],\n }\n }\n })\n })\n\n return tree\n }\n}\n\nexport { rehypeTable }\n"],"mappings":";;;;;;;;;;;;AAgBA,SAAS,cAA2B;CAChC,QAAQ,GAAG,CAAC,UAA4D;EACpE,MAAM,MAAM,YAAY,SAAmB;GACvC,IAAI,CAAC,kBAAkB,MAAM,IAAI,KAAK,CAAC,kBAAkB,MAAM,IAAI,GAC/D;GAMJ,MAAM,MAAM,QAAQ,KAAK,OAAO,WAAW;IACvC,IACI,UACA,OAAO,UAAU,YACjB,WAAW,OACX,OAAO,IAAI,UAAU,YACrB,eAAe,KAAK,IAAI,KAAK,GAE7B,OAAO,SAAS,SAAS;KACrB,MAAM;KACN,SAAS;KACT,YAAY,CAAC;KACb,UAAU,CAAC;IACf;GAER,CAAC;EACL,CAAC;EAED,OAAO;CACX;AACJ"}
@@ -0,0 +1,27 @@
1
+ import { gfmTableFromMarkdown, gfmTableToMarkdown } from "mdast-util-gfm-table";
2
+ import { gfmTable } from "micromark-extension-gfm-table";
3
+ //#region src/serializers/html/plugins/remark-table.ts
4
+ /**
5
+ * A remark plugin to add support for the table extension from the GitHub Flavored Markdown (GFM)
6
+ * specification.
7
+ *
8
+ * This is an standalone plugin which makes use of both the `mdast-util-gfm-table` and
9
+ * `micromark-extension-gfm-table` packages, and the implementation is inspired by the third-party
10
+ * `remark-gfm` plugin.
11
+ *
12
+ * The reason why we don't use `remark-gfm` directly is because we don't want to support all other
13
+ * GFM features (footnotes, tagfilter, and tasklists).
14
+ */
15
+ function remarkTable() {
16
+ const data = this.data();
17
+ const micromarkExtensions = data.micromarkExtensions || (data.micromarkExtensions = []);
18
+ const fromMarkdownExtensions = data.fromMarkdownExtensions || (data.fromMarkdownExtensions = []);
19
+ const toMarkdownExtensions = data.toMarkdownExtensions || (data.toMarkdownExtensions = []);
20
+ micromarkExtensions.push(gfmTable());
21
+ fromMarkdownExtensions.push(gfmTableFromMarkdown());
22
+ toMarkdownExtensions.push(gfmTableToMarkdown());
23
+ }
24
+ //#endregion
25
+ export { remarkTable };
26
+
27
+ //# sourceMappingURL=remark-table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remark-table.js","names":[],"sources":["../../../../src/serializers/html/plugins/remark-table.ts"],"sourcesContent":["import { gfmTableFromMarkdown, gfmTableToMarkdown } from 'mdast-util-gfm-table'\nimport { gfmTable } from 'micromark-extension-gfm-table'\n\nimport type { Processor } from 'unified'\n\n/**\n * A remark plugin to add support for the table extension from the GitHub Flavored Markdown (GFM)\n * specification.\n *\n * This is an standalone plugin which makes use of both the `mdast-util-gfm-table` and\n * `micromark-extension-gfm-table` packages, and the implementation is inspired by the third-party\n * `remark-gfm` plugin.\n *\n * The reason why we don't use `remark-gfm` directly is because we don't want to support all other\n * GFM features (footnotes, tagfilter, and tasklists).\n */\nfunction remarkTable(this: Processor) {\n const data = this.data()\n\n const micromarkExtensions = data.micromarkExtensions || (data.micromarkExtensions = [])\n const fromMarkdownExtensions = data.fromMarkdownExtensions || (data.fromMarkdownExtensions = [])\n const toMarkdownExtensions = data.toMarkdownExtensions || (data.toMarkdownExtensions = [])\n\n micromarkExtensions.push(gfmTable())\n fromMarkdownExtensions.push(gfmTableFromMarkdown())\n toMarkdownExtensions.push(gfmTableToMarkdown())\n}\n\nexport { remarkTable }\n"],"mappings":";;;;;;;;;;;;;;AAgBA,SAAS,cAA6B;CAClC,MAAM,OAAO,KAAK,KAAK;CAEvB,MAAM,sBAAsB,KAAK,wBAAwB,KAAK,sBAAsB,CAAC;CACrF,MAAM,yBAAyB,KAAK,2BAA2B,KAAK,yBAAyB,CAAC;CAC9F,MAAM,uBAAuB,KAAK,yBAAyB,KAAK,uBAAuB,CAAC;CAExF,oBAAoB,KAAK,SAAS,CAAC;CACnC,uBAAuB,KAAK,qBAAqB,CAAC;CAClD,qBAAqB,KAAK,mBAAmB,CAAC;AAClD"}
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.d.ts","names":[],"sources":["../../../src/serializers/markdown/markdown.ts"],"mappings":";;;;;AAa8C;KAKzC,4BAAA;;;AAQuB;AAaJ;;;;EAbpB,SAAA,GAAY,IAAY;AAAA;;;AAuEmD;;;;;;;;AA6G1B;;;;;;iBA7G5C,wBAAA,CAAyB,MAAA,EAAQ,MAAA,GAAS,4BAA4B;;;;;;;;iBA6GtE,6BAAA,CAA8B,MAAA,EAAQ,MAAA,GAAM,4BAAA"}
1
+ {"version":3,"file":"markdown.d.ts","names":[],"sources":["../../../src/serializers/markdown/markdown.ts"],"mappings":";;;;;AAc8C;KAKzC,4BAAA;;;AAQuB;AAaJ;;;;EAbpB,SAAA,GAAY,IAAY;AAAA;;;AAuEmD;;;;;;;;AAoI1B;;;;;;iBApI5C,wBAAA,CAAyB,MAAA,EAAQ,MAAA,GAAS,4BAA4B;;;;;;;;iBAoItE,6BAAA,CAA8B,MAAA,EAAQ,MAAA,GAAM,4BAAA"}
@@ -6,6 +6,7 @@ import { listItem } from "./plugins/list-item.js";
6
6
  import { paragraph } from "./plugins/paragraph.js";
7
7
  import { strikethrough } from "./plugins/strikethrough.js";
8
8
  import { suggestion } from "./plugins/suggestion.js";
9
+ import { table } from "./plugins/table.js";
9
10
  import { taskItem } from "./plugins/task-item.js";
10
11
  import Turndown from "turndown";
11
12
  /**
@@ -55,12 +56,19 @@ function createMarkdownSerializer(schema) {
55
56
  const turndown = new Turndown(INITIAL_TURNDOWN_OPTIONS);
56
57
  if (isPlainTextDocument(schema)) turndown.escape = (str) => str;
57
58
  else turndown.escape = (str) => {
58
- return str.replace(new RegExp(`(\\\\${REGEX_PUNCTUATION.source})`, "g"), "\\$1").replace(/^(\d+)\.(\s.+|$)/, "$1\\.$2");
59
+ return str.replace(new RegExp(`(\\\\${REGEX_PUNCTUATION.source})`, "g"), "\\$1").replace(/^(\d+)\.(\s.+|$)/, "$1\\.$2").replace(/<(br\s*\/?)>/gi, "\\<$1>");
59
60
  };
60
61
  turndown.use(paragraph(schema.nodes.paragraph, isPlainTextDocument(schema)));
61
62
  if (schema.nodes.image) turndown.use(image(schema.nodes.image));
62
63
  if ((schema.nodes.bulletList || schema.nodes.orderedList) && schema.nodes.listItem) turndown.use(listItem(schema.nodes.listItem));
63
64
  if (schema.marks.strike) turndown.use(strikethrough(schema.marks.strike));
65
+ if (schema.nodes.table && schema.nodes.tableRow && schema.nodes.tableHeader && schema.nodes.tableCell) turndown.use(table({
66
+ table: schema.nodes.table,
67
+ tableRow: schema.nodes.tableRow,
68
+ tableHeader: schema.nodes.tableHeader,
69
+ tableCell: schema.nodes.tableCell,
70
+ hardBreak: schema.nodes.hardBreak
71
+ }));
64
72
  if (schema.nodes.taskList && schema.nodes.taskItem) turndown.use(taskItem(schema.nodes.taskItem));
65
73
  getSuggestionNodes(schema).forEach((suggestionNode) => {
66
74
  turndown.use(suggestion(suggestionNode));
@@ -1 +1 @@
1
- {"version":3,"file":"markdown.js","names":[],"sources":["../../../src/serializers/markdown/markdown.ts"],"sourcesContent":["import Turndown from 'turndown'\n\nimport { REGEX_PUNCTUATION } from '../../constants/regular-expressions'\nimport { computeSchemaId, isPlainTextDocument } from '../../helpers/schema'\nimport { getSuggestionNodes } from '../../helpers/serializer'\n\nimport { image } from './plugins/image'\nimport { listItem } from './plugins/list-item'\nimport { paragraph } from './plugins/paragraph'\nimport { strikethrough } from './plugins/strikethrough'\nimport { suggestion } from './plugins/suggestion'\nimport { taskItem } from './plugins/task-item'\n\nimport type { Schema } from '@tiptap/pm/model'\n\n/**\n * The return type for the `createMarkdownSerializer` function.\n */\ntype MarkdownSerializerReturnType = {\n /**\n * Serializes an input HTML string to an output Markdown string.\n *\n * @param html The HTML string to serialize.\n *\n * @returns The serialized Markdown.\n */\n serialize: (html: string) => string\n}\n\n/**\n * The type for the object that holds multiple Markdown serializer instances.\n */\ntype MarkdownSerializerInstanceById = {\n [id: string]: MarkdownSerializerReturnType\n}\n\n/**\n * The bullet list marker for both standard and task list items.\n */\nconst BULLET_LIST_MARKER = '-'\n\n/**\n * Sensible default options to initialize the Turndown with.\n *\n * @see https://github.com/mixmark-io/turndown#options\n */\nconst INITIAL_TURNDOWN_OPTIONS: Turndown.Options = {\n headingStyle: 'atx',\n hr: '---',\n bulletListMarker: BULLET_LIST_MARKER,\n codeBlockStyle: 'fenced',\n fence: '```',\n emDelimiter: '*',\n strongDelimiter: '**',\n linkStyle: 'inlined',\n /**\n * Special rule to handle blank elements (overrides EVERY rule).\n *\n * @see https://github.com/mixmark-io/turndown#special-rules\n */\n blankReplacement(_, node) {\n const parentNode = node.parentNode as HTMLElement\n\n // Return the list marker for empty bullet list items\n if (node.nodeName === 'UL' || (parentNode?.nodeName === 'UL' && node.nodeName === 'LI')) {\n return `${BULLET_LIST_MARKER} \\n`\n }\n\n // Return the list marker for empty ordered list items\n if (node.nodeName === 'OL' || (parentNode?.nodeName === 'OL' && node.nodeName === 'LI')) {\n const start =\n node.nodeName === 'LI'\n ? parentNode.getAttribute('start')\n : node.getAttribute('start')\n const index = Array.prototype.indexOf.call(parentNode.children, node)\n\n return `${start ? Number(start) + index : index + 1}. \\n`\n }\n\n // @ts-ignore: The `Turndown.Node` type does not include `isBlock`\n return node.isBlock ? '\\n\\n' : ''\n },\n}\n\n/**\n * Create an HTML to Markdown serializer with the Turndown library for both a rich-text editor, and\n * a plain-text editor. The editor schema is used to detect which nodes and marks are available in\n * the editor, and only parses the input with the minimal required rules.\n *\n * **Note:** Unlike the HTML serializer, built-in rules that are not supported by the schema are not\n * disabled because if the schema does not support certain nodes/marks, the parsing rules don't have\n * valid HTML elements to match in the editor HTML output.\n *\n * @param schema The editor schema to be used for nodes and marks detection.\n *\n * @returns A normalized object for the Markdown serializer.\n */\nfunction createMarkdownSerializer(schema: Schema): MarkdownSerializerReturnType {\n // Initialize Turndown with custom options\n const turndown = new Turndown(INITIAL_TURNDOWN_OPTIONS)\n\n // Turndown ensures Markdown characters are escaped (i.e. `\\`) by default, so they are not\n // interpreted as Markdown when the output is compiled back to HTML. However, for plain-text\n // editors, we need to override the `escape` function to return the input as-is (effectively\n // disabling the escaping behaviour), so that all characters are interpreted as Markdown.\n if (isPlainTextDocument(schema)) {\n turndown.escape = (str) => str\n }\n\n // As for rich-text editors, we need to override the built-in escaping behaviour with a custom\n // implementation to suit our requirements. Please note that the `escape` function takes the\n // text content of each HTML element, with the exception of code elements, so we can be sure\n // that the escaping behaviour will only touch relevant Markdown characters.\n else {\n turndown.escape = (str) => {\n return (\n str\n // Escape all backslash characters that precede any punctuation marks, to\n // prevent the backslash itself from being interpreted as an escape sequence\n // for the subsequent character. It's important to apply this rule first to\n // avoid double escaping.\n .replace(new RegExp(`(\\\\\\\\${REGEX_PUNCTUATION.source})`, 'g'), '\\\\$1')\n\n // Although the CommonMark specification allows for bulleted or ordered lists\n // inside other bulleted or ordered lists (i.e. `- 1. - 1. Item`), the markup\n // generated by Markdown compilers is not supported by Tiptap, and we need to\n // make sure that text context that matches the ordered list syntax is\n // correctly escaped in order to be interpreted as text.\n .replace(/^(\\d+)\\.(\\s.+|$)/, '$1\\\\.$2')\n )\n }\n }\n\n // Overwrite some built-in rules for handling of special behaviours\n // (see documentation for each extension for more details)\n turndown.use(paragraph(schema.nodes.paragraph, isPlainTextDocument(schema)))\n\n // Overwrite the built-in `image` rule if the corresponding node exists in the schema\n if (schema.nodes.image) {\n turndown.use(image(schema.nodes.image))\n }\n\n // Overwrite the built-in `listItem` rule if the corresponding node exists in the schema\n if ((schema.nodes.bulletList || schema.nodes.orderedList) && schema.nodes.listItem) {\n turndown.use(listItem(schema.nodes.listItem))\n }\n\n // Add a rule for `strikethrough` if the corresponding node exists in the schema\n if (schema.marks.strike) {\n turndown.use(strikethrough(schema.marks.strike))\n }\n\n // Add a rule for `taskItem` if the corresponding nodes exists in the schema\n if (schema.nodes.taskList && schema.nodes.taskItem) {\n turndown.use(taskItem(schema.nodes.taskItem))\n }\n\n // Add a custom rule for all suggestion nodes available in the schema\n getSuggestionNodes(schema).forEach((suggestionNode) => {\n turndown.use(suggestion(suggestionNode))\n })\n\n // Return a normalized `serialize` function\n return {\n serialize(html: string) {\n let markdownResult = html\n\n // Turndown was built to convert HTML into Markdown, expecting the input to be\n // standard-compliant HTML. As such, it collapses all whitespace by default, and there's\n // currently no way to opt-out of this behavior. However, for plain-text editors, we\n // need to preserve Markdown whitespace (otherwise we lose syntax like nested lists) by\n // replacing all instances of the space character (but only if it's preceded by another\n // space character) by the non-breaking space character, and after processing the input\n // with Turndown, we restore the original space character.\n if (isPlainTextDocument(schema)) {\n markdownResult = markdownResult.replace(/ {2,}/g, (m) => m.replace(/ /g, '\\u00a0'))\n }\n\n // Get the serialized Markdown parsed with Turndown\n markdownResult = turndown.turndown(markdownResult)\n\n // Restore the original space character for plain-text editors (as mentioned above),\n // after Markdown serialization has been performed\n if (isPlainTextDocument(schema)) {\n markdownResult = markdownResult.replace(/\\u00a0/g, ' ')\n }\n\n // Return the serialized Markdown parsed with Turndown, and with trailing space\n // characters removed\n return markdownResult.replace(/ +$/gm, '')\n },\n }\n}\n\n/**\n * Object that holds multiple Markdown serializer instances based on a given ID.\n */\nconst markdownSerializerInstanceById: MarkdownSerializerInstanceById = {}\n\n/**\n * Returns a singleton instance of a Markdown serializer based on the provided editor schema.\n *\n * @param schema The editor schema connected to the Markdown serializer instance.\n *\n * @returns The Markdown serializer instance for the given editor schema.\n */\nfunction getMarkdownSerializerInstance(schema: Schema) {\n const id = computeSchemaId(schema)\n\n if (!markdownSerializerInstanceById[id]) {\n markdownSerializerInstanceById[id] = createMarkdownSerializer(schema)\n }\n\n return markdownSerializerInstanceById[id]\n}\n\nexport { BULLET_LIST_MARKER, createMarkdownSerializer, getMarkdownSerializerInstance }\n\nexport type { MarkdownSerializerReturnType }\n"],"mappings":";;;;;;;;;;;;;;;AA8CA,MAAM,2BAA6C;CAC/C,cAAc;CACd,IAAI;CACJ,kBAAA;CACA,gBAAgB;CAChB,OAAO;CACP,aAAa;CACb,iBAAiB;CACjB,WAAW;;;;;;CAMX,iBAAiB,GAAG,MAAM;EACtB,MAAM,aAAa,KAAK;EAGxB,IAAI,KAAK,aAAa,QAAS,YAAY,aAAa,QAAQ,KAAK,aAAa,MAC9E,OAAO;EAIX,IAAI,KAAK,aAAa,QAAS,YAAY,aAAa,QAAQ,KAAK,aAAa,MAAO;GACrF,MAAM,QACF,KAAK,aAAa,OACZ,WAAW,aAAa,OAAO,IAC/B,KAAK,aAAa,OAAO;GACnC,MAAM,QAAQ,MAAM,UAAU,QAAQ,KAAK,WAAW,UAAU,IAAI;GAEpE,OAAO,GAAG,QAAQ,OAAO,KAAK,IAAI,QAAQ,QAAQ,EAAE;EACxD;EAGA,OAAO,KAAK,UAAU,SAAS;CACnC;AACJ;;;;;;;;;;;;;;AAeA,SAAS,yBAAyB,QAA8C;CAE5E,MAAM,WAAW,IAAI,SAAS,wBAAwB;CAMtD,IAAI,oBAAoB,MAAM,GAC1B,SAAS,UAAU,QAAQ;MAQ3B,SAAS,UAAU,QAAQ;EACvB,OACI,IAKK,QAAQ,IAAI,OAAO,QAAQ,kBAAkB,OAAO,IAAI,GAAG,GAAG,MAAM,CAAC,CAOrE,QAAQ,oBAAoB,SAAS;CAElD;CAKJ,SAAS,IAAI,UAAU,OAAO,MAAM,WAAW,oBAAoB,MAAM,CAAC,CAAC;CAG3E,IAAI,OAAO,MAAM,OACb,SAAS,IAAI,MAAM,OAAO,MAAM,KAAK,CAAC;CAI1C,KAAK,OAAO,MAAM,cAAc,OAAO,MAAM,gBAAgB,OAAO,MAAM,UACtE,SAAS,IAAI,SAAS,OAAO,MAAM,QAAQ,CAAC;CAIhD,IAAI,OAAO,MAAM,QACb,SAAS,IAAI,cAAc,OAAO,MAAM,MAAM,CAAC;CAInD,IAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UACtC,SAAS,IAAI,SAAS,OAAO,MAAM,QAAQ,CAAC;CAIhD,mBAAmB,MAAM,CAAC,CAAC,SAAS,mBAAmB;EACnD,SAAS,IAAI,WAAW,cAAc,CAAC;CAC3C,CAAC;CAGD,OAAO,EACH,UAAU,MAAc;EACpB,IAAI,iBAAiB;EASrB,IAAI,oBAAoB,MAAM,GAC1B,iBAAiB,eAAe,QAAQ,WAAW,MAAM,EAAE,QAAQ,MAAM,MAAQ,CAAC;EAItF,iBAAiB,SAAS,SAAS,cAAc;EAIjD,IAAI,oBAAoB,MAAM,GAC1B,iBAAiB,eAAe,QAAQ,WAAW,GAAG;EAK1D,OAAO,eAAe,QAAQ,SAAS,EAAE;CAC7C,EACJ;AACJ;;;;AAKA,MAAM,iCAAiE,CAAC;;;;;;;;AASxE,SAAS,8BAA8B,QAAgB;CACnD,MAAM,KAAK,gBAAgB,MAAM;CAEjC,IAAI,CAAC,+BAA+B,KAChC,+BAA+B,MAAM,yBAAyB,MAAM;CAGxE,OAAO,+BAA+B;AAC1C"}
1
+ {"version":3,"file":"markdown.js","names":[],"sources":["../../../src/serializers/markdown/markdown.ts"],"sourcesContent":["import Turndown from 'turndown'\n\nimport { REGEX_PUNCTUATION } from '../../constants/regular-expressions'\nimport { computeSchemaId, isPlainTextDocument } from '../../helpers/schema'\nimport { getSuggestionNodes } from '../../helpers/serializer'\n\nimport { image } from './plugins/image'\nimport { listItem } from './plugins/list-item'\nimport { paragraph } from './plugins/paragraph'\nimport { strikethrough } from './plugins/strikethrough'\nimport { suggestion } from './plugins/suggestion'\nimport { table } from './plugins/table'\nimport { taskItem } from './plugins/task-item'\n\nimport type { Schema } from '@tiptap/pm/model'\n\n/**\n * The return type for the `createMarkdownSerializer` function.\n */\ntype MarkdownSerializerReturnType = {\n /**\n * Serializes an input HTML string to an output Markdown string.\n *\n * @param html The HTML string to serialize.\n *\n * @returns The serialized Markdown.\n */\n serialize: (html: string) => string\n}\n\n/**\n * The type for the object that holds multiple Markdown serializer instances.\n */\ntype MarkdownSerializerInstanceById = {\n [id: string]: MarkdownSerializerReturnType\n}\n\n/**\n * The bullet list marker for both standard and task list items.\n */\nconst BULLET_LIST_MARKER = '-'\n\n/**\n * Sensible default options to initialize the Turndown with.\n *\n * @see https://github.com/mixmark-io/turndown#options\n */\nconst INITIAL_TURNDOWN_OPTIONS: Turndown.Options = {\n headingStyle: 'atx',\n hr: '---',\n bulletListMarker: BULLET_LIST_MARKER,\n codeBlockStyle: 'fenced',\n fence: '```',\n emDelimiter: '*',\n strongDelimiter: '**',\n linkStyle: 'inlined',\n /**\n * Special rule to handle blank elements (overrides EVERY rule).\n *\n * @see https://github.com/mixmark-io/turndown#special-rules\n */\n blankReplacement(_, node) {\n const parentNode = node.parentNode as HTMLElement\n\n // Return the list marker for empty bullet list items\n if (node.nodeName === 'UL' || (parentNode?.nodeName === 'UL' && node.nodeName === 'LI')) {\n return `${BULLET_LIST_MARKER} \\n`\n }\n\n // Return the list marker for empty ordered list items\n if (node.nodeName === 'OL' || (parentNode?.nodeName === 'OL' && node.nodeName === 'LI')) {\n const start =\n node.nodeName === 'LI'\n ? parentNode.getAttribute('start')\n : node.getAttribute('start')\n const index = Array.prototype.indexOf.call(parentNode.children, node)\n\n return `${start ? Number(start) + index : index + 1}. \\n`\n }\n\n // @ts-ignore: The `Turndown.Node` type does not include `isBlock`\n return node.isBlock ? '\\n\\n' : ''\n },\n}\n\n/**\n * Create an HTML to Markdown serializer with the Turndown library for both a rich-text editor, and\n * a plain-text editor. The editor schema is used to detect which nodes and marks are available in\n * the editor, and only parses the input with the minimal required rules.\n *\n * **Note:** Unlike the HTML serializer, built-in rules that are not supported by the schema are not\n * disabled because if the schema does not support certain nodes/marks, the parsing rules don't have\n * valid HTML elements to match in the editor HTML output.\n *\n * @param schema The editor schema to be used for nodes and marks detection.\n *\n * @returns A normalized object for the Markdown serializer.\n */\nfunction createMarkdownSerializer(schema: Schema): MarkdownSerializerReturnType {\n // Initialize Turndown with custom options\n const turndown = new Turndown(INITIAL_TURNDOWN_OPTIONS)\n\n // Turndown ensures Markdown characters are escaped (i.e. `\\`) by default, so they are not\n // interpreted as Markdown when the output is compiled back to HTML. However, for plain-text\n // editors, we need to override the `escape` function to return the input as-is (effectively\n // disabling the escaping behaviour), so that all characters are interpreted as Markdown.\n if (isPlainTextDocument(schema)) {\n turndown.escape = (str) => str\n }\n\n // As for rich-text editors, we need to override the built-in escaping behaviour with a custom\n // implementation to suit our requirements. Please note that the `escape` function takes the\n // text content of each HTML element, with the exception of code elements, so we can be sure\n // that the escaping behaviour will only touch relevant Markdown characters.\n else {\n turndown.escape = (str) => {\n return (\n str\n // Escape all backslash characters that precede any punctuation marks, to\n // prevent the backslash itself from being interpreted as an escape sequence\n // for the subsequent character. It's important to apply this rule first to\n // avoid double escaping.\n .replace(new RegExp(`(\\\\\\\\${REGEX_PUNCTUATION.source})`, 'g'), '\\\\$1')\n\n // Although the CommonMark specification allows for bulleted or ordered lists\n // inside other bulleted or ordered lists (i.e. `- 1. - 1. Item`), the markup\n // generated by Markdown compilers is not supported by Tiptap, and we need to\n // make sure that text context that matches the ordered list syntax is\n // correctly escaped in order to be interpreted as text.\n .replace(/^(\\d+)\\.(\\s.+|$)/, '$1\\\\.$2')\n\n // Escape text that looks like a `<br>` element so that it isn't confused\n // with the literal `<br>` elements the table plugin emits for hard breaks\n // within table cells (which the HTML serializer restores into hard breaks)\n .replace(/<(br\\s*\\/?)>/gi, '\\\\<$1>')\n )\n }\n }\n\n // Overwrite some built-in rules for handling of special behaviours\n // (see documentation for each extension for more details)\n turndown.use(paragraph(schema.nodes.paragraph, isPlainTextDocument(schema)))\n\n // Overwrite the built-in `image` rule if the corresponding node exists in the schema\n if (schema.nodes.image) {\n turndown.use(image(schema.nodes.image))\n }\n\n // Overwrite the built-in `listItem` rule if the corresponding node exists in the schema\n if ((schema.nodes.bulletList || schema.nodes.orderedList) && schema.nodes.listItem) {\n turndown.use(listItem(schema.nodes.listItem))\n }\n\n // Add a rule for `strikethrough` if the corresponding node exists in the schema\n if (schema.marks.strike) {\n turndown.use(strikethrough(schema.marks.strike))\n }\n\n // Add rules for `table` if the corresponding nodes exists in the schema\n if (\n schema.nodes.table &&\n schema.nodes.tableRow &&\n schema.nodes.tableHeader &&\n schema.nodes.tableCell\n ) {\n turndown.use(\n table({\n table: schema.nodes.table,\n tableRow: schema.nodes.tableRow,\n tableHeader: schema.nodes.tableHeader,\n tableCell: schema.nodes.tableCell,\n hardBreak: schema.nodes.hardBreak,\n }),\n )\n }\n\n // Add a rule for `taskItem` if the corresponding nodes exists in the schema\n if (schema.nodes.taskList && schema.nodes.taskItem) {\n turndown.use(taskItem(schema.nodes.taskItem))\n }\n\n // Add a custom rule for all suggestion nodes available in the schema\n getSuggestionNodes(schema).forEach((suggestionNode) => {\n turndown.use(suggestion(suggestionNode))\n })\n\n // Return a normalized `serialize` function\n return {\n serialize(html: string) {\n let markdownResult = html\n\n // Turndown was built to convert HTML into Markdown, expecting the input to be\n // standard-compliant HTML. As such, it collapses all whitespace by default, and there's\n // currently no way to opt-out of this behavior. However, for plain-text editors, we\n // need to preserve Markdown whitespace (otherwise we lose syntax like nested lists) by\n // replacing all instances of the space character (but only if it's preceded by another\n // space character) by the non-breaking space character, and after processing the input\n // with Turndown, we restore the original space character.\n if (isPlainTextDocument(schema)) {\n markdownResult = markdownResult.replace(/ {2,}/g, (m) => m.replace(/ /g, '\\u00a0'))\n }\n\n // Get the serialized Markdown parsed with Turndown\n markdownResult = turndown.turndown(markdownResult)\n\n // Restore the original space character for plain-text editors (as mentioned above),\n // after Markdown serialization has been performed\n if (isPlainTextDocument(schema)) {\n markdownResult = markdownResult.replace(/\\u00a0/g, ' ')\n }\n\n // Return the serialized Markdown parsed with Turndown, and with trailing space\n // characters removed\n return markdownResult.replace(/ +$/gm, '')\n },\n }\n}\n\n/**\n * Object that holds multiple Markdown serializer instances based on a given ID.\n */\nconst markdownSerializerInstanceById: MarkdownSerializerInstanceById = {}\n\n/**\n * Returns a singleton instance of a Markdown serializer based on the provided editor schema.\n *\n * @param schema The editor schema connected to the Markdown serializer instance.\n *\n * @returns The Markdown serializer instance for the given editor schema.\n */\nfunction getMarkdownSerializerInstance(schema: Schema) {\n const id = computeSchemaId(schema)\n\n if (!markdownSerializerInstanceById[id]) {\n markdownSerializerInstanceById[id] = createMarkdownSerializer(schema)\n }\n\n return markdownSerializerInstanceById[id]\n}\n\nexport { BULLET_LIST_MARKER, createMarkdownSerializer, getMarkdownSerializerInstance }\n\nexport type { MarkdownSerializerReturnType }\n"],"mappings":";;;;;;;;;;;;;;;;AA+CA,MAAM,2BAA6C;CAC/C,cAAc;CACd,IAAI;CACJ,kBAAA;CACA,gBAAgB;CAChB,OAAO;CACP,aAAa;CACb,iBAAiB;CACjB,WAAW;;;;;;CAMX,iBAAiB,GAAG,MAAM;EACtB,MAAM,aAAa,KAAK;EAGxB,IAAI,KAAK,aAAa,QAAS,YAAY,aAAa,QAAQ,KAAK,aAAa,MAC9E,OAAO;EAIX,IAAI,KAAK,aAAa,QAAS,YAAY,aAAa,QAAQ,KAAK,aAAa,MAAO;GACrF,MAAM,QACF,KAAK,aAAa,OACZ,WAAW,aAAa,OAAO,IAC/B,KAAK,aAAa,OAAO;GACnC,MAAM,QAAQ,MAAM,UAAU,QAAQ,KAAK,WAAW,UAAU,IAAI;GAEpE,OAAO,GAAG,QAAQ,OAAO,KAAK,IAAI,QAAQ,QAAQ,EAAE;EACxD;EAGA,OAAO,KAAK,UAAU,SAAS;CACnC;AACJ;;;;;;;;;;;;;;AAeA,SAAS,yBAAyB,QAA8C;CAE5E,MAAM,WAAW,IAAI,SAAS,wBAAwB;CAMtD,IAAI,oBAAoB,MAAM,GAC1B,SAAS,UAAU,QAAQ;MAQ3B,SAAS,UAAU,QAAQ;EACvB,OACI,IAKK,QAAQ,IAAI,OAAO,QAAQ,kBAAkB,OAAO,IAAI,GAAG,GAAG,MAAM,CAAC,CAOrE,QAAQ,oBAAoB,SAAS,CAAC,CAKtC,QAAQ,kBAAkB,QAAQ;CAE/C;CAKJ,SAAS,IAAI,UAAU,OAAO,MAAM,WAAW,oBAAoB,MAAM,CAAC,CAAC;CAG3E,IAAI,OAAO,MAAM,OACb,SAAS,IAAI,MAAM,OAAO,MAAM,KAAK,CAAC;CAI1C,KAAK,OAAO,MAAM,cAAc,OAAO,MAAM,gBAAgB,OAAO,MAAM,UACtE,SAAS,IAAI,SAAS,OAAO,MAAM,QAAQ,CAAC;CAIhD,IAAI,OAAO,MAAM,QACb,SAAS,IAAI,cAAc,OAAO,MAAM,MAAM,CAAC;CAInD,IACI,OAAO,MAAM,SACb,OAAO,MAAM,YACb,OAAO,MAAM,eACb,OAAO,MAAM,WAEb,SAAS,IACL,MAAM;EACF,OAAO,OAAO,MAAM;EACpB,UAAU,OAAO,MAAM;EACvB,aAAa,OAAO,MAAM;EAC1B,WAAW,OAAO,MAAM;EACxB,WAAW,OAAO,MAAM;CAC5B,CAAC,CACL;CAIJ,IAAI,OAAO,MAAM,YAAY,OAAO,MAAM,UACtC,SAAS,IAAI,SAAS,OAAO,MAAM,QAAQ,CAAC;CAIhD,mBAAmB,MAAM,CAAC,CAAC,SAAS,mBAAmB;EACnD,SAAS,IAAI,WAAW,cAAc,CAAC;CAC3C,CAAC;CAGD,OAAO,EACH,UAAU,MAAc;EACpB,IAAI,iBAAiB;EASrB,IAAI,oBAAoB,MAAM,GAC1B,iBAAiB,eAAe,QAAQ,WAAW,MAAM,EAAE,QAAQ,MAAM,MAAQ,CAAC;EAItF,iBAAiB,SAAS,SAAS,cAAc;EAIjD,IAAI,oBAAoB,MAAM,GAC1B,iBAAiB,eAAe,QAAQ,WAAW,GAAG;EAK1D,OAAO,eAAe,QAAQ,SAAS,EAAE;CAC7C,EACJ;AACJ;;;;AAKA,MAAM,iCAAiE,CAAC;;;;;;;;AASxE,SAAS,8BAA8B,QAAgB;CACnD,MAAM,KAAK,gBAAgB,MAAM;CAEjC,IAAI,CAAC,+BAA+B,KAChC,+BAA+B,MAAM,yBAAyB,MAAM;CAGxE,OAAO,+BAA+B;AAC1C"}
@@ -0,0 +1,113 @@
1
+ import { extractTagsFromParseRules } from "../../../helpers/serializer.js";
2
+ //#region src/serializers/markdown/plugins/table.ts
3
+ /**
4
+ * Serializes the content of a table cell delimited by the GFM table syntax. Since the GFM table
5
+ * syntax cannot represent multiple lines or blocks within a cell, all inner whitespace (including
6
+ * newlines from block content) is collapsed into a single space character, and pipe characters are
7
+ * escaped so they are not interpreted as cell delimiters.
8
+ *
9
+ * @param content The serialized Markdown content of the table cell.
10
+ * @param node The table cell element that matches this rule.
11
+ *
12
+ * @returns The serialized Markdown for the table cell.
13
+ */
14
+ function serializeTableCell(content, node) {
15
+ return `${node.previousElementSibling ? " " : "| "}${content.replace(/\s*\n\s*/g, " ").replace(/\|/g, "\\|").trim()} |`;
16
+ }
17
+ /**
18
+ * Checks if the given table row is a heading row, that is, a row inside a `<thead>` element, or
19
+ * the very first row of a table in which every cell is a header cell. The first row is looked up
20
+ * with the `rows` collection (instead of sibling-based checks), since it only contains the table
21
+ * rows in document order, regardless of intermingled `<colgroup>`/`<caption>` elements (Tiptap
22
+ * always renders a `<colgroup>` element before the table rows).
23
+ *
24
+ * @param row The table row element to check.
25
+ * @param headerTags The HTML tags that match a header cell.
26
+ *
27
+ * @returns `true` if the table row is a heading row, `false` otherwise.
28
+ */
29
+ function isHeadingRow(row, headerTags) {
30
+ const rowElement = row;
31
+ if (rowElement.parentNode?.nodeName === "THEAD") return true;
32
+ return rowElement.closest("table")?.rows[0] === rowElement && Array.from(rowElement.children).every((cell) => headerTags.some((tag) => tag.toUpperCase() === cell.nodeName));
33
+ }
34
+ /**
35
+ * A Turndown plugin which adds rules for tables following the GitHub Flavored Markdown (GFM)
36
+ * specification, based on the original third-party plugin. Unlike the original plugin, which keeps
37
+ * tables without a heading row as raw HTML, this implementation adds an empty heading row instead,
38
+ * since the GFM table syntax requires one.
39
+ *
40
+ * @see https://github.com/mixmark-io/turndown-plugin-gfm/blob/v1.0.1/src/tables.js
41
+ *
42
+ * @param nodeTypes The node objects that match the table rules.
43
+ */
44
+ function table(nodeTypes) {
45
+ const tableTags = extractTagsFromParseRules(nodeTypes.table.spec.parseDOM);
46
+ const rowTags = extractTagsFromParseRules(nodeTypes.tableRow.spec.parseDOM);
47
+ const headerTags = extractTagsFromParseRules(nodeTypes.tableHeader.spec.parseDOM);
48
+ const cellTags = extractTagsFromParseRules(nodeTypes.tableCell.spec.parseDOM);
49
+ return (turndown) => {
50
+ turndown.addRule(nodeTypes.table.name, {
51
+ filter(node) {
52
+ return tableTags.some((tag) => tag.toUpperCase() === node.nodeName);
53
+ },
54
+ replacement(content, node) {
55
+ const rows = content.replace(/\n+/g, "\n").trim();
56
+ let headingRows = "";
57
+ const firstRow = node.rows?.[0];
58
+ if (firstRow && !isHeadingRow(firstRow, headerTags)) {
59
+ const cells = Array.from(firstRow.children);
60
+ headingRows = `${cells.map((cell) => serializeTableCell("", cell)).join("")}\n${cells.map((cell) => serializeTableCell("---", cell)).join("")}\n`;
61
+ }
62
+ return `\n\n${headingRows}${rows}\n\n`;
63
+ }
64
+ });
65
+ turndown.addRule(nodeTypes.tableRow.name, {
66
+ filter(node) {
67
+ return rowTags.some((tag) => tag.toUpperCase() === node.nodeName);
68
+ },
69
+ replacement(content, node) {
70
+ let delimiterRow = "";
71
+ if (isHeadingRow(node, headerTags)) delimiterRow = `\n${Array.from(node.children).map((cell) => serializeTableCell("---", cell)).join("")}`;
72
+ return `\n${content}${delimiterRow}`;
73
+ }
74
+ });
75
+ turndown.addRule(nodeTypes.tableHeader.name, {
76
+ filter(node) {
77
+ return headerTags.some((tag) => tag.toUpperCase() === node.nodeName);
78
+ },
79
+ replacement: serializeTableCell
80
+ });
81
+ turndown.addRule(nodeTypes.tableCell.name, {
82
+ filter(node) {
83
+ return cellTags.some((tag) => tag.toUpperCase() === node.nodeName);
84
+ },
85
+ replacement: serializeTableCell
86
+ });
87
+ turndown.addRule("tableSection", {
88
+ filter: [
89
+ "thead",
90
+ "tbody",
91
+ "tfoot"
92
+ ],
93
+ replacement(content) {
94
+ return content;
95
+ }
96
+ });
97
+ if (nodeTypes.hardBreak) {
98
+ const cellSelector = [...headerTags, ...cellTags].join(", ");
99
+ turndown.addRule("tableCellHardBreak", {
100
+ filter(node) {
101
+ return node.nodeName === "BR" && node.closest(cellSelector) !== null;
102
+ },
103
+ replacement() {
104
+ return "<br>";
105
+ }
106
+ });
107
+ }
108
+ };
109
+ }
110
+ //#endregion
111
+ export { table };
112
+
113
+ //# sourceMappingURL=table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"table.js","names":[],"sources":["../../../../src/serializers/markdown/plugins/table.ts"],"sourcesContent":["import { extractTagsFromParseRules } from '../../../helpers/serializer'\n\nimport type { NodeType } from '@tiptap/pm/model'\nimport type Turndown from 'turndown'\n\n/**\n * The node types required by the table rules for tags and names detection.\n *\n * The `hardBreak` node type is optional because hard breaks may not be available in the editor\n * schema, in which case hard breaks within table cells are serialized with the built-in Turndown\n * rule.\n */\ntype TableNodeTypes = {\n table: NodeType\n tableRow: NodeType\n tableHeader: NodeType\n tableCell: NodeType\n hardBreak?: NodeType\n}\n\n/**\n * Serializes the content of a table cell delimited by the GFM table syntax. Since the GFM table\n * syntax cannot represent multiple lines or blocks within a cell, all inner whitespace (including\n * newlines from block content) is collapsed into a single space character, and pipe characters are\n * escaped so they are not interpreted as cell delimiters.\n *\n * @param content The serialized Markdown content of the table cell.\n * @param node The table cell element that matches this rule.\n *\n * @returns The serialized Markdown for the table cell.\n */\nfunction serializeTableCell(content: string, node: Node): string {\n // Only the first cell opens the row with a leading pipe, while every cell (this one\n // included) closes itself with a trailing pipe, forming the full row delimiters\n const prefix = (node as Element).previousElementSibling ? ' ' : '| '\n\n const cellContent = content\n .replace(/\\s*\\n\\s*/g, ' ')\n .replace(/\\|/g, '\\\\|')\n .trim()\n\n return `${prefix}${cellContent} |`\n}\n\n/**\n * Checks if the given table row is a heading row, that is, a row inside a `<thead>` element, or\n * the very first row of a table in which every cell is a header cell. The first row is looked up\n * with the `rows` collection (instead of sibling-based checks), since it only contains the table\n * rows in document order, regardless of intermingled `<colgroup>`/`<caption>` elements (Tiptap\n * always renders a `<colgroup>` element before the table rows).\n *\n * @param row The table row element to check.\n * @param headerTags The HTML tags that match a header cell.\n *\n * @returns `true` if the table row is a heading row, `false` otherwise.\n */\nfunction isHeadingRow(row: Node, headerTags: string[]): boolean {\n const rowElement = row as HTMLTableRowElement\n\n if (rowElement.parentNode?.nodeName === 'THEAD') {\n return true\n }\n\n const tableElement = rowElement.closest('table')\n\n return (\n tableElement?.rows[0] === rowElement &&\n Array.from(rowElement.children).every((cell) =>\n headerTags.some((tag) => tag.toUpperCase() === cell.nodeName),\n )\n )\n}\n\n/**\n * A Turndown plugin which adds rules for tables following the GitHub Flavored Markdown (GFM)\n * specification, based on the original third-party plugin. Unlike the original plugin, which keeps\n * tables without a heading row as raw HTML, this implementation adds an empty heading row instead,\n * since the GFM table syntax requires one.\n *\n * @see https://github.com/mixmark-io/turndown-plugin-gfm/blob/v1.0.1/src/tables.js\n *\n * @param nodeTypes The node objects that match the table rules.\n */\nfunction table(nodeTypes: TableNodeTypes): Turndown.Plugin {\n const tableTags = extractTagsFromParseRules(nodeTypes.table.spec.parseDOM)\n const rowTags = extractTagsFromParseRules(nodeTypes.tableRow.spec.parseDOM)\n const headerTags = extractTagsFromParseRules(nodeTypes.tableHeader.spec.parseDOM)\n const cellTags = extractTagsFromParseRules(nodeTypes.tableCell.spec.parseDOM)\n\n return (turndown: Turndown) => {\n turndown.addRule(nodeTypes.table.name, {\n filter(node) {\n return tableTags.some((tag) => tag.toUpperCase() === node.nodeName)\n },\n replacement(content, node) {\n // Make sure there is exactly one newline between every table row\n const rows = content.replace(/\\n+/g, '\\n').trim()\n\n let headingRows = ''\n\n const firstRow = (node as HTMLTableElement).rows?.[0]\n\n // The GFM table syntax requires a heading row, so when the table doesn't have\n // one, an empty heading row (followed by the required delimiter row) is added,\n // with as many cells as the first row of the table\n if (firstRow && !isHeadingRow(firstRow, headerTags)) {\n const cells = Array.from(firstRow.children)\n const emptyRow = cells.map((cell) => serializeTableCell('', cell)).join('')\n const delimiterRow = cells\n .map((cell) => serializeTableCell('---', cell))\n .join('')\n\n headingRows = `${emptyRow}\\n${delimiterRow}\\n`\n }\n\n return `\\n\\n${headingRows}${rows}\\n\\n`\n },\n })\n\n turndown.addRule(nodeTypes.tableRow.name, {\n filter(node) {\n return rowTags.some((tag) => tag.toUpperCase() === node.nodeName)\n },\n replacement(content, node) {\n let delimiterRow = ''\n\n // The GFM table syntax requires a delimiter row (i.e. `| --- |`) between the\n // heading row and the table body, so one is appended to the heading row\n if (isHeadingRow(node, headerTags)) {\n delimiterRow = `\\n${Array.from(node.children)\n .map((cell) => serializeTableCell('---', cell))\n .join('')}`\n }\n\n return `\\n${content}${delimiterRow}`\n },\n })\n\n turndown.addRule(nodeTypes.tableHeader.name, {\n filter(node) {\n return headerTags.some((tag) => tag.toUpperCase() === node.nodeName)\n },\n replacement: serializeTableCell,\n })\n\n turndown.addRule(nodeTypes.tableCell.name, {\n filter(node) {\n return cellTags.some((tag) => tag.toUpperCase() === node.nodeName)\n },\n replacement: serializeTableCell,\n })\n\n // Table sections have no Markdown representation, so they are unwrapped, keeping only\n // their serialized rows\n turndown.addRule('tableSection', {\n filter: ['thead', 'tbody', 'tfoot'],\n replacement(content) {\n return content\n },\n })\n\n // Serialize hard breaks inside table cells as literal `<br>` elements (instead of the\n // built-in trailing double space followed by a newline), since the GFM table syntax\n // cannot represent multiple lines within a cell, while raw `<br>` elements are rendered\n // as line breaks by GFM, and parsed back into hard breaks by the HTML serializer (the\n // rule is skipped when the schema doesn't support hard breaks, matching the HTML\n // serializer, which only restores `<br>` elements when the schema supports them)\n if (nodeTypes.hardBreak) {\n const cellSelector = [...headerTags, ...cellTags].join(', ')\n\n turndown.addRule('tableCellHardBreak', {\n filter(node) {\n return node.nodeName === 'BR' && node.closest(cellSelector) !== null\n },\n replacement() {\n return '<br>'\n },\n })\n }\n }\n}\n\nexport { table }\n"],"mappings":";;;;;;;;;;;;;AA+BA,SAAS,mBAAmB,SAAiB,MAAoB;CAU7D,OAAO,GAPS,KAAiB,yBAAyB,MAAM,OAE5C,QACf,QAAQ,aAAa,GAAG,CAAC,CACzB,QAAQ,OAAO,KAAK,CAAC,CACrB,KAEwB,EAAE;AACnC;;;;;;;;;;;;;AAcA,SAAS,aAAa,KAAW,YAA+B;CAC5D,MAAM,aAAa;CAEnB,IAAI,WAAW,YAAY,aAAa,SACpC,OAAO;CAKX,OAFqB,WAAW,QAAQ,OAGzB,CAAC,EAAE,KAAK,OAAO,cAC1B,MAAM,KAAK,WAAW,QAAQ,CAAC,CAAC,OAAO,SACnC,WAAW,MAAM,QAAQ,IAAI,YAAY,MAAM,KAAK,QAAQ,CAChE;AAER;;;;;;;;;;;AAYA,SAAS,MAAM,WAA4C;CACvD,MAAM,YAAY,0BAA0B,UAAU,MAAM,KAAK,QAAQ;CACzE,MAAM,UAAU,0BAA0B,UAAU,SAAS,KAAK,QAAQ;CAC1E,MAAM,aAAa,0BAA0B,UAAU,YAAY,KAAK,QAAQ;CAChF,MAAM,WAAW,0BAA0B,UAAU,UAAU,KAAK,QAAQ;CAE5E,QAAQ,aAAuB;EAC3B,SAAS,QAAQ,UAAU,MAAM,MAAM;GACnC,OAAO,MAAM;IACT,OAAO,UAAU,MAAM,QAAQ,IAAI,YAAY,MAAM,KAAK,QAAQ;GACtE;GACA,YAAY,SAAS,MAAM;IAEvB,MAAM,OAAO,QAAQ,QAAQ,QAAQ,IAAI,CAAC,CAAC,KAAK;IAEhD,IAAI,cAAc;IAElB,MAAM,WAAY,KAA0B,OAAO;IAKnD,IAAI,YAAY,CAAC,aAAa,UAAU,UAAU,GAAG;KACjD,MAAM,QAAQ,MAAM,KAAK,SAAS,QAAQ;KAM1C,cAAc,GALG,MAAM,KAAK,SAAS,mBAAmB,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,EAKhD,EAAE,IAJL,MAChB,KAAK,SAAS,mBAAmB,OAAO,IAAI,CAAC,CAAC,CAC9C,KAAK,EAE+B,EAAE;IAC/C;IAEA,OAAO,OAAO,cAAc,KAAK;GACrC;EACJ,CAAC;EAED,SAAS,QAAQ,UAAU,SAAS,MAAM;GACtC,OAAO,MAAM;IACT,OAAO,QAAQ,MAAM,QAAQ,IAAI,YAAY,MAAM,KAAK,QAAQ;GACpE;GACA,YAAY,SAAS,MAAM;IACvB,IAAI,eAAe;IAInB,IAAI,aAAa,MAAM,UAAU,GAC7B,eAAe,KAAK,MAAM,KAAK,KAAK,QAAQ,CAAC,CACxC,KAAK,SAAS,mBAAmB,OAAO,IAAI,CAAC,CAAC,CAC9C,KAAK,EAAE;IAGhB,OAAO,KAAK,UAAU;GAC1B;EACJ,CAAC;EAED,SAAS,QAAQ,UAAU,YAAY,MAAM;GACzC,OAAO,MAAM;IACT,OAAO,WAAW,MAAM,QAAQ,IAAI,YAAY,MAAM,KAAK,QAAQ;GACvE;GACA,aAAa;EACjB,CAAC;EAED,SAAS,QAAQ,UAAU,UAAU,MAAM;GACvC,OAAO,MAAM;IACT,OAAO,SAAS,MAAM,QAAQ,IAAI,YAAY,MAAM,KAAK,QAAQ;GACrE;GACA,aAAa;EACjB,CAAC;EAID,SAAS,QAAQ,gBAAgB;GAC7B,QAAQ;IAAC;IAAS;IAAS;GAAO;GAClC,YAAY,SAAS;IACjB,OAAO;GACX;EACJ,CAAC;EAQD,IAAI,UAAU,WAAW;GACrB,MAAM,eAAe,CAAC,GAAG,YAAY,GAAG,QAAQ,CAAC,CAAC,KAAK,IAAI;GAE3D,SAAS,QAAQ,sBAAsB;IACnC,OAAO,MAAM;KACT,OAAO,KAAK,aAAa,QAAQ,KAAK,QAAQ,YAAY,MAAM;IACpE;IACA,cAAc;KACV,OAAO;IACX;GACJ,CAAC;EACL;CACJ;AACJ"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@doist/typist",
3
3
  "description": "The mighty Tiptap-based rich-text editor React component that powers Doist products.",
4
- "version": "13.0.1",
4
+ "version": "14.0.0",
5
5
  "license": "MIT",
6
6
  "homepage": "https://typist.doist.dev/",
7
7
  "repository": {
@@ -79,6 +79,10 @@
79
79
  "@tiptap/extension-paragraph": "2.27.2",
80
80
  "@tiptap/extension-placeholder": "2.27.2",
81
81
  "@tiptap/extension-strike": "2.27.2",
82
+ "@tiptap/extension-table": "2.27.2",
83
+ "@tiptap/extension-table-cell": "2.27.2",
84
+ "@tiptap/extension-table-header": "2.27.2",
85
+ "@tiptap/extension-table-row": "2.27.2",
82
86
  "@tiptap/extension-task-item": "2.27.2",
83
87
  "@tiptap/extension-task-list": "2.27.2",
84
88
  "@tiptap/extension-text": "2.27.2",
@@ -89,8 +93,10 @@
89
93
  "@tiptap/suggestion": "2.27.2",
90
94
  "mdast-util-gfm-autolink-literal": "2.0.1",
91
95
  "mdast-util-gfm-strikethrough": "2.0.0",
96
+ "mdast-util-gfm-table": "2.0.0",
92
97
  "micromark-extension-gfm-autolink-literal": "2.1.0",
93
98
  "micromark-extension-gfm-strikethrough": "2.1.0",
99
+ "micromark-extension-gfm-table": "2.1.1",
94
100
  "prosemirror-codemark": "0.4.2",
95
101
  "rehype-minify-whitespace": "6.0.2",
96
102
  "rehype-stringify": "10.0.1",