@lofcz/platejs-markdown 52.3.8 → 53.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/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import * as platejs2 from "platejs";
2
2
  import { Descendant, NodeKey, Nullable, OmitFirst, PluginConfig, SlateEditor, TElement, TListElement, TNodeMap, TText, Value } from "platejs";
3
3
  import { MdxJsxFlowElement, MdxJsxFlowElement as MdMdxJsxFlowElement, MdxJsxTextElement, MdxJsxTextElement as MdMdxJsxTextElement } from "mdast-util-mdx";
4
- import { Options } from "remark-stringify";
5
4
  import { Plugin } from "unified";
5
+ import { Options } from "remark-stringify";
6
6
  import { Token } from "marked";
7
7
  import baseRemarkMdx from "remark-mdx";
8
8
  import { Blockquote as MdBlockquote, Break as MdBreak, Code as MdCode, Content as MdContent, Definition as MdDefinition, Delete as MdDelete, Emphasis as MdEmphasis, FootnoteDefinition as MdFootnoteDefinition, FootnoteReference as MdFootnoteReference, Heading as MdHeading, Html as MdHtml, Image as MdImage, ImageReference as MdImageReference, InlineCode as MdInlineCode, Link as MdLink, LinkReference as MdLinkReference, List as MdList, ListItem as MdListItem, Paragraph as MdParagraph, PhrasingContent as MDPhrasingContent, Root, Root as MdRoot, RootContent as MdRootContent, Strong as MdStrong, Table as MdTable, TableCell as MdTableCell, TableRow as MdTableRow, Text as MdText, ThematicBreak as MdThematicBreak, Yaml as MdYaml } from "mdast";
