@doist/typist 14.1.0 → 14.1.1

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,9 @@
1
+ ## [14.1.1](https://github.com/Doist/typist/compare/v14.1.0...v14.1.1) (2026-06-17)
2
+
3
+ ### Bug Fixes
4
+
5
+ * **serializers:** preserve inline Markdown characters in suggestion labels ([#1384](https://github.com/Doist/typist/issues/1384)) ([b29bb3d](https://github.com/Doist/typist/commit/b29bb3d6a607ce1a5555e3e99e374f3e2e16ba1d))
6
+
1
7
  ## [14.1.0](https://github.com/Doist/typist/compare/v14.0.0...v14.1.0) (2026-06-12)
2
8
 
3
9
  ### Features
@@ -12,6 +12,21 @@ function getSuggestionUrlScheme(node) {
12
12
  return kebabCase(node.name.replace(/Suggestion$/, ""));
13
13
  }
14
14
  /**
15
+ * Backslash-escapes the inline Markdown characters in a suggestion label so that a serialized
16
+ * suggestion link (e.g. `[label](mention://id)`) parses back to the exact same label. Without it,
17
+ * characters such as backticks or asterisks in the label would be re-interpreted as Markdown and
18
+ * truncate the label when the content is parsed again. The escaped characters are backtick code
19
+ * spans, emphasis, strikethrough, autolinks, and the link brackets themselves. The backslash is
20
+ * matched first so it is never misread as an escape sequence for the character that follows it.
21
+ *
22
+ * @param label The raw suggestion label.
23
+ *
24
+ * @returns The label with all inline Markdown characters backslash-escaped.
25
+ */
26
+ function escapeSuggestionLabel(label) {
27
+ return label.replace(/[\\`*_~[\]<]/g, "\\$&");
28
+ }
29
+ /**
15
30
  * Returns all suggestion nodes available in the given editor schema (e.g. `mentionSuggestion`,
16
31
  * `channelSuggestion`).
17
32
  *
@@ -65,6 +80,6 @@ function extractTagsFromParseRules(parseRules) {
65
80
  return parseRules.filter((rule) => rule.tag).map((rule) => rule.tag);
66
81
  }
67
82
  //#endregion
68
- export { buildSuggestionSchemaInfo, computeSuggestionTriggerCharsId, extractTagsFromParseRules, getSuggestionNodes, getSuggestionUrlScheme };
83
+ export { buildSuggestionSchemaInfo, computeSuggestionTriggerCharsId, escapeSuggestionLabel, extractTagsFromParseRules, getSuggestionNodes, getSuggestionUrlScheme };
69
84
 
70
85
  //# sourceMappingURL=serializer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"serializer.js","names":[],"sources":["../../src/helpers/serializer.ts"],"sourcesContent":["import { kebabCase } from 'lodash-es'\n\nimport { DEFAULT_SUGGESTION_TRIGGER_CHAR } from '../constants/suggestions'\n\nimport type { NodeType, ParseRule, Schema } from '@tiptap/pm/model'\n\n/**\n * Extracts the URL scheme used by a suggestion node (e.g. `mention`, `channel`) from its node\n * name (e.g. `mentionSuggestion`, `channelSuggestion`).\n *\n * @param node The suggestion node type.\n *\n * @returns The URL scheme as a kebab-case string.\n */\nfunction getSuggestionUrlScheme(node: NodeType): string {\n return kebabCase(node.name.replace(/Suggestion$/, ''))\n}\n\n/**\n * Information derived from the suggestion nodes available in the editor schema, used by the\n * HTML serializer to identify and transform suggestion links into spans.\n */\ntype SuggestionSchemaInfo = {\n /**\n * A partial regular expression that matches the URL schemes used by all the available\n * suggestion nodes (e.g. `(?:mention|channel)://`).\n */\n urlSchemeRegex: string\n\n /**\n * A map from each URL scheme (e.g. `mention`, `channel`) to its configured trigger character\n * (e.g. `@`, `#`).\n */\n triggerCharByScheme: Map<string, string>\n}\n\n/**\n * Returns all suggestion nodes available in the given editor schema (e.g. `mentionSuggestion`,\n * `channelSuggestion`).\n *\n * @param schema The editor schema to be used for suggestion nodes detection.\n *\n * @returns An array of `NodeType` objects for the available suggestion nodes.\n */\nfunction getSuggestionNodes(schema: Schema): NodeType[] {\n return Object.values(schema.nodes).filter((node) => node.name.endsWith('Suggestion'))\n}\n\n/**\n * Builds the information derived from all the suggestion nodes available in the given editor\n * schema, in a single iteration. Returns `null` if there are no suggestion nodes in the schema.\n *\n * @param schema The editor schema to be used for suggestion nodes detection.\n *\n * @returns A `SuggestionSchemaInfo` object, or `null` if there are no suggestion nodes.\n */\nfunction buildSuggestionSchemaInfo(schema: Schema): SuggestionSchemaInfo | null {\n const suggestionNodes = getSuggestionNodes(schema)\n\n if (suggestionNodes.length === 0) {\n return null\n }\n\n const triggerCharByScheme = new Map(\n suggestionNodes.map((node) => [\n getSuggestionUrlScheme(node),\n String(\n (node.spec as { triggerChar?: string }).triggerChar ??\n DEFAULT_SUGGESTION_TRIGGER_CHAR,\n ),\n ]),\n )\n\n const urlSchemes = [...triggerCharByScheme.keys()]\n\n return {\n urlSchemeRegex: `(?:${urlSchemes.join('|')})://`,\n triggerCharByScheme,\n }\n}\n\n/**\n * Computes a string ID that identifies the configured trigger characters of all the suggestion\n * nodes in the given editor schema. Used to discriminate cache keys for serializers whose output\n * depends on the trigger character (e.g. the HTML serializer).\n *\n * @param schema The current editor document schema.\n *\n * @returns A string ID matching the suggestion trigger characters in the schema.\n */\nfunction computeSuggestionTriggerCharsId(schema: Schema): string {\n const suggestionSchemaInfo = buildSuggestionSchemaInfo(schema)\n\n if (!suggestionSchemaInfo) {\n return ''\n }\n\n return [...suggestionSchemaInfo.triggerCharByScheme]\n .map(([scheme, triggerChar]) => `${scheme}=${triggerChar}`)\n .join()\n}\n\n/**\n * Extract all tags from the given parse rules argument, and returns an array of said tags.\n *\n * @param parseRules The parse rules for a DOM node or inline style.\n *\n * @returns An array of tags extracted from the parse rules.\n */\nfunction extractTagsFromParseRules(\n parseRules?: readonly ParseRule[],\n): (keyof HTMLElementTagNameMap)[] {\n if (!parseRules || parseRules.length === 0) {\n return []\n }\n\n return parseRules\n .filter((rule) => rule.tag)\n .map((rule) => rule.tag as keyof HTMLElementTagNameMap)\n}\n\nexport {\n buildSuggestionSchemaInfo,\n computeSuggestionTriggerCharsId,\n extractTagsFromParseRules,\n getSuggestionNodes,\n getSuggestionUrlScheme,\n}\n"],"mappings":";;;;;;;;;;AAcA,SAAS,uBAAuB,MAAwB;CACpD,OAAO,UAAU,KAAK,KAAK,QAAQ,eAAe,EAAE,CAAC;AACzD;;;;;;;;;AA4BA,SAAS,mBAAmB,QAA4B;CACpD,OAAO,OAAO,OAAO,OAAO,KAAK,CAAC,CAAC,QAAQ,SAAS,KAAK,KAAK,SAAS,YAAY,CAAC;AACxF;;;;;;;;;AAUA,SAAS,0BAA0B,QAA6C;CAC5E,MAAM,kBAAkB,mBAAmB,MAAM;CAEjD,IAAI,gBAAgB,WAAW,GAC3B,OAAO;CAGX,MAAM,sBAAsB,IAAI,IAC5B,gBAAgB,KAAK,SAAS,CAC1B,uBAAuB,IAAI,GAC3B,OACK,KAAK,KAAkC,eAAA,GAE5C,CACJ,CAAC,CACL;CAIA,OAAO;EACH,gBAAgB,MAAM,CAHN,GAAG,oBAAoB,KAAK,CAGb,CAAC,CAAC,KAAK,GAAG,EAAE;EAC3C;CACJ;AACJ;;;;;;;;;;AAWA,SAAS,gCAAgC,QAAwB;CAC7D,MAAM,uBAAuB,0BAA0B,MAAM;CAE7D,IAAI,CAAC,sBACD,OAAO;CAGX,OAAO,CAAC,GAAG,qBAAqB,mBAAmB,CAAC,CAC/C,KAAK,CAAC,QAAQ,iBAAiB,GAAG,OAAO,GAAG,aAAa,CAAC,CAC1D,KAAK;AACd;;;;;;;;AASA,SAAS,0BACL,YAC+B;CAC/B,IAAI,CAAC,cAAc,WAAW,WAAW,GACrC,OAAO,CAAC;CAGZ,OAAO,WACF,QAAQ,SAAS,KAAK,GAAG,CAAC,CAC1B,KAAK,SAAS,KAAK,GAAkC;AAC9D"}
1
+ {"version":3,"file":"serializer.js","names":[],"sources":["../../src/helpers/serializer.ts"],"sourcesContent":["import { kebabCase } from 'lodash-es'\n\nimport { DEFAULT_SUGGESTION_TRIGGER_CHAR } from '../constants/suggestions'\n\nimport type { NodeType, ParseRule, Schema } from '@tiptap/pm/model'\n\n/**\n * Extracts the URL scheme used by a suggestion node (e.g. `mention`, `channel`) from its node\n * name (e.g. `mentionSuggestion`, `channelSuggestion`).\n *\n * @param node The suggestion node type.\n *\n * @returns The URL scheme as a kebab-case string.\n */\nfunction getSuggestionUrlScheme(node: NodeType): string {\n return kebabCase(node.name.replace(/Suggestion$/, ''))\n}\n\n/**\n * Backslash-escapes the inline Markdown characters in a suggestion label so that a serialized\n * suggestion link (e.g. `[label](mention://id)`) parses back to the exact same label. Without it,\n * characters such as backticks or asterisks in the label would be re-interpreted as Markdown and\n * truncate the label when the content is parsed again. The escaped characters are backtick code\n * spans, emphasis, strikethrough, autolinks, and the link brackets themselves. The backslash is\n * matched first so it is never misread as an escape sequence for the character that follows it.\n *\n * @param label The raw suggestion label.\n *\n * @returns The label with all inline Markdown characters backslash-escaped.\n */\nfunction escapeSuggestionLabel(label: string): string {\n return label.replace(/[\\\\`*_~[\\]<]/g, '\\\\$&')\n}\n\n/**\n * Information derived from the suggestion nodes available in the editor schema, used by the\n * HTML serializer to identify and transform suggestion links into spans.\n */\ntype SuggestionSchemaInfo = {\n /**\n * A partial regular expression that matches the URL schemes used by all the available\n * suggestion nodes (e.g. `(?:mention|channel)://`).\n */\n urlSchemeRegex: string\n\n /**\n * A map from each URL scheme (e.g. `mention`, `channel`) to its configured trigger character\n * (e.g. `@`, `#`).\n */\n triggerCharByScheme: Map<string, string>\n}\n\n/**\n * Returns all suggestion nodes available in the given editor schema (e.g. `mentionSuggestion`,\n * `channelSuggestion`).\n *\n * @param schema The editor schema to be used for suggestion nodes detection.\n *\n * @returns An array of `NodeType` objects for the available suggestion nodes.\n */\nfunction getSuggestionNodes(schema: Schema): NodeType[] {\n return Object.values(schema.nodes).filter((node) => node.name.endsWith('Suggestion'))\n}\n\n/**\n * Builds the information derived from all the suggestion nodes available in the given editor\n * schema, in a single iteration. Returns `null` if there are no suggestion nodes in the schema.\n *\n * @param schema The editor schema to be used for suggestion nodes detection.\n *\n * @returns A `SuggestionSchemaInfo` object, or `null` if there are no suggestion nodes.\n */\nfunction buildSuggestionSchemaInfo(schema: Schema): SuggestionSchemaInfo | null {\n const suggestionNodes = getSuggestionNodes(schema)\n\n if (suggestionNodes.length === 0) {\n return null\n }\n\n const triggerCharByScheme = new Map(\n suggestionNodes.map((node) => [\n getSuggestionUrlScheme(node),\n String(\n (node.spec as { triggerChar?: string }).triggerChar ??\n DEFAULT_SUGGESTION_TRIGGER_CHAR,\n ),\n ]),\n )\n\n const urlSchemes = [...triggerCharByScheme.keys()]\n\n return {\n urlSchemeRegex: `(?:${urlSchemes.join('|')})://`,\n triggerCharByScheme,\n }\n}\n\n/**\n * Computes a string ID that identifies the configured trigger characters of all the suggestion\n * nodes in the given editor schema. Used to discriminate cache keys for serializers whose output\n * depends on the trigger character (e.g. the HTML serializer).\n *\n * @param schema The current editor document schema.\n *\n * @returns A string ID matching the suggestion trigger characters in the schema.\n */\nfunction computeSuggestionTriggerCharsId(schema: Schema): string {\n const suggestionSchemaInfo = buildSuggestionSchemaInfo(schema)\n\n if (!suggestionSchemaInfo) {\n return ''\n }\n\n return [...suggestionSchemaInfo.triggerCharByScheme]\n .map(([scheme, triggerChar]) => `${scheme}=${triggerChar}`)\n .join()\n}\n\n/**\n * Extract all tags from the given parse rules argument, and returns an array of said tags.\n *\n * @param parseRules The parse rules for a DOM node or inline style.\n *\n * @returns An array of tags extracted from the parse rules.\n */\nfunction extractTagsFromParseRules(\n parseRules?: readonly ParseRule[],\n): (keyof HTMLElementTagNameMap)[] {\n if (!parseRules || parseRules.length === 0) {\n return []\n }\n\n return parseRules\n .filter((rule) => rule.tag)\n .map((rule) => rule.tag as keyof HTMLElementTagNameMap)\n}\n\nexport {\n buildSuggestionSchemaInfo,\n computeSuggestionTriggerCharsId,\n escapeSuggestionLabel,\n extractTagsFromParseRules,\n getSuggestionNodes,\n getSuggestionUrlScheme,\n}\n"],"mappings":";;;;;;;;;;AAcA,SAAS,uBAAuB,MAAwB;CACpD,OAAO,UAAU,KAAK,KAAK,QAAQ,eAAe,EAAE,CAAC;AACzD;;;;;;;;;;;;;AAcA,SAAS,sBAAsB,OAAuB;CAClD,OAAO,MAAM,QAAQ,iBAAiB,MAAM;AAChD;;;;;;;;;AA4BA,SAAS,mBAAmB,QAA4B;CACpD,OAAO,OAAO,OAAO,OAAO,KAAK,CAAC,CAAC,QAAQ,SAAS,KAAK,KAAK,SAAS,YAAY,CAAC;AACxF;;;;;;;;;AAUA,SAAS,0BAA0B,QAA6C;CAC5E,MAAM,kBAAkB,mBAAmB,MAAM;CAEjD,IAAI,gBAAgB,WAAW,GAC3B,OAAO;CAGX,MAAM,sBAAsB,IAAI,IAC5B,gBAAgB,KAAK,SAAS,CAC1B,uBAAuB,IAAI,GAC3B,OACK,KAAK,KAAkC,eAAA,GAE5C,CACJ,CAAC,CACL;CAIA,OAAO;EACH,gBAAgB,MAAM,CAHN,GAAG,oBAAoB,KAAK,CAGb,CAAC,CAAC,KAAK,GAAG,EAAE;EAC3C;CACJ;AACJ;;;;;;;;;;AAWA,SAAS,gCAAgC,QAAwB;CAC7D,MAAM,uBAAuB,0BAA0B,MAAM;CAE7D,IAAI,CAAC,sBACD,OAAO;CAGX,OAAO,CAAC,GAAG,qBAAqB,mBAAmB,CAAC,CAC/C,KAAK,CAAC,QAAQ,iBAAiB,GAAG,OAAO,GAAG,aAAa,CAAC,CAC1D,KAAK;AACd;;;;;;;;AASA,SAAS,0BACL,YAC+B;CAC/B,IAAI,CAAC,cAAc,WAAW,WAAW,GACrC,OAAO,CAAC;CAGZ,OAAO,WACF,QAAQ,SAAS,KAAK,GAAG,CAAC,CAC1B,KAAK,SAAS,KAAK,GAAkC;AAC9D"}
@@ -25,7 +25,20 @@ function isHastElementNode(node, tagName) {
25
25
  function isHastTextNode(node) {
26
26
  return is(node, { type: "text" });
27
27
  }
28
+ /**
29
+ * Collects the plain text content of a hast node by concatenating the values of all its descendant
30
+ * text nodes, in document order.
31
+ *
32
+ * @param node The hast node to extract text content from.
33
+ *
34
+ * @returns The concatenated text content of the node and all its descendants.
35
+ */
36
+ function getHastTextContent(node) {
37
+ if (isHastTextNode(node)) return node.value;
38
+ if ("children" in node) return node.children.map(getHastTextContent).join("");
39
+ return "";
40
+ }
28
41
  //#endregion
29
- export { isHastElementNode, isHastTextNode };
42
+ export { getHastTextContent, isHastElementNode, isHastTextNode };
30
43
 
31
44
  //# sourceMappingURL=unified.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"unified.js","names":[],"sources":["../../src/helpers/unified.ts"],"sourcesContent":["import { is } from 'unist-util-is'\n\nimport type { Element, Node as HastNode, Text } from 'hast'\n\n/**\n * Determines whether a given hast node is an element node with a specific tag name.\n *\n * @param node The hast node to check.\n * @param tagName The tag name to check for.\n *\n * @returns `true` if the hast node is an element node with the specified tag name, `false`\n * otherwise.\n */\nfunction isHastElementNode(node: HastNode, tagName: Element['tagName']): node is Element {\n return is(node, { type: 'element', tagName })\n}\n\n/**\n * Determines whether a given hast node is a text node.\n *\n * @param node The hast node to check.\n *\n * @returns `true` if the hast node is a text node, `false` otherwise.\n */\nfunction isHastTextNode(node: HastNode): node is Text {\n return is(node, { type: 'text' })\n}\n\nexport { isHastElementNode, isHastTextNode }\n"],"mappings":";;;;;;;;;;;AAaA,SAAS,kBAAkB,MAAgB,SAA8C;CACrF,OAAO,GAAG,MAAM;EAAE,MAAM;EAAW;CAAQ,CAAC;AAChD;;;;;;;;AASA,SAAS,eAAe,MAA8B;CAClD,OAAO,GAAG,MAAM,EAAE,MAAM,OAAO,CAAC;AACpC"}
1
+ {"version":3,"file":"unified.js","names":[],"sources":["../../src/helpers/unified.ts"],"sourcesContent":["import { is } from 'unist-util-is'\n\nimport type { Element, Node as HastNode, Text } from 'hast'\n\n/**\n * Determines whether a given hast node is an element node with a specific tag name.\n *\n * @param node The hast node to check.\n * @param tagName The tag name to check for.\n *\n * @returns `true` if the hast node is an element node with the specified tag name, `false`\n * otherwise.\n */\nfunction isHastElementNode(node: HastNode, tagName: Element['tagName']): node is Element {\n return is(node, { type: 'element', tagName })\n}\n\n/**\n * Determines whether a given hast node is a text node.\n *\n * @param node The hast node to check.\n *\n * @returns `true` if the hast node is a text node, `false` otherwise.\n */\nfunction isHastTextNode(node: HastNode): node is Text {\n return is(node, { type: 'text' })\n}\n\n/**\n * Collects the plain text content of a hast node by concatenating the values of all its descendant\n * text nodes, in document order.\n *\n * @param node The hast node to extract text content from.\n *\n * @returns The concatenated text content of the node and all its descendants.\n */\nfunction getHastTextContent(node: HastNode): string {\n if (isHastTextNode(node)) {\n return node.value\n }\n\n if ('children' in node) {\n return (node.children as HastNode[]).map(getHastTextContent).join('')\n }\n\n return ''\n}\n\nexport { getHastTextContent, isHastElementNode, isHastTextNode }\n"],"mappings":";;;;;;;;;;;AAaA,SAAS,kBAAkB,MAAgB,SAA8C;CACrF,OAAO,GAAG,MAAM;EAAE,MAAM;EAAW;CAAQ,CAAC;AAChD;;;;;;;;AASA,SAAS,eAAe,MAA8B;CAClD,OAAO,GAAG,MAAM,EAAE,MAAM,OAAO,CAAC;AACpC;;;;;;;;;AAUA,SAAS,mBAAmB,MAAwB;CAChD,IAAI,eAAe,IAAI,GACnB,OAAO,KAAK;CAGhB,IAAI,cAAc,MACd,OAAQ,KAAK,SAAwB,IAAI,kBAAkB,CAAC,CAAC,KAAK,EAAE;CAGxE,OAAO;AACX"}
@@ -1,5 +1,5 @@
1
1
  import { buildSuggestionSchemaInfo } from "../../../helpers/serializer.js";
2
- import { isHastElementNode, isHastTextNode } from "../../../helpers/unified.js";
2
+ import { getHastTextContent, isHastElementNode } from "../../../helpers/unified.js";
3
3
  import { visit } from "unist-util-visit";
4
4
  //#region src/serializers/html/plugins/rehype-suggestions.ts
5
5
  /**
@@ -15,8 +15,8 @@ function rehypeSuggestions(schema) {
15
15
  visit(tree, "element", (node) => {
16
16
  if (isHastElementNode(node, "a") && suggestionSchemaRegex.test(String(node.properties?.href))) {
17
17
  const [, urlScheme, id] = /^([a-z-]+):\/\/(\S+)$/i.exec(String(node.properties?.href)) || [];
18
- if (urlScheme && id && isHastTextNode(node.children[0])) {
19
- const label = node.children[0].value;
18
+ const label = getHastTextContent(node);
19
+ if (urlScheme && id && label) {
20
20
  const triggerChar = suggestionSchemaInfo.triggerCharByScheme.get(urlScheme);
21
21
  node.tagName = "span";
22
22
  node.properties = {
@@ -24,7 +24,10 @@ function rehypeSuggestions(schema) {
24
24
  "data-id": id,
25
25
  "data-label": label
26
26
  };
27
- node.children[0].value = `${triggerChar}${label}`;
27
+ node.children = [{
28
+ type: "text",
29
+ value: `${triggerChar}${label}`
30
+ }];
28
31
  }
29
32
  }
30
33
  });
@@ -1 +1 @@
1
- {"version":3,"file":"rehype-suggestions.js","names":[],"sources":["../../../../src/serializers/html/plugins/rehype-suggestions.ts"],"sourcesContent":["import { visit } from 'unist-util-visit'\n\nimport { buildSuggestionSchemaInfo } from '../../../helpers/serializer'\nimport { isHastElementNode, isHastTextNode } from '../../../helpers/unified'\n\nimport type { Schema } from '@tiptap/pm/model'\nimport type { Node as HastNode } from 'hast'\nimport type { Transformer } from 'unified'\n\n/**\n * A rehype plugin to add support for suggestions nodes (e.g., `@username` or `#channel).\n *\n * @param schema The editor schema to be used for suggestion nodes detection.\n */\nfunction rehypeSuggestions(schema: Schema): Transformer {\n const suggestionSchemaInfo = buildSuggestionSchemaInfo(schema)\n\n // Return the tree as-is if the editor does not support suggestions\n if (!suggestionSchemaInfo) {\n return (tree: HastNode) => tree\n }\n\n const suggestionSchemaRegex = new RegExp(`^${suggestionSchemaInfo.urlSchemeRegex}`)\n\n return (...[tree]: Parameters<Transformer>): ReturnType<Transformer> => {\n visit(tree, 'element', (node: HastNode) => {\n if (\n isHastElementNode(node, 'a') &&\n suggestionSchemaRegex.test(String(node.properties?.href))\n ) {\n const [, urlScheme, id] =\n /^([a-z-]+):\\/\\/(\\S+)$/i.exec(String(node.properties?.href)) || []\n\n // Replace the link element with a span containing the suggestion attributes,\n // keeping the visible label (prefixed with the trigger character) as text content\n // so the span renders correctly when used outside of an editor.\n if (urlScheme && id && isHastTextNode(node.children[0])) {\n const label = node.children[0].value\n\n // The URL scheme was matched against the regex built from the same map of\n // suggestion nodes, so the trigger character is guaranteed to exist\n const triggerChar = suggestionSchemaInfo.triggerCharByScheme.get(\n urlScheme,\n ) as string\n\n node.tagName = 'span'\n node.properties = {\n [`data-${urlScheme}`]: '',\n 'data-id': id,\n 'data-label': label,\n }\n node.children[0].value = `${triggerChar}${label}`\n }\n }\n })\n\n return tree\n }\n}\n\nexport { rehypeSuggestions }\n"],"mappings":";;;;;;;;;AAcA,SAAS,kBAAkB,QAA6B;CACpD,MAAM,uBAAuB,0BAA0B,MAAM;CAG7D,IAAI,CAAC,sBACD,QAAQ,SAAmB;CAG/B,MAAM,wBAAwB,IAAI,OAAO,IAAI,qBAAqB,gBAAgB;CAElF,QAAQ,GAAG,CAAC,UAA4D;EACpE,MAAM,MAAM,YAAY,SAAmB;GACvC,IACI,kBAAkB,MAAM,GAAG,KAC3B,sBAAsB,KAAK,OAAO,KAAK,YAAY,IAAI,CAAC,GAC1D;IACE,MAAM,GAAG,WAAW,MAChB,yBAAyB,KAAK,OAAO,KAAK,YAAY,IAAI,CAAC,KAAK,CAAC;IAKrE,IAAI,aAAa,MAAM,eAAe,KAAK,SAAS,EAAE,GAAG;KACrD,MAAM,QAAQ,KAAK,SAAS,EAAE,CAAC;KAI/B,MAAM,cAAc,qBAAqB,oBAAoB,IACzD,SACJ;KAEA,KAAK,UAAU;KACf,KAAK,aAAa;OACb,QAAQ,cAAc;MACvB,WAAW;MACX,cAAc;KAClB;KACA,KAAK,SAAS,EAAE,CAAC,QAAQ,GAAG,cAAc;IAC9C;GACJ;EACJ,CAAC;EAED,OAAO;CACX;AACJ"}
1
+ {"version":3,"file":"rehype-suggestions.js","names":[],"sources":["../../../../src/serializers/html/plugins/rehype-suggestions.ts"],"sourcesContent":["import { visit } from 'unist-util-visit'\n\nimport { buildSuggestionSchemaInfo } from '../../../helpers/serializer'\nimport { getHastTextContent, isHastElementNode } from '../../../helpers/unified'\n\nimport type { Schema } from '@tiptap/pm/model'\nimport type { Node as HastNode } from 'hast'\nimport type { Transformer } from 'unified'\n\n/**\n * A rehype plugin to add support for suggestions nodes (e.g., `@username` or `#channel).\n *\n * @param schema The editor schema to be used for suggestion nodes detection.\n */\nfunction rehypeSuggestions(schema: Schema): Transformer {\n const suggestionSchemaInfo = buildSuggestionSchemaInfo(schema)\n\n // Return the tree as-is if the editor does not support suggestions\n if (!suggestionSchemaInfo) {\n return (tree: HastNode) => tree\n }\n\n const suggestionSchemaRegex = new RegExp(`^${suggestionSchemaInfo.urlSchemeRegex}`)\n\n return (...[tree]: Parameters<Transformer>): ReturnType<Transformer> => {\n visit(tree, 'element', (node: HastNode) => {\n if (\n isHastElementNode(node, 'a') &&\n suggestionSchemaRegex.test(String(node.properties?.href))\n ) {\n const [, urlScheme, id] =\n /^([a-z-]+):\\/\\/(\\S+)$/i.exec(String(node.properties?.href)) || []\n\n // The label is always meant to be plain text, so we flatten the full text content\n // of the link instead of reading a single child. This keeps the label intact when\n // the Markdown parser splits it into multiple inline nodes (e.g. a backtick code\n // span or emphasis within the label).\n const label = getHastTextContent(node)\n\n // Replace the link element with a span containing the suggestion attributes,\n // keeping the visible label (prefixed with the trigger character) as text content\n // so the span renders correctly when used outside of an editor.\n if (urlScheme && id && label) {\n // The URL scheme was matched against the regex built from the same map of\n // suggestion nodes, so the trigger character is guaranteed to exist\n const triggerChar = suggestionSchemaInfo.triggerCharByScheme.get(\n urlScheme,\n ) as string\n\n node.tagName = 'span'\n node.properties = {\n [`data-${urlScheme}`]: '',\n 'data-id': id,\n 'data-label': label,\n }\n node.children = [{ type: 'text', value: `${triggerChar}${label}` }]\n }\n }\n })\n\n return tree\n }\n}\n\nexport { rehypeSuggestions }\n"],"mappings":";;;;;;;;;AAcA,SAAS,kBAAkB,QAA6B;CACpD,MAAM,uBAAuB,0BAA0B,MAAM;CAG7D,IAAI,CAAC,sBACD,QAAQ,SAAmB;CAG/B,MAAM,wBAAwB,IAAI,OAAO,IAAI,qBAAqB,gBAAgB;CAElF,QAAQ,GAAG,CAAC,UAA4D;EACpE,MAAM,MAAM,YAAY,SAAmB;GACvC,IACI,kBAAkB,MAAM,GAAG,KAC3B,sBAAsB,KAAK,OAAO,KAAK,YAAY,IAAI,CAAC,GAC1D;IACE,MAAM,GAAG,WAAW,MAChB,yBAAyB,KAAK,OAAO,KAAK,YAAY,IAAI,CAAC,KAAK,CAAC;IAMrE,MAAM,QAAQ,mBAAmB,IAAI;IAKrC,IAAI,aAAa,MAAM,OAAO;KAG1B,MAAM,cAAc,qBAAqB,oBAAoB,IACzD,SACJ;KAEA,KAAK,UAAU;KACf,KAAK,aAAa;OACb,QAAQ,cAAc;MACvB,WAAW;MACX,cAAc;KAClB;KACA,KAAK,WAAW,CAAC;MAAE,MAAM;MAAQ,OAAO,GAAG,cAAc;KAAQ,CAAC;IACtE;GACJ;EACJ,CAAC;EAED,OAAO;CACX;AACJ"}
@@ -71,7 +71,7 @@ function createMarkdownSerializer(schema) {
71
71
  }));
72
72
  if (schema.nodes.taskList && schema.nodes.taskItem) turndown.use(taskItem(schema.nodes.taskItem));
73
73
  getSuggestionNodes(schema).forEach((suggestionNode) => {
74
- turndown.use(suggestion(suggestionNode));
74
+ turndown.use(suggestion(suggestionNode, isPlainTextDocument(schema)));
75
75
  });
76
76
  return { serialize(html) {
77
77
  let markdownResult = html;
@@ -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 { 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"}
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, isPlainTextDocument(schema)))\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,gBAAgB,oBAAoB,MAAM,CAAC,CAAC;CACxE,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,12 +1,13 @@
1
- import { getSuggestionUrlScheme } from "../../../helpers/serializer.js";
1
+ import { escapeSuggestionLabel, getSuggestionUrlScheme } from "../../../helpers/serializer.js";
2
2
  //#region src/serializers/markdown/plugins/suggestion.ts
3
3
  /**
4
4
  * A Turndown plugin which adds a custom rule for suggestion nodes created by the suggestion
5
5
  * extension factory function.
6
6
  *
7
7
  * @param nodeType The node object that matches this rule.
8
+ * @param isPlainText Specifies if the schema represents a plain-text document.
8
9
  */
9
- function suggestion(nodeType) {
10
+ function suggestion(nodeType, isPlainText) {
10
11
  const attributeType = getSuggestionUrlScheme(nodeType);
11
12
  return (turndown) => {
12
13
  turndown.addRule(nodeType.name, {
@@ -14,7 +15,9 @@ function suggestion(nodeType) {
14
15
  return node.hasAttribute(`data-${attributeType}`);
15
16
  },
16
17
  replacement(_, node) {
17
- return `[${String(node.getAttribute("data-label"))}](${attributeType}://${String(node.getAttribute("data-id"))})`;
18
+ const label = String(node.getAttribute("data-label"));
19
+ const id = String(node.getAttribute("data-id"));
20
+ return `[${isPlainText ? label : escapeSuggestionLabel(label)}](${attributeType}://${id})`;
18
21
  }
19
22
  });
20
23
  };
@@ -1 +1 @@
1
- {"version":3,"file":"suggestion.js","names":[],"sources":["../../../../src/serializers/markdown/plugins/suggestion.ts"],"sourcesContent":["import { getSuggestionUrlScheme } from '../../../helpers/serializer'\n\nimport type { NodeType } from '@tiptap/pm/model'\nimport type Turndown from 'turndown'\n\n/**\n * A Turndown plugin which adds a custom rule for suggestion nodes created by the suggestion\n * extension factory function.\n *\n * @param nodeType The node object that matches this rule.\n */\nfunction suggestion(nodeType: NodeType): Turndown.Plugin {\n const attributeType = getSuggestionUrlScheme(nodeType)\n\n return (turndown: Turndown) => {\n turndown.addRule(nodeType.name, {\n filter(node: Element) {\n return node.hasAttribute(`data-${attributeType}`)\n },\n replacement(_, node) {\n const label = String((node as Element).getAttribute('data-label'))\n const id = String((node as Element).getAttribute('data-id'))\n\n return `[${label}](${attributeType}://${id})`\n },\n })\n }\n}\n\nexport { suggestion }\n"],"mappings":";;;;;;;;AAWA,SAAS,WAAW,UAAqC;CACrD,MAAM,gBAAgB,uBAAuB,QAAQ;CAErD,QAAQ,aAAuB;EAC3B,SAAS,QAAQ,SAAS,MAAM;GAC5B,OAAO,MAAe;IAClB,OAAO,KAAK,aAAa,QAAQ,eAAe;GACpD;GACA,YAAY,GAAG,MAAM;IAIjB,OAAO,IAHO,OAAQ,KAAiB,aAAa,YAAY,CAGjD,EAAE,IAAI,cAAc,KAFxB,OAAQ,KAAiB,aAAa,SAAS,CAEjB,EAAE;GAC/C;EACJ,CAAC;CACL;AACJ"}
1
+ {"version":3,"file":"suggestion.js","names":[],"sources":["../../../../src/serializers/markdown/plugins/suggestion.ts"],"sourcesContent":["import { escapeSuggestionLabel, getSuggestionUrlScheme } from '../../../helpers/serializer'\n\nimport type { NodeType } from '@tiptap/pm/model'\nimport type Turndown from 'turndown'\n\n/**\n * A Turndown plugin which adds a custom rule for suggestion nodes created by the suggestion\n * extension factory function.\n *\n * @param nodeType The node object that matches this rule.\n * @param isPlainText Specifies if the schema represents a plain-text document.\n */\nfunction suggestion(nodeType: NodeType, isPlainText: boolean): Turndown.Plugin {\n const attributeType = getSuggestionUrlScheme(nodeType)\n\n return (turndown: Turndown) => {\n turndown.addRule(nodeType.name, {\n filter(node: Element) {\n return node.hasAttribute(`data-${attributeType}`)\n },\n replacement(_, node) {\n const label = String((node as Element).getAttribute('data-label'))\n const id = String((node as Element).getAttribute('data-id'))\n\n // Rich-text editors parse the label back as inline Markdown, so its Markdown\n // characters are escaped to keep the label intact across a serialize then parse\n // round-trip. Plain-text editors keep the label verbatim when parsing it back, so\n // it must not be escaped.\n const serializedLabel = isPlainText ? label : escapeSuggestionLabel(label)\n\n return `[${serializedLabel}](${attributeType}://${id})`\n },\n })\n }\n}\n\nexport { suggestion }\n"],"mappings":";;;;;;;;;AAYA,SAAS,WAAW,UAAoB,aAAuC;CAC3E,MAAM,gBAAgB,uBAAuB,QAAQ;CAErD,QAAQ,aAAuB;EAC3B,SAAS,QAAQ,SAAS,MAAM;GAC5B,OAAO,MAAe;IAClB,OAAO,KAAK,aAAa,QAAQ,eAAe;GACpD;GACA,YAAY,GAAG,MAAM;IACjB,MAAM,QAAQ,OAAQ,KAAiB,aAAa,YAAY,CAAC;IACjE,MAAM,KAAK,OAAQ,KAAiB,aAAa,SAAS,CAAC;IAQ3D,OAAO,IAFiB,cAAc,QAAQ,sBAAsB,KAAK,EAE9C,IAAI,cAAc,KAAK,GAAG;GACzD;EACJ,CAAC;CACL;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": "14.1.0",
4
+ "version": "14.1.1",
5
5
  "license": "MIT",
6
6
  "homepage": "https://typist.doist.dev/",
7
7
  "repository": {
@@ -110,14 +110,14 @@
110
110
  "unist-util-visit": "5.1.0"
111
111
  },
112
112
  "devDependencies": {
113
- "@doist/reactist": "33.0.1",
113
+ "@doist/reactist": "33.1.0",
114
114
  "@mdx-js/react": "3.1.1",
115
115
  "@semantic-release/changelog": "6.0.3",
116
116
  "@semantic-release/exec": "7.1.0",
117
117
  "@semantic-release/git": "10.0.1",
118
- "@storybook/addon-a11y": "10.4.2",
119
- "@storybook/addon-docs": "10.4.2",
120
- "@storybook/react-vite": "10.4.2",
118
+ "@storybook/addon-a11y": "10.4.4",
119
+ "@storybook/addon-docs": "10.4.4",
120
+ "@storybook/react-vite": "10.4.4",
121
121
  "@testing-library/dom": "10.4.1",
122
122
  "@testing-library/jest-dom": "6.9.1",
123
123
  "@testing-library/react": "16.3.2",
@@ -136,8 +136,8 @@
136
136
  "jsdom": "29.1.1",
137
137
  "lefthook": "2.1.9",
138
138
  "npm-run-all-next": "1.4.2",
139
- "oxfmt": "0.53.0",
140
- "oxlint": "1.68.0",
139
+ "oxfmt": "0.54.0",
140
+ "oxlint": "1.69.0",
141
141
  "publint": "0.3.21",
142
142
  "react": "18.3.1",
143
143
  "react-dom": "18.3.1",
@@ -148,8 +148,8 @@
148
148
  "rehype-raw": "7.0.0",
149
149
  "remark-gfm": "4.0.1",
150
150
  "rimraf": "6.1.3",
151
- "semantic-release": "25.0.3",
152
- "storybook": "10.4.2",
151
+ "semantic-release": "25.0.5",
152
+ "storybook": "10.4.4",
153
153
  "storybook-css-modules": "1.0.8",
154
154
  "tippy.js": "6.3.7",
155
155
  "tsdown": "0.22.2",