/** * MDAST <-> Lexical visitors for the structural plan components. * * Children are visited normally in both directions, so their content is made of * ordinary Lexical nodes and ordinary MDAST nodes — nothing is snapshotted into * a property, which is what keeps concurrent editing inside these containers * working. */ import { $createCalloutNode, $createResearchNode, $createTabNode, $createTabsNode, $isCalloutNode, $isResearchNode, $isTabNode, $isTabsNode, } from "./containers"; import { attribute, identity, isFlow, PRIORITY } from "./shared"; import type { LexicalExportVisitor, MdastImportVisitor } from "@mdxeditor/editor"; import type { ElementNode, LexicalNode } from "lexical"; import type { MdxJsxFlowElement } from "mdast-util-mdx-jsx"; import type { CalloutNode, CalloutType, ResearchNode, TabNode, TabsNode } from "./containers"; type Importer = MdastImportVisitor; type Exporter = LexicalExportVisitor; /** Import a flow component as an element node and visit its children into it. */ function importer(name: string, create: (node: MdxJsxFlowElement) => LexicalNode): Importer { return { testNode: isFlow(name), visitNode({ mdastNode, actions }) { actions.addAndStepInto(create(mdastNode)); }, priority: PRIORITY, }; } /** Export an element node as a flow component and visit its children into it. */ function exporter( name: string, test: (node: LexicalNode | null | undefined) => node is T, attrs: (node: T) => ReturnType, ): Exporter { return { testLexicalNode: (node): node is T => test(node), visitLexicalNode({ lexicalNode, actions }) { actions.addAndStepInto("mdxJsxFlowElement", { name, attributes: attrs(lexicalNode) }); }, priority: PRIORITY, }; } export const MdastTabsVisitor = importer( "Tabs", node => $createTabsNode(attribute(node, "id") ?? ""), ); export const MdastTabVisitor = importer( "Tab", node => $createTabNode(attribute(node, "id") ?? "", attribute(node, "label") ?? ""), ); export const MdastCalloutVisitor = importer("Callout", node => $createCalloutNode( attribute(node, "id") ?? "", (attribute(node, "type") ?? "note") as CalloutType, attribute(node, "title") ?? "", )); export const MdastResearchVisitor: Importer = { testNode: isFlow("Research"), visitNode({ mdastNode, lexicalParent }) { (lexicalParent as ElementNode).append( $createResearchNode(attribute(mdastNode, "id") ?? ""), ); }, priority: PRIORITY, }; export const LexicalTabsVisitor: Exporter = exporter( "Tabs", $isTabsNode, node => identity(node.getId()), ); export const LexicalTabVisitor: Exporter = exporter( "Tab", $isTabNode, node => identity(node.getId(), { label: node.getLabel() }), ); export const LexicalCalloutVisitor: Exporter = exporter( "Callout", $isCalloutNode, node => identity(node.getId(), { type: node.getCalloutType(), title: node.getTitle() }), ); export const LexicalResearchVisitor: Exporter = { testLexicalNode: $isResearchNode, visitLexicalNode({ lexicalNode, mdastParent, actions }) { actions.appendToParent(mdastParent, { type: "mdxJsxFlowElement", name: "Research", attributes: identity(lexicalNode.getId()), children: [], }); }, priority: PRIORITY, }; export const CONTAINER_IMPORT_VISITORS = [ MdastTabsVisitor, MdastTabVisitor, MdastCalloutVisitor, MdastResearchVisitor, ]; export const CONTAINER_EXPORT_VISITORS = [ LexicalTabsVisitor, LexicalTabVisitor, LexicalCalloutVisitor, LexicalResearchVisitor, ];