@@ -40,9 +40,6 @@ declare const convertTextsSerialize: (slateTexts: readonly TText[], options: Ser
40
40
  //#region src/lib/serializer/listToMdastTree.d.ts
41
41
  declare function listToMdastTree(nodes: TListElement[], options: SerializeMdOptions, isBlock?: boolean): any;
42
42
  //#endregion
43
- //#region src/lib/serializer/serializeInlineMd.d.ts
44
- declare const serializeInlineMd: (editor: SlateEditor, options?: SerializeMdOptions) => string;
45
- //#endregion
46
43
  //#region src/lib/serializer/serializeMdWithSourceMap.d.ts
47
44
  type SelectionPath = number[];
48
45
  type MarkdownSourceMapKind = 'block' | 'blockquote' | 'code_block' | 'heading' | 'list_item' | 'media' | 'paragraph' | 'table_cell';
@@ -58,11 +55,36 @@ type MarkdownSourceMapSegment = {
58
55
  text: string;
59
56
  };
60
57
  type SerializeMdSourceMapResult = {
58
+ /** All segments including container-level (parent MDX) segments, sorted by path. */
59
+ allSegments: MarkdownSourceMapSegment[];
61
60
  markdown: string;
61
+ /** Leaf-level segments only (most precise per path). */
62
62
  segments: MarkdownSourceMapSegment[];
63
63
  };
64
64
  declare const serializeMdWithSourceMap: (editor: SlateEditor, options?: Omit<SerializeMdOptions, "editor">) => SerializeMdSourceMapResult;
65
65
  //#endregion
66
+ //#region src/lib/serializer/resolveSelectionLines.d.ts
67
+ type SelectionPoint = {
68
+ offset: number;
69
+ path: number[];
70
+ };
71
+ type SelectionRange = {
72
+ anchor: SelectionPoint;
73
+ focus: SelectionPoint;
74
+ };
75
+ type ResolveSelectionByPathResult = {
76
+ containsMdx: boolean;
77
+ endLine: number;
78
+ extractedMarkdown: string;
79
+ markdown: string;
80
+ segments: MarkdownSourceMapSegment[];
81
+ startLine: number;
82
+ };
83
+ declare const resolveSelectionByPath: (editor: SlateEditor, selection: SelectionRange, options?: Omit<SerializeMdOptions, "editor">) => ResolveSelectionByPathResult;
84
+ //#endregion
85
+ //#region src/lib/serializer/serializeInlineMd.d.ts
86
+ declare const serializeInlineMd: (editor: SlateEditor, options?: SerializeMdOptions) => string;
87
+ //#endregion
66
88
  //#region src/lib/serializer/wrapWithBlockId.d.ts
67
89
  /**
68
90
  * Wraps an mdast node with a block element containing an ID attribute. Used for
@@ -514,4 +536,4 @@ declare const tagRemarkPlugin: (pluginFn: any, tag: string) => {
514
536
  };
515
537
  declare const getRemarkPluginsWithoutMdx: (plugins: Plugin[]) => Plugin[];
516
538
  //#endregion
517
- export { AllowNodeConfig, DeserializeMdOptions, type MDPhrasingContent, MarkdownConfig, MarkdownPlugin, MarkdownSourceMapKind, MarkdownSourceMapSegment, type MdBlockquote, type MdBreak, type MdCode, type MdContent, MdDecoration, type MdDefinition, type MdDelete, type MdEmphasis, type MdFootnoteDefinition, type MdFootnoteReference, type MdHeading, type MdHtml, type MdImage, type MdImageReference, type MdInlineCode, type MdInlineMath, type MdLink, type MdLinkReference, type MdList, type MdListItem, MdMark, type MdMath, type MdMdxJsxFlowElement, type MdMdxJsxTextElement, MdNodeParser, type MdParagraph, type MdRoot, type MdRootContent, MdRules, type MdStrong, type MdTable, type MdTableCell, type MdTableRow, type MdText, type MdThematicBreak, MdType, type MdYaml, MentionNode, ParseMarkdownBlocksOptions, PlateType, REMARK_MDX_TAG, SerializeMdOptions, SerializeMdSourceMapResult, StrictPlateType, basicMarkdownMarks, buildMdastNode, buildRules, buildSlateNode, columnRules, convertChildrenDeserialize, convertNodesDeserialize, convertNodesSerialize, convertTextsDeserialize, convertTextsSerialize, customMdxDeserialize, defaultRules, deserializeInlineMd, deserializeMd, fontRules, getCustomMark, getDeserializerByKey, getMergedOptionsDeserialize, getMergedOptionsSerialize, getRemarkPluginsWithoutMdx, getSerializerByKey, getStyleValue, htmlToJsx, listToMdastTree, markdownToAstProcessor, markdownToSlateNodes, markdownToSlateNodesSafely, mdastToPlate, mdastToSlate, mediaRules, parseAttributes, parseMarkdownBlocks, plateToMdast, propsToAttributes, remarkMdx, remarkMention, serializeInlineMd, serializeMd, serializeMdWithSourceMap, splitIncompleteMdx, stripMarkdown, stripMarkdownBlocks, stripMarkdownInline, tagRemarkPlugin, type unistLib, unreachable, wrapWithBlockId };
539
+ export { AllowNodeConfig, DeserializeMdOptions, type MDPhrasingContent, MarkdownConfig, MarkdownPlugin, MarkdownSourceMapKind, MarkdownSourceMapSegment, type MdBlockquote, type MdBreak, type MdCode, type MdContent, MdDecoration, type MdDefinition, type MdDelete, type MdEmphasis, type MdFootnoteDefinition, type MdFootnoteReference, type MdHeading, type MdHtml, type MdImage, type MdImageReference, type MdInlineCode, type MdInlineMath, type MdLink, type MdLinkReference, type MdList, type MdListItem, MdMark, type MdMath, type MdMdxJsxFlowElement, type MdMdxJsxTextElement, MdNodeParser, type MdParagraph, type MdRoot, type MdRootContent, MdRules, type MdStrong, type MdTable, type MdTableCell, type MdTableRow, type MdText, type MdThematicBreak, MdType, type MdYaml, MentionNode, ParseMarkdownBlocksOptions, PlateType, REMARK_MDX_TAG, ResolveSelectionByPathResult, SelectionRange, SerializeMdOptions, SerializeMdSourceMapResult, StrictPlateType, basicMarkdownMarks, buildMdastNode, buildRules, buildSlateNode, columnRules, convertChildrenDeserialize, convertNodesDeserialize, convertNodesSerialize, convertTextsDeserialize, convertTextsSerialize, customMdxDeserialize, defaultRules, deserializeInlineMd, deserializeMd, fontRules, getCustomMark, getDeserializerByKey, getMergedOptionsDeserialize, getMergedOptionsSerialize, getRemarkPluginsWithoutMdx, getSerializerByKey, getStyleValue, htmlToJsx, listToMdastTree, markdownToAstProcessor, markdownToSlateNodes, markdownToSlateNodesSafely, mdastToPlate, mdastToSlate, mediaRules, parseAttributes, parseMarkdownBlocks, plateToMdast, propsToAttributes, remarkMdx, remarkMention, resolveSelectionByPath, serializeInlineMd, serializeMd, serializeMdWithSourceMap, splitIncompleteMdx, stripMarkdown, stripMarkdownBlocks, stripMarkdownInline, tagRemarkPlugin, type unistLib, unreachable, wrapWithBlockId };
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import { ElementApi, KEYS, TextApi, bindFirst, createTSlatePlugin, getPluginKey, getPluginType, isUrl } from "platejs";
2
2
  import "mdast-util-mdx";
3
+ import { normalizeDateValue } from "@platejs/date";
3
4
  import kebabCase from "lodash/kebabCase.js";
4
- import remarkStringify from "remark-stringify";
5
- import { unified } from "unified";
6
5
  import { defaultHandlers, toMarkdown } from "mdast-util-to-markdown";
6
+ import { unified } from "unified";
7
+ import remarkStringify from "remark-stringify";
7
8
  import remarkParse from "remark-parse";
8
9
  import { marked } from "marked";
9
10
  import baseRemarkMdx from "remark-mdx";
@@ -131,7 +132,7 @@ function propsToAttributes(props) {
131
132
  return Object.entries(props).map(([name, value]) => ({
132
133
  name,
133
134
  type: "mdxJsxAttribute",
134
- value: typeof value === "string" ? value : JSON.stringify(value)
135
+ value: typeof value === "string" || value?.type === "mdxJsxAttributeValueExpression" ? value : JSON.stringify(value)
135
136
  }));
136
137
  }
137
138
 
@@ -239,7 +240,7 @@ function createMediaRule() {
239
240
  };
240
241
  },
241
242
  serialize: (node, options) => {
242
- const { id, children, type, url, ...rest } = node;
243
+ const { children, type, url, ...rest } = node;
243
244
  return {
244
245
  attributes: propsToAttributes({
245
246
  ...rest,
@@ -255,6 +256,7 @@ function createMediaRule() {
255
256
  const mediaRules = {
256
257
  audio: createMediaRule(),
257
258
  file: createMediaRule(),
259
+ media_embed: createMediaRule(),
258
260
  video: createMediaRule()
259
261
  };
260
262
 
@@ -655,45 +657,6 @@ const shouldIncludeNode$2 = (node, options) => {
655
657
  return true;
656
658
  };
657
659
 
658
- //#endregion
659
- //#region src/lib/serializer/serializeInlineMd.ts
660
- const serializeInlineMd = (editor, options) => {
661
- const mergedOptions = getMergedOptionsSerialize(editor, options);
662
- const toRemarkProcessor = unified().use(mergedOptions.remarkPlugins ?? []).use(remarkStringify, {
663
- emphasis: "_",
664
- ...mergedOptions?.remarkStringifyOptions
665
- });
666
- if (options?.value?.length === 0) return "";
667
- const convertedTexts = convertTextsSerialize(mergedOptions.value, { editor });
668
- return toRemarkProcessor.stringify({
669
- children: convertedTexts,
670
- type: "root"
671
- });
672
- };
673
-
674
- //#endregion
675
- //#region src/lib/serializer/serializeMd.ts
676
- /** Serialize the editor value to Markdown. */
677
- const serializeMd = (editor, options) => {
678
- const mergedOptions = getMergedOptionsSerialize(editor, options);
679
- const { remarkPlugins, value } = mergedOptions;
680
- const toRemarkProcessor = unified().use(remarkPlugins ?? []).use(remarkStringify, {
681
- emphasis: "_",
682
- ...mergedOptions?.remarkStringifyOptions
683
- });
684
- const mdast = slateToMdast({
685
- children: value,
686
- options: mergedOptions
687
- });
688
- return toRemarkProcessor.stringify(mdast);
689
- };
690
- const slateToMdast = ({ children, options }) => {
691
- return {
692
- children: convertNodesSerialize(children, options, true),
693
- type: "root"
694
- };
695
- };
696
-
697
660
  //#endregion
698
661
  //#region src/lib/serializer/serializeMdWithSourceMap.ts
699
662
  const collectSlateText = (node) => {
@@ -770,7 +733,6 @@ const buildSource = (node, editor) => {
770
733
  const path = Array.isArray(node.__sourceMapPath) ? node.__sourceMapPath : null;
771
734
  if (!path) return null;
772
735
  const text = collectSlateText(node);
773
- if (!text.trim() && kind !== "media" && kind !== "block") return null;
774
736
  return {
775
737
  containsMdx: false,
776
738
  kind,
@@ -826,6 +788,72 @@ const attachTableCellSources = (mdastTable, slateTable, editor) => {
826
788
  }
827
789
  }
828
790
  };
791
+ /**
792
+ * Custom serializers (e.g. MDX components) build their own mdast subtree,
793
+ * bypassing convertNodesWithSource. Walk Slate and mdast element children
794
+ * in parallel (by position) and attach sourceMap data so the handler
795
+ * wrappers capture per-child segments during serialization.
796
+ *
797
+ * Position-based matching works because custom serializers preserve child
798
+ * order. We filter to element children only (skip text/leaf nodes) in both
799
+ * trees before zipping.
800
+ */
801
+ /**
802
+ * Walk the mdast list tree and attach sources from the flat Slate list
803
+ * paragraphs. Slate uses flat paragraphs with `listStyleType` while mdast
804
+ * nests them as list > listItem > paragraph. We walk both in document order
805
+ * to zip them correctly.
806
+ */
807
+ const attachListSources = (mdastList, slateListItems, editor, insideMdx) => {
808
+ let slateIdx = 0;
809
+ const walkList = (list) => {
810
+ if (!Array.isArray(list.children)) return;
811
+ for (const listItem of list.children) {
812
+ if (listItem.type !== "listItem" || !Array.isArray(listItem.children)) continue;
813
+ for (const child of listItem.children) if (child.type === "list") walkList(child);
814
+ else if (slateIdx < slateListItems.length) {
815
+ const slateChild = slateListItems[slateIdx++];
816
+ const source = buildSource(slateChild, editor);
817
+ if (source) {
818
+ if (insideMdx) source.containsMdx = true;
819
+ attachSource(child, source);
820
+ }
821
+ }
822
+ }
823
+ };
824
+ walkList(mdastList);
825
+ };
826
+ const attachDescendantSources = (mdastNode, slateNode, editor, insideMdx) => {
827
+ if (!insideMdx && !MDX_MDAST_TYPES.has(mdastNode?.type ?? "")) return;
828
+ const slateChildren = slateNode?.children;
829
+ const mdastChildren = mdastNode?.children;
830
+ if (!Array.isArray(slateChildren) || !Array.isArray(mdastChildren)) return;
831
+ const slateElements = slateChildren.filter((c) => c?.type && Array.isArray(c.children));
832
+ const mdastElements = mdastChildren.filter((c) => c.type && c.type !== "text");
833
+ let si = 0;
834
+ for (const mdastChild of mdastElements) {
835
+ if (mdastChild.type === "list") {
836
+ const listRun = [];
837
+ while (si < slateElements.length && slateElements[si].listStyleType) {
838
+ listRun.push(slateElements[si]);
839
+ si++;
840
+ }
841
+ attachListSources(mdastChild, listRun, editor, insideMdx);
842
+ continue;
843
+ }
844
+ if (si >= slateElements.length) break;
845
+ const slateChild = slateElements[si];
846
+ si++;
847
+ const isMdx = insideMdx || MDX_MDAST_TYPES.has(mdastChild.type ?? "");
848
+ const source = buildSource(slateChild, editor);
849
+ if (source) {
850
+ if (isMdx) source.containsMdx = true;
851
+ attachSource(mdastChild, source);
852
+ }
853
+ if (mdastChild.type === "table") attachTableCellSources(mdastChild, slateChild, editor);
854
+ else if (insideMdx || MDX_MDAST_TYPES.has(mdastChild.type ?? "")) attachDescendantSources(mdastChild, slateChild, editor, isMdx);
855
+ }
856
+ };
829
857
  const buildMdastNodeWithSource = (node, options, isBlock = false) => {
830
858
  let mdastNode = buildMdastNode(node, options, isBlock);
831
859
  if (!mdastNode) {
@@ -847,19 +875,20 @@ const buildMdastNodeWithSource = (node, options, isBlock = false) => {
847
875
  return mdastNode;
848
876
  }
849
877
  const source = buildSource(node, options.editor);
878
+ const isMdxOutput = MDX_MDAST_TYPES.has(mdastNode?.type ?? "");
850
879
  if (!source) {
851
- if (mdastNode && MDX_MDAST_TYPES.has(mdastNode.type ?? "")) propagateMdxFlag(mdastNode);
880
+ if (isMdxOutput) propagateMdxFlag(mdastNode);
881
+ attachDescendantSources(mdastNode, node, options.editor, isMdxOutput);
852
882
  return mdastNode;
853
883
  }
854
- if (MDX_MDAST_TYPES.has(mdastNode?.type ?? "")) {
855
- source.containsMdx = true;
856
- if (Array.isArray(mdastNode?.children)) for (const child of mdastNode.children) propagateMdxFlag(child);
857
- }
884
+ if (isMdxOutput) source.containsMdx = true;
858
885
  if (source.kind === "blockquote" && Array.isArray(mdastNode?.children) && mdastNode.children[0]) {
859
886
  attachSource(mdastNode.children[0], source);
860
887
  return mdastNode;
861
888
  }
862
- return attachSource(mdastNode, source);
889
+ attachSource(mdastNode, source);
890
+ attachDescendantSources(mdastNode, node, options.editor, isMdxOutput);
891
+ return mdastNode;
863
892
  };
864
893
  const convertNodesWithSource = (nodes, options, isBlock = false) => {
865
894
  const mdastNodes = [];
@@ -1046,13 +1075,248 @@ const serializeMdWithSourceMap = (editor, options) => {
1046
1075
  }
1047
1076
  const ordered = Array.from(deduped.values()).sort((a, b) => comparePaths(a.path, b.path));
1048
1077
  return {
1078
+ allSegments: ordered,
1049
1079
  markdown,
1050
1080
  segments: ordered.filter((seg) => !ordered.some((other) => other !== seg && isPathPrefix(seg.path, other.path)))
1051
1081
  };
1052
1082
  };
1053
1083
 
1084
+ //#endregion
1085
+ //#region src/lib/serializer/resolveSelectionLines.ts
1086
+ const MDX_TAG_RE = /<[a-zA-Z_][\w.-]*/;
1087
+ /** `a` is an ancestor-or-self of `b`: a's path is a prefix of (or equal to) b's. */
1088
+ const isAncestorOrSelf = (a, b) => a.length <= b.length && a.every((v, i) => b[i] === v);
1089
+ const comparePoints = (a, b) => {
1090
+ const len = Math.max(a.path.length, b.path.length);
1091
+ for (let i = 0; i < len; i++) {
1092
+ const av = a.path[i] ?? 0;
1093
+ const bv = b.path[i] ?? 0;
1094
+ if (av !== bv) return av - bv;
1095
+ }
1096
+ return a.offset - b.offset;
1097
+ };
1098
+ /**
1099
+ * Find the segment covering the start of a selection at `path`.
1100
+ *
1101
+ * 1. Ancestor-or-self: segment whose path is a prefix of `path` (most specific wins)
1102
+ * 2. First descendant: `path` is a prefix of segment's path (container selection)
1103
+ */
1104
+ const findStartSegment = (segments, path) => {
1105
+ let best = null;
1106
+ let bestLen = -1;
1107
+ for (const seg of segments) if (isAncestorOrSelf(seg.path, path) && seg.path.length > bestLen) {
1108
+ best = seg;
1109
+ bestLen = seg.path.length;
1110
+ }
1111
+ if (best) return best;
1112
+ for (const seg of segments) if (isAncestorOrSelf(path, seg.path)) return seg;
1113
+ return null;
1114
+ };
1115
+ /**
1116
+ * Find the segment covering the end of a selection at `path`.
1117
+ *
1118
+ * Same as findStartSegment but for descendant matches picks the LAST one
1119
+ * (the container's last child segment).
1120
+ */
1121
+ const findEndSegment = (segments, path) => {
1122
+ let best = null;
1123
+ let bestLen = -1;
1124
+ for (const seg of segments) if (isAncestorOrSelf(seg.path, path) && seg.path.length > bestLen) {
1125
+ best = seg;
1126
+ bestLen = seg.path.length;
1127
+ }
1128
+ if (best) return best;
1129
+ let last = null;
1130
+ for (const seg of segments) if (isAncestorOrSelf(path, seg.path)) last = seg;
1131
+ return last;
1132
+ };
1133
+ /** Recursively compute the total text length of a Slate node. */
1134
+ const getTextLength = (node) => {
1135
+ if (typeof node?.text === "string") return node.text.length;
1136
+ if (Array.isArray(node?.children)) return node.children.reduce((sum, child) => sum + getTextLength(child), 0);
1137
+ return 0;
1138
+ };
1139
+ /** Walk the editor tree to reach the node at `path`. */
1140
+ const getNodeAtPath = (editor, path) => {
1141
+ let node = { children: editor.children };
1142
+ for (const idx of path) {
1143
+ if (!node?.children?.[idx]) return null;
1144
+ node = node.children[idx];
1145
+ }
1146
+ return node;
1147
+ };
1148
+ /**
1149
+ * Compute the absolute character offset within a segment's text.
1150
+ *
1151
+ * Given a segment at `segPath` (e.g. [1,3,0] = a paragraph) and a selection
1152
+ * point at `selPath` (e.g. [1,3,0,1] = second text node) with `selOffset`,
1153
+ * sum up text lengths of all Slate nodes preceding the selection point within
1154
+ * the segment, then add `selOffset`.
1155
+ */
1156
+ const computeAbsoluteOffset = (editor, segPath, selPath, selOffset) => {
1157
+ const segNode = getNodeAtPath(editor, segPath);
1158
+ if (!segNode) return null;
1159
+ const remainingPath = selPath.slice(segPath.length);
1160
+ if (remainingPath.length === 0) {
1161
+ let textOffset = 0;
1162
+ const children = segNode.children ?? [];
1163
+ for (let i = 0; i < Math.min(selOffset, children.length); i++) textOffset += getTextLength(children[i]);
1164
+ return textOffset;
1165
+ }
1166
+ let current = segNode;
1167
+ let accumulated = 0;
1168
+ for (const childIdx of remainingPath) {
1169
+ const children = current.children ?? [];
1170
+ for (let i = 0; i < childIdx && i < children.length; i++) accumulated += getTextLength(children[i]);
1171
+ if (!children[childIdx]) return null;
1172
+ current = children[childIdx];
1173
+ }
1174
+ return accumulated + selOffset;
1175
+ };
1176
+ /**
1177
+ * Given a segment and an absolute text offset, determine which line within
1178
+ * the segment the offset falls on.
1179
+ */
1180
+ const offsetToLine = (segText, segStartLine, absOffset) => {
1181
+ const clamped = Math.max(0, Math.min(absOffset, segText.length));
1182
+ return segStartLine + (segText.slice(0, clamped).match(/\n/g) ?? []).length;
1183
+ };
1184
+ /**
1185
+ * Whether a segment supports sub-line narrowing. Only for kinds where text
1186
+ * content lines map 1:1 to markdown output lines (no structural wrappers
1187
+ * like code fences, MDX tags, or table delimiters).
1188
+ */
1189
+ const NARROWABLE_KINDS = new Set([
1190
+ "paragraph",
1191
+ "heading",
1192
+ "list_item",
1193
+ "blockquote"
1194
+ ]);
1195
+ const canNarrow = (seg) => NARROWABLE_KINDS.has(seg.kind);
1196
+ const extractLines = (md, start, end) => md.split("\n").slice(start - 1, end).join("\n");
1197
+ /**
1198
+ * Resolve which markdown lines correspond to a Plate editor selection
1199
+ * using the source-map path mapping (no text matching).
1200
+ *
1201
+ * 1. Serializes editor to markdown with source-map segments
1202
+ * 2. Finds the segment matching the anchor path (longest prefix)
1203
+ * 3. Finds the segment matching the focus path
1204
+ * 4. Narrows to sub-line precision using selection offsets
1205
+ * 5. Returns the line range and extracted markdown
1206
+ */
1207
+ /**
1208
+ * When the selection lands on a container path (e.g. clicking an MDX label),
1209
+ * look up the container segment in `allSegments` to include the MDX wrapper
1210
+ * lines (opening/closing tags) rather than just the inner content.
1211
+ */
1212
+ const tryContainerExpand = ({ allSegments, leafSeg, selPath, currentLine, direction }) => {
1213
+ if (!leafSeg) return currentLine;
1214
+ if (selPath.length >= leafSeg.path.length) return currentLine;
1215
+ let containerSeg = null;
1216
+ let containerLen = -1;
1217
+ for (const seg of allSegments) if (isAncestorOrSelf(seg.path, selPath) && seg.path.length > containerLen && seg !== leafSeg) {
1218
+ containerSeg = seg;
1219
+ containerLen = seg.path.length;
1220
+ }
1221
+ for (const seg of allSegments) if (seg.path.length === selPath.length && seg.path.every((v, i) => selPath[i] === v)) {
1222
+ containerSeg = seg;
1223
+ break;
1224
+ }
1225
+ if (!containerSeg) return currentLine;
1226
+ return direction === "start" ? Math.min(currentLine, containerSeg.startLine) : Math.max(currentLine, containerSeg.endLine);
1227
+ };
1228
+ const resolveSelectionByPath = (editor, selection, options) => {
1229
+ const { allSegments, markdown, segments } = serializeMdWithSourceMap(editor, options);
1230
+ const [start, end] = comparePoints(selection.anchor, selection.focus) <= 0 ? [selection.anchor, selection.focus] : [selection.focus, selection.anchor];
1231
+ const startSeg = findStartSegment(segments, start.path);
1232
+ const endSeg = findEndSegment(segments, end.path);
1233
+ if (!startSeg && !endSeg) return {
1234
+ containsMdx: false,
1235
+ endLine: 1,
1236
+ extractedMarkdown: "",
1237
+ markdown,
1238
+ segments,
1239
+ startLine: 1
1240
+ };
1241
+ let startLine = startSeg?.startLine ?? endSeg.startLine;
1242
+ let endLine = endSeg?.endLine ?? startSeg.endLine;
1243
+ if (startSeg && startSeg.endLine > startSeg.startLine && canNarrow(startSeg)) {
1244
+ const absOffset = computeAbsoluteOffset(editor, startSeg.path, start.path, start.offset);
1245
+ if (absOffset !== null) startLine = offsetToLine(startSeg.text, startSeg.startLine, absOffset);
1246
+ }
1247
+ if (endSeg && endSeg.endLine > endSeg.startLine && canNarrow(endSeg)) {
1248
+ const absOffset = computeAbsoluteOffset(editor, endSeg.path, end.path, end.offset);
1249
+ if (absOffset !== null) endLine = offsetToLine(endSeg.text, endSeg.startLine, absOffset);
1250
+ }
1251
+ startLine = tryContainerExpand({
1252
+ allSegments,
1253
+ currentLine: startLine,
1254
+ direction: "start",
1255
+ leafSeg: startSeg,
1256
+ selPath: start.path
1257
+ });
1258
+ endLine = tryContainerExpand({
1259
+ allSegments,
1260
+ currentLine: endLine,
1261
+ direction: "end",
1262
+ leafSeg: endSeg,
1263
+ selPath: end.path
1264
+ });
1265
+ if (startLine > endLine) [startLine, endLine] = [endLine, startLine];
1266
+ const extractedMarkdown = extractLines(markdown, startLine, endLine);
1267
+ return {
1268
+ containsMdx: extractedMarkdown.includes("<") && MDX_TAG_RE.test(extractedMarkdown),
1269
+ endLine,
1270
+ extractedMarkdown,
1271
+ markdown,
1272
+ segments,
1273
+ startLine
1274
+ };
1275
+ };
1276
+
1277
+ //#endregion
1278
+ //#region src/lib/serializer/serializeInlineMd.ts
1279
+ const serializeInlineMd = (editor, options) => {
1280
+ const mergedOptions = getMergedOptionsSerialize(editor, options);
1281
+ const toRemarkProcessor = unified().use(mergedOptions.remarkPlugins ?? []).use(remarkStringify, {
1282
+ emphasis: "_",
1283
+ ...mergedOptions?.remarkStringifyOptions
1284
+ });
1285
+ if (options?.value?.length === 0) return "";
1286
+ const convertedTexts = convertTextsSerialize(mergedOptions.value, { editor });
1287
+ return toRemarkProcessor.stringify({
1288
+ children: convertedTexts,
1289
+ type: "root"
1290
+ });
1291
+ };
1292
+
1293
+ //#endregion
1294
+ //#region src/lib/serializer/serializeMd.ts
1295
+ /** Serialize the editor value to Markdown. */
1296
+ const serializeMd = (editor, options) => {
1297
+ const mergedOptions = getMergedOptionsSerialize(editor, options);
1298
+ const { remarkPlugins, value } = mergedOptions;
1299
+ const toRemarkProcessor = unified().use(remarkPlugins ?? []).use(remarkStringify, {
1300
+ emphasis: "_",
1301
+ resourceLink: false,
1302
+ ...mergedOptions?.remarkStringifyOptions
1303
+ });
1304
+ const mdast = slateToMdast({
1305
+ children: value,
1306
+ options: mergedOptions
1307
+ });
1308
+ return toRemarkProcessor.stringify(mdast);
1309
+ };
1310
+ const slateToMdast = ({ children, options }) => {
1311
+ return {
1312
+ children: convertNodesSerialize(children, options, true),
1313
+ type: "root"
1314
+ };
1315
+ };
1316
+
1054
1317
  //#endregion
1055
1318
  //#region src/lib/rules/defaultRules.ts
1319
+ const BARE_AUTOLINK_PROTOCOL_REGEX = /^https?:\/\//i;
1056
1320
  const LEADING_NEWLINE_REGEX = /^\n/;
1057
1321
  function isBoolean(value) {
1058
1322
  return value === true || value === false || !!value && typeof value === "object" && Object.prototype.toString.call(value) === "[object Boolean]";
@@ -1070,6 +1334,33 @@ const deserializeClassicListItemChildren = (mdastChildren, deco, options) => {
1070
1334
  if (!children.some((child) => child.type === licType)) children.unshift(createClassicListItemContent(options.editor));
1071
1335
  return children;
1072
1336
  };
1337
+ const groupInlineChildrenIntoParagraphs = (editor, children = []) => {
1338
+ const paragraphType = getPluginType(editor, KEYS.p);
1339
+ const elements = [];
1340
+ let inlineNodes = [];
1341
+ const flushInlineNodes = () => {
1342
+ if (inlineNodes.length === 0) return;
1343
+ elements.push({
1344
+ children: inlineNodes,
1345
+ type: paragraphType
1346
+ });
1347
+ inlineNodes = [];
1348
+ };
1349
+ children.forEach((child) => {
1350
+ if (ElementApi.isElement(child) && !editor.api.isInline(child) && editor.api.isBlock(child)) {
1351
+ flushInlineNodes();
1352
+ elements.push(child);
1353
+ return;
1354
+ }
1355
+ inlineNodes.push(child);
1356
+ });
1357
+ flushInlineNodes();
1358
+ if (elements.length > 0) return elements;
1359
+ return [{
1360
+ children: [{ text: "" }],
1361
+ type: paragraphType
1362
+ }];
1363
+ };
1073
1364
  const defaultRules = {
1074
1365
  a: {
1075
1366
  deserialize: (mdastNode, deco, options) => ({
@@ -1077,48 +1368,29 @@ const defaultRules = {
1077
1368
  type: getPluginType(options.editor, KEYS.a),
1078
1369
  url: mdastNode.url
1079
1370
  }),
1080
- serialize: (node, options) => ({
1081
- children: convertNodesSerialize(node.children, options),
1082
- type: "link",
1083
- url: node.url
1084
- })
1085
- },
1086
- blockquote: {
1087
- deserialize: (mdastNode, deco, options) => {
1088
- return {
1089
- children: (mdastNode.children.length > 0 ? mdastNode.children.flatMap((paragraph, index, children) => {
1090
- if (paragraph.type === "paragraph") {
1091
- if (children.length > 1 && children.length - 1 !== index) {
1092
- const paragraphChildren = convertChildrenDeserialize(paragraph.children, deco, options);
1093
- paragraphChildren.push({ text: "\n" }, { text: "\n" });
1094
- return paragraphChildren;
1095
- }
1096
- return convertChildrenDeserialize(paragraph.children, deco, options);
1097
- }
1098
- if ("children" in paragraph) return convertChildrenDeserialize(paragraph.children, deco, options);
1099
- return [{ text: "" }];
1100
- }) : [{ text: "" }]).flatMap((child) => child.type === "blockquote" ? child.children : [child]),
1101
- type: getPluginType(options.editor, KEYS.blockquote)
1102
- };
1103
- },
1104
1371
  serialize: (node, options) => {
1105
- const nodes = [];
1106
- for (const child of node.children) if (child.text === "\n") nodes.push({ type: "break" });
1107
- else nodes.push(child);
1108
- const paragraphChildren = convertNodesSerialize(nodes, options);
1109
- if (paragraphChildren.length > 0 && paragraphChildren.at(-1).type === "break") {
1110
- paragraphChildren.at(-1).type = "html";
1111
- paragraphChildren.at(-1).value = "\n<br />";
1112
- }
1372
+ const children = convertNodesSerialize(node.children, options);
1373
+ if (children.length === 1 && children[0]?.type === "text" && children[0].value === node.url && BARE_AUTOLINK_PROTOCOL_REGEX.test(node.url ?? "")) return {
1374
+ type: "html",
1375
+ value: node.url
1376
+ };
1113
1377
  return {
1114
- children: [{
1115
- children: paragraphChildren,
1116
- type: "paragraph"
1117
- }],
1118
- type: "blockquote"
1378
+ children,
1379
+ type: "link",
1380
+ url: node.url
1119
1381
  };
1120
1382
  }
1121
1383
  },
1384
+ blockquote: {
1385
+ deserialize: (mdastNode, deco, options) => ({
1386
+ children: groupInlineChildrenIntoParagraphs(options.editor, convertNodesDeserialize(mdastNode.children, deco, options)),
1387
+ type: getPluginType(options.editor, KEYS.blockquote)
1388
+ }),
1389
+ serialize: (node, options) => ({
1390
+ children: convertNodesSerialize(groupInlineChildrenIntoParagraphs(options.editor, node.children), options),
1391
+ type: "blockquote"
1392
+ })
1393
+ },
1122
1394
  bold: {
1123
1395
  mark: true,
1124
1396
  deserialize: (mdastNode, deco, options) => convertTextsDeserialize(mdastNode, deco, options)
@@ -1194,18 +1466,25 @@ const defaultRules = {
1194
1466
  },
1195
1467
  date: {
1196
1468
  deserialize(mdastNode, _deco, options) {
1469
+ const props = parseAttributes(mdastNode.attributes);
1197
1470
  return {
1198
1471
  children: [{ text: "" }],
1199
- date: (mdastNode.children?.[0])?.value || "",
1472
+ ...normalizeDateValue(typeof props.value === "string" ? props.value : (mdastNode.children?.[0])?.value || ""),
1200
1473
  type: getPluginType(options.editor, KEYS.date)
1201
1474
  };
1202
1475
  },
1203
- serialize({ date }) {
1476
+ serialize({ date, rawDate }) {
1477
+ if (date && !rawDate) return {
1478
+ attributes: propsToAttributes({ value: date }),
1479
+ children: [],
1480
+ name: "date",
1481
+ type: "mdxJsxTextElement"
1482
+ };
1204
1483
  return {
1205
1484
  attributes: [],
1206
1485
  children: [{
1207
1486
  type: "text",
1208
- value: date ?? ""
1487
+ value: rawDate ?? date ?? ""
1209
1488
  }],
1210
1489
  name: "date",
1211
1490
  type: "mdxJsxTextElement"
@@ -1230,13 +1509,47 @@ const defaultRules = {
1230
1509
  value: node.texExpression
1231
1510
  })
1232
1511
  },
1233
- footnoteDefinition: { deserialize: (mdastNode, deco, options) => {
1234
- return {
1235
- children: convertChildrenDeserialize(mdastNode.children, deco, options).flatMap((child) => child.type === "p" ? child.children : [child]),
1236
- type: getPluginType(options.editor, KEYS.p)
1237
- };
1238
- } },
1239
- footnoteReference: {},
1512
+ footnoteDefinition: {
1513
+ deserialize: (mdastNode, deco, options) => {
1514
+ const paragraphType = getPluginType(options.editor, KEYS.p);
1515
+ const blocks = convertNodesDeserialize(mdastNode.children, deco, options).map((child) => child.type === paragraphType ? child : {
1516
+ children: [child],
1517
+ type: paragraphType
1518
+ });
1519
+ const identifier = mdastNode.identifier;
1520
+ if (blocks.length === 0) return {
1521
+ children: [{
1522
+ children: [{ text: "" }],
1523
+ type: paragraphType
1524
+ }],
1525
+ identifier,
1526
+ type: getPluginType(options.editor, "footnoteDefinition")
1527
+ };
1528
+ return {
1529
+ children: blocks,
1530
+ identifier,
1531
+ type: getPluginType(options.editor, "footnoteDefinition")
1532
+ };
1533
+ },
1534
+ serialize: (node, options) => ({
1535
+ children: convertNodesSerialize(node.children, options),
1536
+ identifier: node.identifier,
1537
+ type: "footnoteDefinition"
1538
+ })
1539
+ },
1540
+ footnoteReference: {
1541
+ deserialize: (mdastNode, _deco, options) => {
1542
+ return {
1543
+ children: [{ text: "" }],
1544
+ identifier: mdastNode.identifier ?? "",
1545
+ type: getPluginType(options.editor, "footnoteReference")
1546
+ };
1547
+ },
1548
+ serialize: (node) => ({
1549
+ identifier: node.identifier ?? node.children?.map((child) => child.text ?? "").join("") ?? "",
1550
+ type: "footnoteReference"
1551
+ })
1552
+ },
1240
1553
  heading: {
1241
1554
  deserialize: (mdastNode, deco, options) => {
1242
1555
  const defaultType = {
@@ -1308,11 +1621,11 @@ const defaultRules = {
1308
1621
  ...rest
1309
1622
  };
1310
1623
  },
1311
- serialize: ({ caption, url }) => {
1624
+ serialize: ({ caption, title, url }) => {
1312
1625
  return {
1313
1626
  children: [{
1314
1627
  alt: caption ? caption.map((c) => c.text).join("") : void 0,
1315
- title: caption ? caption.map((c) => c.text).join("") : void 0,
1628
+ title: typeof title === "string" ? title : void 0,
1316
1629
  type: "image",
1317
1630
  url
1318
1631
  }],
@@ -1758,6 +2071,34 @@ const getDeserializerByKey = (key, options) => {
1758
2071
 
1759
2072
  //#endregion
1760
2073
  //#region src/lib/deserializer/utils/customMdxDeserialize.ts
2074
+ const MDX_ATTR_NAME_TO_HTML_ATTR = {
2075
+ className: "class",
2076
+ htmlFor: "for"
2077
+ };
2078
+ const serializeUnknownMdxChild = (child) => {
2079
+ if (child?.type === "mdxJsxTextElement" || child?.type === "mdxJsxFlowElement") return serializeUnknownMdxNode(child);
2080
+ if ("value" in (child ?? {})) return child.value ?? "";
2081
+ if (Array.isArray(child?.children)) return child.children.map(serializeUnknownMdxChild).join("");
2082
+ return "";
2083
+ };
2084
+ const serializeUnknownMdxAttributes = (attributes) => {
2085
+ if (!attributes?.length) return "";
2086
+ const serialized = attributes.map((attribute) => {
2087
+ const name = MDX_ATTR_NAME_TO_HTML_ATTR[attribute.name] ?? attribute.name;
2088
+ if (attribute.value === void 0 || attribute.value === null) return name;
2089
+ if (typeof attribute.value === "object" && attribute.value?.type === "mdxJsxAttributeValueExpression") return `${name}={${attribute.value.value}}`;
2090
+ return `${name}="${String(attribute.value)}"`;
2091
+ });
2092
+ return serialized.length > 0 ? ` ${serialized.join(" ")}` : "";
2093
+ };
2094
+ const serializeUnknownMdxNode = (mdastNode) => {
2095
+ const attrs = serializeUnknownMdxAttributes(mdastNode.attributes);
2096
+ const openTag = `<${mdastNode.name}${attrs}`;
2097
+ if (!mdastNode.children?.length) return `${openTag} />`;
2098
+ const inner = mdastNode.children.map(serializeUnknownMdxChild).join(mdastNode.type === "mdxJsxFlowElement" ? "\n" : "");
2099
+ if (mdastNode.type === "mdxJsxFlowElement") return `${openTag}>\n${inner}\n</${mdastNode.name}>`;
2100
+ return `${openTag}>${inner}</${mdastNode.name}>`;
2101
+ };
1761
2102
  const customMdxDeserialize = (mdastNode, deco, options) => {
1762
2103
  const customJsxElementKey = mdastNode.name;
1763
2104
  const key = getPluginKey(options.editor, customJsxElementKey) ?? mdastNode.name;
@@ -1765,26 +2106,11 @@ const customMdxDeserialize = (mdastNode, deco, options) => {
1765
2106
  const nodeParserDeserialize = getDeserializerByKey(mdastToPlate(options.editor, key), options);
1766
2107
  if (nodeParserDeserialize) return nodeParserDeserialize(mdastNode, deco, options);
1767
2108
  } else console.warn("This MDX node does not have a parser for deserialization", mdastNode);
1768
- if (mdastNode.type === "mdxJsxTextElement") {
1769
- const tagName = mdastNode.name;
1770
- let textContent = "";
1771
- if (mdastNode.children) textContent = mdastNode.children.map((child) => {
1772
- if ("value" in child) return child.value;
1773
- return "";
1774
- }).join("");
1775
- return [{ text: `<${tagName}>${textContent}</${tagName}>` }];
1776
- }
1777
- if (mdastNode.type === "mdxJsxFlowElement") {
1778
- const tagName = mdastNode.name;
1779
- return [{
1780
- children: [
1781
- { text: `<${tagName}>\n` },
1782
- ...convertChildrenDeserialize(mdastNode.children, deco, options),
1783
- { text: `\n</${tagName}>` }
1784
- ],
1785
- type: getPluginType(options.editor, KEYS.p)
1786
- }];
1787
- }
2109
+ if (mdastNode.type === "mdxJsxTextElement") return [{ text: serializeUnknownMdxNode(mdastNode) }];
2110
+ if (mdastNode.type === "mdxJsxFlowElement") return [{
2111
+ children: [{ text: serializeUnknownMdxNode(mdastNode) }],
2112
+ type: getPluginType(options.editor, KEYS.p)
2113
+ }];
1788
2114
  };
1789
2115
 
1790
2116
  //#endregion
@@ -2292,4 +2618,4 @@ const remarkMention = () => (tree) => {
2292
2618
  };
2293
2619
 
2294
2620
  //#endregion
2295
- export { MarkdownPlugin, REMARK_MDX_TAG, basicMarkdownMarks, buildMdastNode, buildRules, buildSlateNode, columnRules, convertChildrenDeserialize, convertNodesDeserialize, convertNodesSerialize, convertTextsDeserialize, convertTextsSerialize, customMdxDeserialize, defaultRules, deserializeInlineMd, deserializeMd, fontRules, getCustomMark, getDeserializerByKey, getMergedOptionsDeserialize, getMergedOptionsSerialize, getRemarkPluginsWithoutMdx, getSerializerByKey, getStyleValue, htmlToJsx, listToMdastTree, markdownToAstProcessor, markdownToSlateNodes, markdownToSlateNodesSafely, mdastToPlate, mdastToSlate, mediaRules, parseAttributes, parseMarkdownBlocks, plateToMdast, propsToAttributes, remarkMdx, remarkMention, serializeInlineMd, serializeMd, serializeMdWithSourceMap, splitIncompleteMdx, stripMarkdown, stripMarkdownBlocks, stripMarkdownInline, tagRemarkPlugin, unreachable, wrapWithBlockId };
2621
+ export { MarkdownPlugin, REMARK_MDX_TAG, basicMarkdownMarks, buildMdastNode, buildRules, buildSlateNode, columnRules, convertChildrenDeserialize, convertNodesDeserialize, convertNodesSerialize, convertTextsDeserialize, convertTextsSerialize, customMdxDeserialize, defaultRules, deserializeInlineMd, deserializeMd, fontRules, getCustomMark, getDeserializerByKey, getMergedOptionsDeserialize, getMergedOptionsSerialize, getRemarkPluginsWithoutMdx, getSerializerByKey, getStyleValue, htmlToJsx, listToMdastTree, markdownToAstProcessor, markdownToSlateNodes, markdownToSlateNodesSafely, mdastToPlate, mdastToSlate, mediaRules, parseAttributes, parseMarkdownBlocks, plateToMdast, propsToAttributes, remarkMdx, remarkMention, resolveSelectionByPath, serializeInlineMd, serializeMd, serializeMdWithSourceMap, splitIncompleteMdx, stripMarkdown, stripMarkdownBlocks, stripMarkdownInline, tagRemarkPlugin, unreachable, wrapWithBlockId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lofcz/platejs-markdown",
3
- "version": "52.3.8",
3
+ "version": "53.0.0",
4
4
  "description": "Markdown serializer plugin for Plate",
5
5
  "keywords": [
6
6
  "markdown",
@@ -42,18 +42,21 @@
42
42
  "remark-stringify": "^11.0.0",
43
43
  "ts-essentials": "10.1.0",
44
44
  "unified": "^11.0.5",
45
- "unist-util-visit": "5.0.0"
45
+ "unist-util-visit": "5.0.0",
46
+ "@platejs/date": "npm:@lofcz/platejs-date@53.0.0"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@types/mdast": "^4.0.4",
49
50
  "@types/unist": "^3.0.3",
51
+ "remark-emoji": "5.0.1",
50
52
  "remark-gfm": "4.0.1",
51
53
  "remark-math": "6.0.0",
52
54
  "@plate/scripts": "1.0.0",
53
- "platejs": "npm:@lofcz/platejs@52.3.6"
55
+ "@platejs/footnote": "npm:@lofcz/platejs-footnote@53.0.0",
56
+ "platejs": "npm:@lofcz/platejs@53.0.0"
54
57
  },
55
58
  "peerDependencies": {
56
- "platejs": ">=52.0.11",
59
+ "platejs": ">=53.0.0",
57
60
  "react": ">=18.0.0",
58
61
  "react-dom": ">=18.0.0"
59
62
  },