import type { Node, Schema } from 'prosemirror-model'; import { Token } from './types.js'; import { DocumentMarkdownInlineTokenizer, DocumentMarkdownTokenizerSpec, MarkTokenizerSpec, } from './DocumentMarkdownInlineTokenizer.js'; const blankNode: DocumentMarkdownTokenizerSpec = { open: '', close: '' }; export class DocumentMarkdownTokenizer { private tokens: Array = []; inlineTokenizer: DocumentMarkdownInlineTokenizer; constructor( readonly nodes: { [node: string]: ( node: Node, index: number, ) => DocumentMarkdownTokenizerSpec; }, readonly marks: { [mark: string]: MarkTokenizerSpec }, readonly schema: Schema, readonly options: { hardBreakNodeName: string } = { hardBreakNodeName: 'hard_break', }, ) { this.inlineTokenizer = new DocumentMarkdownInlineTokenizer( nodes, marks, options, schema, ); } async iterateNode(node: Node, currentPos: number, idx = 0, level = -1) { const nodeSpec = this.nodes[node.type.name] ? this.nodes[node.type.name](node, currentPos) : blankNode; let tag = ''; if (node.type.spec.toDOM) { const dom = node.type.spec.toDOM(node); if (Array.isArray(dom) && dom.length > 0) { tag = dom[0]; } } if (nodeSpec.selfClose) { let selfCloseToken; if (typeof nodeSpec.selfClose === 'string') { const token = new Token(nodeSpec.selfClose, tag, 0); token.level = level; token.map = [currentPos]; this.tokens.push(token); selfCloseToken = token; } else { const token = await nodeSpec.selfClose(node, currentPos, idx); token.level = level; token.map = [currentPos]; this.tokens.push(token); selfCloseToken = token; } if (['before', 'both'].includes(nodeSpec.margin || '')) { selfCloseToken.attrSet('margin_before', '1'); } if (['after', 'both'].includes(nodeSpec.margin || '')) { selfCloseToken.attrSet('margin_after', '1'); } return; } let openToken: Token | undefined = undefined; if (nodeSpec.open) { if (typeof nodeSpec.open === 'string') { const token = new Token(nodeSpec.open, tag, 1); token.level = level; token.meta = 'nodeSpec.open'; token.map = [currentPos]; this.tokens.push(token); openToken = token; } else { const token = await nodeSpec.open(node, currentPos, idx); token.level = level; token.meta = 'nodeSpec.open()'; token.map = [currentPos]; this.tokens.push(token); openToken = token; } if (['before', 'both'].includes(nodeSpec.margin || '')) { openToken.attrSet('margin_before', '1'); } } if (node.inlineContent) { const token = new Token('inline', '', 0); token.level = level + 1; token.map = [currentPos]; this.tokens.push(token); token.children = await this.inlineTokenizer.renderInline( node, currentPos + 1, level + 1, ); } else if (node.childCount > 0) { let offset = 0; for (let idx = 0; idx < node.childCount; idx++) { const child = node.child(idx); await this.iterateNode(child, currentPos + offset + 1, idx, level + 1); offset += child.nodeSize; } } if (nodeSpec.close) { if (!openToken) { throw new Error('No openToken'); } let closeToken; if (typeof nodeSpec.close === 'string') { const token = new Token(nodeSpec.close, tag, -1); token.meta = 'nodeSpec.close'; token.level = level; // token.map = [currentPos]; this.tokens.push(token); closeToken = token; } else { const token = await nodeSpec.close(node, currentPos, idx, openToken); token.meta = 'nodeSpec.close()'; token.level = level; // token.map = [currentPos]; this.tokens.push(token); closeToken = token; } if (['after', 'both'].includes(nodeSpec.margin || '')) { closeToken.attrSet('margin_after', '1'); } } } async serialize(content: Node, options: {} = {}): Promise { await this.iterateNode(content, -1, 0); // doc does not have index, so I put -1 for correct calculation return this.tokens; } }