@lofcz/platejs-markdown 52.3.6 → 52.3.9

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,6 +40,48 @@ 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/serializeMdWithSourceMap.d.ts
44
+ type SelectionPath = number[];
45
+ type MarkdownSourceMapKind = 'block' | 'blockquote' | 'code_block' | 'heading' | 'list_item' | 'media' | 'paragraph' | 'table_cell';
46
+ type MarkdownSourceMapSegment = {
47
+ containsMdx: boolean;
48
+ endLine: number;
49
+ kind: MarkdownSourceMapKind;
50
+ markdown: string;
51
+ nodeId?: string;
52
+ path: SelectionPath;
53
+ pathKey: string;
54
+ startLine: number;
55
+ text: string;
56
+ };
57
+ type SerializeMdSourceMapResult = {
58
+ /** All segments including container-level (parent MDX) segments, sorted by path. */
59
+ allSegments: MarkdownSourceMapSegment[];
60
+ markdown: string;
61
+ /** Leaf-level segments only (most precise per path). */
62
+ segments: MarkdownSourceMapSegment[];
63
+ };
64
+ declare const serializeMdWithSourceMap: (editor: SlateEditor, options?: Omit<SerializeMdOptions, "editor">) => SerializeMdSourceMapResult;
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
43
85
  //#region src/lib/serializer/serializeInlineMd.d.ts
44
86
  declare const serializeInlineMd: (editor: SlateEditor, options?: SerializeMdOptions) => string;
45
87
  //#endregion
@@ -494,4 +536,4 @@ declare const tagRemarkPlugin: (pluginFn: any, tag: string) => {
494
536
  };
495
537
  declare const getRemarkPluginsWithoutMdx: (plugins: Plugin[]) => Plugin[];
496
538
  //#endregion
497
- export { AllowNodeConfig, DeserializeMdOptions, type MDPhrasingContent, MarkdownConfig, MarkdownPlugin, 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, 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, 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,8 +1,9 @@
1
1
  import { ElementApi, KEYS, TextApi, bindFirst, createTSlatePlugin, getPluginKey, getPluginType, isUrl } from "platejs";
2
2
  import "mdast-util-mdx";
3
3
  import kebabCase from "lodash/kebabCase.js";
4
- import remarkStringify from "remark-stringify";
4
+ import { defaultHandlers, toMarkdown } from "mdast-util-to-markdown";
5
5
  import { unified } from "unified";
6
+ import remarkStringify from "remark-stringify";
6
7
  import remarkParse from "remark-parse";
7
8
  import { marked } from "marked";
8
9
  import baseRemarkMdx from "remark-mdx";
@@ -591,12 +592,12 @@ const convertNodesSerialize = (nodes, options, isBlock = false) => {
591
592
  for (let i = 0; i <= nodes.length; i++) {
592
593
  const n = nodes[i];
593
594
  if (n && TextApi.isText(n)) {
594
- if (shouldIncludeText(n, options)) textQueue.push(n);
595
+ if (shouldIncludeText$1(n, options)) textQueue.push(n);
595
596
  } else {
596
597
  if (textQueue.length > 0) mdastNodes.push(...convertTextsSerialize(textQueue, options));
597
598
  textQueue = [];
598
599
  if (!n) continue;
599
- if (!shouldIncludeNode$1(n, options)) continue;
600
+ if (!shouldIncludeNode$2(n, options)) continue;
600
601
  const pType = getPluginType(options.editor, KEYS.p) ?? KEYS.p;
601
602
  if (n?.type === pType && "listStyleType" in n) {
602
603
  listBlock.push(n);
@@ -631,6 +632,122 @@ const buildMdastNode = (node, options, isBlock = false) => {
631
632
  }
632
633
  unreachable(node);
633
634
  };
635
+ const shouldIncludeText$1 = (text, options) => {
636
+ const { allowedNodes, allowNode, disallowedNodes } = options;
637
+ if (allowedNodes && disallowedNodes && allowedNodes.length > 0 && disallowedNodes.length > 0) throw new Error("Cannot combine allowedNodes with disallowedNodes");
638
+ for (const [key, value] of Object.entries(text)) {
639
+ if (key === "text") continue;
640
+ if (allowedNodes) {
641
+ if (!allowedNodes.includes(key) && value) return false;
642
+ } else if (disallowedNodes?.includes(key) && value) return false;
643
+ }
644
+ if (allowNode?.serialize) return allowNode.serialize(text);
645
+ return true;
646
+ };
647
+ const shouldIncludeNode$2 = (node, options) => {
648
+ const { allowedNodes, allowNode, disallowedNodes } = options;
649
+ if (!node.type) return true;
650
+ if (allowedNodes && disallowedNodes && allowedNodes.length > 0 && disallowedNodes.length > 0) throw new Error("Cannot combine allowedNodes with disallowedNodes");
651
+ if (allowedNodes) {
652
+ if (!allowedNodes.includes(node.type)) return false;
653
+ } else if (disallowedNodes?.includes(node.type)) return false;
654
+ if (allowNode?.serialize) return allowNode.serialize(node);
655
+ return true;
656
+ };
657
+
658
+ //#endregion
659
+ //#region src/lib/serializer/serializeMdWithSourceMap.ts
660
+ const collectSlateText = (node) => {
661
+ if (typeof node === "string") return node;
662
+ if (node?.text != null) return node.text;
663
+ if (Array.isArray(node?.children)) return node.children.map(collectSlateText).join("");
664
+ return "";
665
+ };
666
+ const comparePaths = (a, b) => {
667
+ const len = Math.min(a.length, b.length);
668
+ for (let i = 0; i < len; i++) if (a[i] !== b[i]) return a[i] - b[i];
669
+ return a.length - b.length;
670
+ };
671
+ const isPathPrefix = (prefix, path) => prefix.length < path.length && prefix.every((part, index) => path[index] === part);
672
+ const toPathKey = (path) => path.join(".");
673
+ const annotatePaths = (node, path) => {
674
+ if (node == null || typeof node !== "object") return node;
675
+ const clone = {
676
+ ...node,
677
+ __sourceMapPath: path
678
+ };
679
+ if (Array.isArray(node.children)) clone.children = node.children.map((child, i) => annotatePaths(child, [...path, i]));
680
+ return clone;
681
+ };
682
+ const MDX_MDAST_TYPES = new Set(["mdxJsxFlowElement", "mdxJsxTextElement"]);
683
+ const propagateMdxFlag = (node) => {
684
+ if (!node || typeof node !== "object") return;
685
+ if (node.data?.sourceMap) node.data.sourceMap.containsMdx = true;
686
+ if (Array.isArray(node.children)) for (const child of node.children) propagateMdxFlag(child);
687
+ };
688
+ const SEGMENT_SKIP_TYPES = new Set([
689
+ "table",
690
+ "tr",
691
+ KEYS.table,
692
+ KEYS.tr,
693
+ "column_group",
694
+ "column",
695
+ KEYS.columnGroup,
696
+ KEYS.column
697
+ ]);
698
+ const MEDIA_TYPES = new Set([
699
+ "img",
700
+ "image",
701
+ "media_embed",
702
+ "video",
703
+ "audio",
704
+ "file",
705
+ "excalidraw",
706
+ KEYS.img,
707
+ KEYS.mediaEmbed,
708
+ KEYS.video,
709
+ KEYS.audio,
710
+ KEYS.file,
711
+ KEYS.excalidraw
712
+ ]);
713
+ const slateNodeKind = (node, editor) => {
714
+ if (node.listStyleType) return "list_item";
715
+ const key = getPluginKey(editor, node.type) ?? node.type;
716
+ if (KEYS.heading.includes(key) || key === "heading") return "heading";
717
+ if (key === KEYS.blockquote) return "blockquote";
718
+ if (key === KEYS.codeBlock || key === "code_block") return "code_block";
719
+ if (key === KEYS.td || key === KEYS.th) return "table_cell";
720
+ if (MEDIA_TYPES.has(key)) return "media";
721
+ const pType = getPluginType(editor, KEYS.p) ?? KEYS.p;
722
+ if (node.type === pType) return "paragraph";
723
+ if (SEGMENT_SKIP_TYPES.has(key)) return null;
724
+ if (Array.isArray(node.children)) return "block";
725
+ return null;
726
+ };
727
+ const buildSource = (node, editor) => {
728
+ if (!node || typeof node !== "object" || !Array.isArray(node.children)) return null;
729
+ const kind = slateNodeKind(node, editor);
730
+ if (!kind) return null;
731
+ const path = Array.isArray(node.__sourceMapPath) ? node.__sourceMapPath : null;
732
+ if (!path) return null;
733
+ const text = collectSlateText(node);
734
+ return {
735
+ containsMdx: false,
736
+ kind,
737
+ nodeId: typeof node.id === "string" ? node.id : void 0,
738
+ path,
739
+ pathKey: toPathKey(path),
740
+ text
741
+ };
742
+ };
743
+ const attachSource = (mdastNode, source) => {
744
+ if (!source || !mdastNode || typeof mdastNode !== "object") return mdastNode;
745
+ mdastNode.data = {
746
+ ...mdastNode.data ?? {},
747
+ sourceMap: source
748
+ };
749
+ return mdastNode;
750
+ };
634
751
  const shouldIncludeText = (text, options) => {
635
752
  const { allowedNodes, allowNode, disallowedNodes } = options;
636
753
  if (allowedNodes && disallowedNodes && allowedNodes.length > 0 && disallowedNodes.length > 0) throw new Error("Cannot combine allowedNodes with disallowedNodes");
@@ -653,6 +770,507 @@ const shouldIncludeNode$1 = (node, options) => {
653
770
  if (allowNode?.serialize) return allowNode.serialize(node);
654
771
  return true;
655
772
  };
773
+ /**
774
+ * After building a table mdast node, attach sourceMap to individual cells by
775
+ * walking the Slate table and mdast table in parallel.
776
+ */
777
+ const attachTableCellSources = (mdastTable, slateTable, editor) => {
778
+ const mdastRows = mdastTable.children || [];
779
+ const slateRows = slateTable.children || [];
780
+ for (let r = 0; r < Math.min(mdastRows.length, slateRows.length); r++) {
781
+ const mdastCells = mdastRows[r]?.children || [];
782
+ const slateCells = slateRows[r]?.children || [];
783
+ for (let c = 0; c < Math.min(mdastCells.length, slateCells.length); c++) {
784
+ const source = buildSource(slateCells[c], editor);
785
+ if (source) attachSource(mdastCells[c], source);
786
+ }
787
+ }
788
+ };
789
+ /**
790
+ * Custom serializers (e.g. MDX components) build their own mdast subtree,
791
+ * bypassing convertNodesWithSource. Walk Slate and mdast element children
792
+ * in parallel (by position) and attach sourceMap data so the handler
793
+ * wrappers capture per-child segments during serialization.
794
+ *
795
+ * Position-based matching works because custom serializers preserve child
796
+ * order. We filter to element children only (skip text/leaf nodes) in both
797
+ * trees before zipping.
798
+ */
799
+ /**
800
+ * Walk the mdast list tree and attach sources from the flat Slate list
801
+ * paragraphs. Slate uses flat paragraphs with `listStyleType` while mdast
802
+ * nests them as list > listItem > paragraph. We walk both in document order
803
+ * to zip them correctly.
804
+ */
805
+ const attachListSources = (mdastList, slateListItems, editor, insideMdx) => {
806
+ let slateIdx = 0;
807
+ const walkList = (list) => {
808
+ if (!Array.isArray(list.children)) return;
809
+ for (const listItem of list.children) {
810
+ if (listItem.type !== "listItem" || !Array.isArray(listItem.children)) continue;
811
+ for (const child of listItem.children) if (child.type === "list") walkList(child);
812
+ else if (slateIdx < slateListItems.length) {
813
+ const slateChild = slateListItems[slateIdx++];
814
+ const source = buildSource(slateChild, editor);
815
+ if (source) {
816
+ if (insideMdx) source.containsMdx = true;
817
+ attachSource(child, source);
818
+ }
819
+ }
820
+ }
821
+ };
822
+ walkList(mdastList);
823
+ };
824
+ const attachDescendantSources = (mdastNode, slateNode, editor, insideMdx) => {
825
+ if (!insideMdx && !MDX_MDAST_TYPES.has(mdastNode?.type ?? "")) return;
826
+ const slateChildren = slateNode?.children;
827
+ const mdastChildren = mdastNode?.children;
828
+ if (!Array.isArray(slateChildren) || !Array.isArray(mdastChildren)) return;
829
+ const slateElements = slateChildren.filter((c) => c?.type && Array.isArray(c.children));
830
+ const mdastElements = mdastChildren.filter((c) => c.type && c.type !== "text");
831
+ let si = 0;
832
+ for (const mdastChild of mdastElements) {
833
+ if (mdastChild.type === "list") {
834
+ const listRun = [];
835
+ while (si < slateElements.length && slateElements[si].listStyleType) {
836
+ listRun.push(slateElements[si]);
837
+ si++;
838
+ }
839
+ attachListSources(mdastChild, listRun, editor, insideMdx);
840
+ continue;
841
+ }
842
+ if (si >= slateElements.length) break;
843
+ const slateChild = slateElements[si];
844
+ si++;
845
+ const isMdx = insideMdx || MDX_MDAST_TYPES.has(mdastChild.type ?? "");
846
+ const source = buildSource(slateChild, editor);
847
+ if (source) {
848
+ if (isMdx) source.containsMdx = true;
849
+ attachSource(mdastChild, source);
850
+ }
851
+ if (mdastChild.type === "table") attachTableCellSources(mdastChild, slateChild, editor);
852
+ else if (insideMdx || MDX_MDAST_TYPES.has(mdastChild.type ?? "")) attachDescendantSources(mdastChild, slateChild, editor, isMdx);
853
+ }
854
+ };
855
+ const buildMdastNodeWithSource = (node, options, isBlock = false) => {
856
+ let mdastNode = buildMdastNode(node, options, isBlock);
857
+ if (!mdastNode) {
858
+ const text = collectSlateText(node);
859
+ if (text.trim()) mdastNode = {
860
+ children: [{
861
+ type: "text",
862
+ value: text
863
+ }],
864
+ type: "paragraph"
865
+ };
866
+ else mdastNode = {
867
+ type: "html",
868
+ value: `<!-- ${node.type ?? "unknown"} -->`
869
+ };
870
+ }
871
+ if (mdastNode?.type === "table") {
872
+ attachTableCellSources(mdastNode, node, options.editor);
873
+ return mdastNode;
874
+ }
875
+ const source = buildSource(node, options.editor);
876
+ const isMdxOutput = MDX_MDAST_TYPES.has(mdastNode?.type ?? "");
877
+ if (!source) {
878
+ if (isMdxOutput) propagateMdxFlag(mdastNode);
879
+ attachDescendantSources(mdastNode, node, options.editor, isMdxOutput);
880
+ return mdastNode;
881
+ }
882
+ if (isMdxOutput) source.containsMdx = true;
883
+ if (source.kind === "blockquote" && Array.isArray(mdastNode?.children) && mdastNode.children[0]) {
884
+ attachSource(mdastNode.children[0], source);
885
+ return mdastNode;
886
+ }
887
+ attachSource(mdastNode, source);
888
+ attachDescendantSources(mdastNode, node, options.editor, isMdxOutput);
889
+ return mdastNode;
890
+ };
891
+ const convertNodesWithSource = (nodes, options, isBlock = false) => {
892
+ const mdastNodes = [];
893
+ let textQueue = [];
894
+ const listBlock = [];
895
+ for (let i = 0; i <= nodes.length; i++) {
896
+ const n = nodes[i];
897
+ if (n && TextApi.isText(n)) {
898
+ if (shouldIncludeText(n, options)) textQueue.push(n);
899
+ continue;
900
+ }
901
+ if (textQueue.length > 0) mdastNodes.push(...convertTextsSerialize(textQueue, options));
902
+ textQueue = [];
903
+ if (!n) continue;
904
+ if (!shouldIncludeNode$1(n, options)) continue;
905
+ const pType = getPluginType(options.editor, KEYS.p) ?? KEYS.p;
906
+ if (n?.type === pType && "listStyleType" in n) {
907
+ listBlock.push(n);
908
+ const next = nodes[i + 1];
909
+ const isNextIndent = next && next.type === pType && "listStyleType" in next;
910
+ const firstList = listBlock.at(0);
911
+ const hasDifferentListStyle = isNextIndent && firstList && next.listStyleType !== firstList.listStyleType && next.indent === firstList.indent;
912
+ if (!isNextIndent || hasDifferentListStyle) {
913
+ mdastNodes.push(listToMdastTreeWithSource(listBlock, options));
914
+ listBlock.length = 0;
915
+ }
916
+ continue;
917
+ }
918
+ const node = buildMdastNodeWithSource(n, options, isBlock);
919
+ if (node) mdastNodes.push(node);
920
+ }
921
+ return mdastNodes;
922
+ };
923
+ const listToMdastTreeWithSource = (nodes, options) => {
924
+ const root = {
925
+ children: [],
926
+ ordered: nodes[0].listStyleType === "decimal",
927
+ spread: options.spread ?? false,
928
+ start: nodes[0].listStart,
929
+ type: "list"
930
+ };
931
+ const indentStack = [{
932
+ indent: nodes[0].indent,
933
+ list: root,
934
+ parent: null,
935
+ styleType: nodes[0].listStyleType
936
+ }];
937
+ for (let i = 0; i < nodes.length; i++) {
938
+ const node = nodes[i];
939
+ const currentIndent = node.indent;
940
+ while (indentStack.length > 1 && indentStack.at(-1).indent > currentIndent) indentStack.pop();
941
+ let stackTop = indentStack.at(-1);
942
+ if (stackTop.indent === currentIndent && stackTop.styleType !== node.listStyleType && !!stackTop.parent) {
943
+ const siblingList = {
944
+ children: [],
945
+ ordered: node.listStyleType === "decimal",
946
+ spread: options.spread ?? false,
947
+ start: node.listStart,
948
+ type: "list"
949
+ };
950
+ stackTop.parent.children.push(siblingList);
951
+ indentStack[indentStack.length - 1] = {
952
+ indent: currentIndent,
953
+ list: siblingList,
954
+ parent: stackTop.parent,
955
+ styleType: node.listStyleType
956
+ };
957
+ stackTop = indentStack.at(-1);
958
+ }
959
+ const paragraph = {
960
+ children: convertNodesWithSource(node.children, options),
961
+ type: "paragraph"
962
+ };
963
+ attachSource(paragraph, buildSource(node, options.editor));
964
+ const listItem = {
965
+ checked: null,
966
+ children: [paragraph],
967
+ spread: options.spread ?? false,
968
+ type: "listItem"
969
+ };
970
+ if (node.listStyleType === "todo" && node.checked !== void 0) listItem.checked = node.checked;
971
+ stackTop.list.children.push(listItem);
972
+ const nextNode = nodes[i + 1];
973
+ if (nextNode && nextNode.indent > currentIndent) {
974
+ const nestedList = {
975
+ children: [],
976
+ ordered: nextNode.listStyleType === "decimal",
977
+ spread: options.spread ?? false,
978
+ start: nextNode.listStart,
979
+ type: "list"
980
+ };
981
+ listItem.children.push(nestedList);
982
+ indentStack.push({
983
+ indent: nextNode.indent,
984
+ list: nestedList,
985
+ parent: listItem,
986
+ styleType: nextNode.listStyleType
987
+ });
988
+ }
989
+ }
990
+ return root;
991
+ };
992
+ const collectExtensionHandlers = (extensions, into) => {
993
+ if (!extensions) return into;
994
+ for (const ext of extensions) {
995
+ if (!ext) continue;
996
+ if (Array.isArray(ext)) {
997
+ collectExtensionHandlers(ext, into);
998
+ continue;
999
+ }
1000
+ if (Array.isArray(ext.extensions)) collectExtensionHandlers(ext.extensions, into);
1001
+ if (ext.handlers) Object.assign(into, ext.handlers);
1002
+ }
1003
+ return into;
1004
+ };
1005
+ /**
1006
+ * Strip `handlers` from extension objects so that `toMarkdown`'s `configure()`
1007
+ * won't overwrite our wrapped handlers while keeping `unsafe`, `join`, etc.
1008
+ */
1009
+ const stripExtensionHandlers = (extensions) => extensions.map((ext) => {
1010
+ if (!ext) return ext;
1011
+ if (Array.isArray(ext)) return stripExtensionHandlers(ext);
1012
+ if (typeof ext !== "object") return ext;
1013
+ const { handlers: _h, ...rest } = ext;
1014
+ if (Array.isArray(rest.extensions)) rest.extensions = stripExtensionHandlers(rest.extensions);
1015
+ return rest;
1016
+ });
1017
+ const serializeMdWithSourceMap = (editor, options) => {
1018
+ const mergedOptions = getMergedOptionsSerialize(editor, options);
1019
+ const mdast = {
1020
+ children: convertNodesWithSource((mergedOptions.value ?? editor.children).map((child, index) => annotatePaths(child, [index])), mergedOptions, true),
1021
+ type: "root"
1022
+ };
1023
+ const extensionProcessor = unified().use(mergedOptions.remarkPlugins ?? []);
1024
+ try {
1025
+ extensionProcessor.freeze();
1026
+ } catch {}
1027
+ const extensions = extensionProcessor.data("toMarkdownExtensions") ?? [];
1028
+ const handlers = { ...defaultHandlers };
1029
+ collectExtensionHandlers(extensions, handlers);
1030
+ const segments = [];
1031
+ const wrappedHandlers = {};
1032
+ const pushSegment = (source, emitted, startLine, endLine) => {
1033
+ segments.push({
1034
+ containsMdx: source.containsMdx,
1035
+ endLine,
1036
+ kind: source.kind,
1037
+ markdown: emitted,
1038
+ nodeId: source.nodeId,
1039
+ path: source.path,
1040
+ pathKey: source.pathKey,
1041
+ startLine,
1042
+ text: source.text
1043
+ });
1044
+ };
1045
+ for (const [type, handler] of Object.entries(handlers)) wrappedHandlers[type] = (node, parent, state, info) => {
1046
+ const startLine = info.now.line;
1047
+ const emitted = handler(node, parent, state, info);
1048
+ const source = node?.data?.sourceMap;
1049
+ if (source && emitted.trim()) pushSegment(source, emitted, startLine, startLine + (emitted.split(/\r?\n|\r/g).length - 1));
1050
+ if (type === "table" && Array.isArray(node?.children)) for (let r = 0; r < node.children.length; r++) {
1051
+ const line = r === 0 ? startLine : startLine + 1 + r;
1052
+ const cells = node.children[r]?.children;
1053
+ if (!Array.isArray(cells)) continue;
1054
+ for (const cell of cells) {
1055
+ const cellSource = cell?.data?.sourceMap;
1056
+ if (cellSource) pushSegment(cellSource, emitted, line, line);
1057
+ }
1058
+ }
1059
+ return emitted;
1060
+ };
1061
+ const markdown = toMarkdown(mdast, {
1062
+ emphasis: "_",
1063
+ ...mergedOptions.remarkStringifyOptions ?? {},
1064
+ extensions: stripExtensionHandlers(extensions),
1065
+ handlers: wrappedHandlers
1066
+ });
1067
+ const deduped = /* @__PURE__ */ new Map();
1068
+ for (const seg of segments) {
1069
+ const existing = deduped.get(seg.pathKey);
1070
+ const span = seg.endLine - seg.startLine;
1071
+ const existingSpan = existing ? existing.endLine - existing.startLine : Number.POSITIVE_INFINITY;
1072
+ if (!existing || span <= existingSpan) deduped.set(seg.pathKey, seg);
1073
+ }
1074
+ const ordered = Array.from(deduped.values()).sort((a, b) => comparePaths(a.path, b.path));
1075
+ return {
1076
+ allSegments: ordered,
1077
+ markdown,
1078
+ segments: ordered.filter((seg) => !ordered.some((other) => other !== seg && isPathPrefix(seg.path, other.path)))
1079
+ };
1080
+ };
1081
+
1082
+ //#endregion
1083
+ //#region src/lib/serializer/resolveSelectionLines.ts
1084
+ const MDX_TAG_RE = /<[a-zA-Z_][\w.-]*/;
1085
+ /** `a` is an ancestor-or-self of `b`: a's path is a prefix of (or equal to) b's. */
1086
+ const isAncestorOrSelf = (a, b) => a.length <= b.length && a.every((v, i) => b[i] === v);
1087
+ const comparePoints = (a, b) => {
1088
+ const len = Math.max(a.path.length, b.path.length);
1089
+ for (let i = 0; i < len; i++) {
1090
+ const av = a.path[i] ?? 0;
1091
+ const bv = b.path[i] ?? 0;
1092
+ if (av !== bv) return av - bv;
1093
+ }
1094
+ return a.offset - b.offset;
1095
+ };
1096
+ /**
1097
+ * Find the segment covering the start of a selection at `path`.
1098
+ *
1099
+ * 1. Ancestor-or-self: segment whose path is a prefix of `path` (most specific wins)
1100
+ * 2. First descendant: `path` is a prefix of segment's path (container selection)
1101
+ */
1102
+ const findStartSegment = (segments, path) => {
1103
+ let best = null;
1104
+ let bestLen = -1;
1105
+ for (const seg of segments) if (isAncestorOrSelf(seg.path, path) && seg.path.length > bestLen) {
1106
+ best = seg;
1107
+ bestLen = seg.path.length;
1108
+ }
1109
+ if (best) return best;
1110
+ for (const seg of segments) if (isAncestorOrSelf(path, seg.path)) return seg;
1111
+ return null;
1112
+ };
1113
+ /**
1114
+ * Find the segment covering the end of a selection at `path`.
1115
+ *
1116
+ * Same as findStartSegment but for descendant matches picks the LAST one
1117
+ * (the container's last child segment).
1118
+ */
1119
+ const findEndSegment = (segments, path) => {
1120
+ let best = null;
1121
+ let bestLen = -1;
1122
+ for (const seg of segments) if (isAncestorOrSelf(seg.path, path) && seg.path.length > bestLen) {
1123
+ best = seg;
1124
+ bestLen = seg.path.length;
1125
+ }
1126
+ if (best) return best;
1127
+ let last = null;
1128
+ for (const seg of segments) if (isAncestorOrSelf(path, seg.path)) last = seg;
1129
+ return last;
1130
+ };
1131
+ /** Recursively compute the total text length of a Slate node. */
1132
+ const getTextLength = (node) => {
1133
+ if (typeof node?.text === "string") return node.text.length;
1134
+ if (Array.isArray(node?.children)) return node.children.reduce((sum, child) => sum + getTextLength(child), 0);
1135
+ return 0;
1136
+ };
1137
+ /** Walk the editor tree to reach the node at `path`. */
1138
+ const getNodeAtPath = (editor, path) => {
1139
+ let node = { children: editor.children };
1140
+ for (const idx of path) {
1141
+ if (!node?.children?.[idx]) return null;
1142
+ node = node.children[idx];
1143
+ }
1144
+ return node;
1145
+ };
1146
+ /**
1147
+ * Compute the absolute character offset within a segment's text.
1148
+ *
1149
+ * Given a segment at `segPath` (e.g. [1,3,0] = a paragraph) and a selection
1150
+ * point at `selPath` (e.g. [1,3,0,1] = second text node) with `selOffset`,
1151
+ * sum up text lengths of all Slate nodes preceding the selection point within
1152
+ * the segment, then add `selOffset`.
1153
+ */
1154
+ const computeAbsoluteOffset = (editor, segPath, selPath, selOffset) => {
1155
+ const segNode = getNodeAtPath(editor, segPath);
1156
+ if (!segNode) return null;
1157
+ const remainingPath = selPath.slice(segPath.length);
1158
+ if (remainingPath.length === 0) {
1159
+ let textOffset = 0;
1160
+ const children = segNode.children ?? [];
1161
+ for (let i = 0; i < Math.min(selOffset, children.length); i++) textOffset += getTextLength(children[i]);
1162
+ return textOffset;
1163
+ }
1164
+ let current = segNode;
1165
+ let accumulated = 0;
1166
+ for (const childIdx of remainingPath) {
1167
+ const children = current.children ?? [];
1168
+ for (let i = 0; i < childIdx && i < children.length; i++) accumulated += getTextLength(children[i]);
1169
+ if (!children[childIdx]) return null;
1170
+ current = children[childIdx];
1171
+ }
1172
+ return accumulated + selOffset;
1173
+ };
1174
+ /**
1175
+ * Given a segment and an absolute text offset, determine which line within
1176
+ * the segment the offset falls on.
1177
+ */
1178
+ const offsetToLine = (segText, segStartLine, absOffset) => {
1179
+ const clamped = Math.max(0, Math.min(absOffset, segText.length));
1180
+ return segStartLine + (segText.slice(0, clamped).match(/\n/g) ?? []).length;
1181
+ };
1182
+ /**
1183
+ * Whether a segment supports sub-line narrowing. Only for kinds where text
1184
+ * content lines map 1:1 to markdown output lines (no structural wrappers
1185
+ * like code fences, MDX tags, or table delimiters).
1186
+ */
1187
+ const NARROWABLE_KINDS = new Set([
1188
+ "paragraph",
1189
+ "heading",
1190
+ "list_item",
1191
+ "blockquote"
1192
+ ]);
1193
+ const canNarrow = (seg) => NARROWABLE_KINDS.has(seg.kind);
1194
+ const extractLines = (md, start, end) => md.split("\n").slice(start - 1, end).join("\n");
1195
+ /**
1196
+ * Resolve which markdown lines correspond to a Plate editor selection
1197
+ * using the source-map path mapping (no text matching).
1198
+ *
1199
+ * 1. Serializes editor to markdown with source-map segments
1200
+ * 2. Finds the segment matching the anchor path (longest prefix)
1201
+ * 3. Finds the segment matching the focus path
1202
+ * 4. Narrows to sub-line precision using selection offsets
1203
+ * 5. Returns the line range and extracted markdown
1204
+ */
1205
+ /**
1206
+ * When the selection lands on a container path (e.g. clicking an MDX label),
1207
+ * look up the container segment in `allSegments` to include the MDX wrapper
1208
+ * lines (opening/closing tags) rather than just the inner content.
1209
+ */
1210
+ const tryContainerExpand = ({ allSegments, leafSeg, selPath, currentLine, direction }) => {
1211
+ if (!leafSeg) return currentLine;
1212
+ if (selPath.length >= leafSeg.path.length) return currentLine;
1213
+ let containerSeg = null;
1214
+ let containerLen = -1;
1215
+ for (const seg of allSegments) if (isAncestorOrSelf(seg.path, selPath) && seg.path.length > containerLen && seg !== leafSeg) {
1216
+ containerSeg = seg;
1217
+ containerLen = seg.path.length;
1218
+ }
1219
+ for (const seg of allSegments) if (seg.path.length === selPath.length && seg.path.every((v, i) => selPath[i] === v)) {
1220
+ containerSeg = seg;
1221
+ break;
1222
+ }
1223
+ if (!containerSeg) return currentLine;
1224
+ return direction === "start" ? Math.min(currentLine, containerSeg.startLine) : Math.max(currentLine, containerSeg.endLine);
1225
+ };
1226
+ const resolveSelectionByPath = (editor, selection, options) => {
1227
+ const { allSegments, markdown, segments } = serializeMdWithSourceMap(editor, options);
1228
+ const [start, end] = comparePoints(selection.anchor, selection.focus) <= 0 ? [selection.anchor, selection.focus] : [selection.focus, selection.anchor];
1229
+ const startSeg = findStartSegment(segments, start.path);
1230
+ const endSeg = findEndSegment(segments, end.path);
1231
+ if (!startSeg && !endSeg) return {
1232
+ containsMdx: false,
1233
+ endLine: 1,
1234
+ extractedMarkdown: "",
1235
+ markdown,
1236
+ segments,
1237
+ startLine: 1
1238
+ };
1239
+ let startLine = startSeg?.startLine ?? endSeg.startLine;
1240
+ let endLine = endSeg?.endLine ?? startSeg.endLine;
1241
+ if (startSeg && startSeg.endLine > startSeg.startLine && canNarrow(startSeg)) {
1242
+ const absOffset = computeAbsoluteOffset(editor, startSeg.path, start.path, start.offset);
1243
+ if (absOffset !== null) startLine = offsetToLine(startSeg.text, startSeg.startLine, absOffset);
1244
+ }
1245
+ if (endSeg && endSeg.endLine > endSeg.startLine && canNarrow(endSeg)) {
1246
+ const absOffset = computeAbsoluteOffset(editor, endSeg.path, end.path, end.offset);
1247
+ if (absOffset !== null) endLine = offsetToLine(endSeg.text, endSeg.startLine, absOffset);
1248
+ }
1249
+ startLine = tryContainerExpand({
1250
+ allSegments,
1251
+ currentLine: startLine,
1252
+ direction: "start",
1253
+ leafSeg: startSeg,
1254
+ selPath: start.path
1255
+ });
1256
+ endLine = tryContainerExpand({
1257
+ allSegments,
1258
+ currentLine: endLine,
1259
+ direction: "end",
1260
+ leafSeg: endSeg,
1261
+ selPath: end.path
1262
+ });
1263
+ if (startLine > endLine) [startLine, endLine] = [endLine, startLine];
1264
+ const extractedMarkdown = extractLines(markdown, startLine, endLine);
1265
+ return {
1266
+ containsMdx: extractedMarkdown.includes("<") && MDX_TAG_RE.test(extractedMarkdown),
1267
+ endLine,
1268
+ extractedMarkdown,
1269
+ markdown,
1270
+ segments,
1271
+ startLine
1272
+ };
1273
+ };
656
1274
 
657
1275
  //#endregion
658
1276
  //#region src/lib/serializer/serializeInlineMd.ts
@@ -1934,4 +2552,4 @@ const remarkMention = () => (tree) => {
1934
2552
  };
1935
2553
 
1936
2554
  //#endregion
1937
- 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, splitIncompleteMdx, stripMarkdown, stripMarkdownBlocks, stripMarkdownInline, tagRemarkPlugin, unreachable, wrapWithBlockId };
2555
+ 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.6",
3
+ "version": "52.3.9",
4
4
  "description": "Markdown serializer plugin for Plate",
5
5
  "keywords": [
6
6
  "markdown",
@@ -33,6 +33,7 @@
33
33
  "dependencies": {
34
34
  "lodash": "^4.17.23",
35
35
  "marked": "^15.0.12",
36
+ "mdast-util-to-markdown": "^2.1.2",
36
37
  "mdast-util-math": "3.0.0",
37
38
  "mdast-util-mdx": "3.0.0",
38
39
  "react-compiler-runtime": "^1.0.0",
@@ -48,8 +49,8 @@
48
49
  "@types/unist": "^3.0.3",
49
50
  "remark-gfm": "4.0.1",
50
51
  "remark-math": "6.0.0",
51
- "@plate/scripts": "1.0.0",
52
- "platejs": "npm:@lofcz/platejs@52.3.6"
52
+ "platejs": "npm:@lofcz/platejs@52.3.6",
53
+ "@plate/scripts": "1.0.0"
53
54
  },
54
55
  "peerDependencies": {
55
56
  "platejs": ">=52.0.11",