"use strict"; /* * hopdown.ts * - Configurable markdown <=> HTML converter. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.HopDown = void 0; exports.escapeHtml = escapeHtml; const defaults_1 = require("./defaults"); const tokenizer_1 = require("./tokenizer"); function escapeHtml(source) { return source .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } /** * A configurable markdown <=> HTML converter. * * Examples: * * const hopdown = new HopDown(); * * const html = hopdown.toHTML('**bold**'); * html === '
bold
'; * * const markdown = hopdown.toMarkdown(html); * markdown === '**bold**'; * * const node = hopdown.toEditorNode('**bold**'); * (node as HTMLElement).outerHTML === ( * "' + this.inlineToHTML(markdown) + '
'; }; /** * Recursive entrypoint for converting markdown to html. */ inlineToHTML = (markdown) => { const tokens = this.tokenize(markdown); return this.tokensToHTML(tokens); }; /** * Recursive entrypoint for converting markdown to editor nodes. */ inlineToEditorNodes = (markdown) => { const tokens = this.tokenize(markdown); return this.tokensToEditorNodes(tokens); }; /** * Convert an HTML node to a markdown string. Used by toMarkdown(). */ nodeToMarkdown = (node) => { // DocumentFragment if (node.nodeType == 11) { return Array.from(node.childNodes).map(child => { return this.nodeToMarkdown(child); }).join(''); } // text if (node.nodeType == 3) { return node.textContent || ''; // not a block-level element } else if (node.nodeType !== 1) { return ''; } const tag = this.tags.getByElementName(node.nodeName); if (!tag) { return Array.from(node.childNodes) .map(child => this.nodeToMarkdown(child)) .join(''); } const ret = tag.toMarkdown(node, this.nodeToMarkdown); return ret; }; /** * Convert a token stream to editor nodes. */ tokensToEditorNodes(tokens) { // First pass: match open/close pairs using a stack const paired = this.pairDelimiters(tokens); // Second pass: build HTML from paired tokens const fragment = document.createDocumentFragment(); var grouping = []; const groupAsNode = () => { const div = document.createElement('div'); for (const node of grouping) { div.appendChild(node); } return div; }; for (const token of paired) { if (token.tag) { const thisNode = token.tag.toEditorNode(token, this.inlineToEditorNodes); if (!thisNode) { continue; } if (token.role == 'open') { if (grouping.length) { fragment.appendChild(groupAsNode()); } grouping = [thisNode]; continue; } if (token.role == 'close') { grouping.push(thisNode); fragment.appendChild(groupAsNode()); grouping = []; continue; } if (!grouping.length) { fragment.appendChild(thisNode.cloneNode(true)); } continue; } const text = document.createTextNode(token.value); grouping.push(text); continue; } if (grouping.length) { fragment.appendChild(groupAsNode()); } return fragment; } /** * Convert a token stream to HTML. Matches open/close delimiter * pairs and wraps their content in the appropriate HTML tags. * Unmatched delimiters are emitted as literal text. */ tokensToHTML(tokens) { // First pass: match open/close pairs using a stack const paired = this.pairDelimiters(tokens); // Second pass: build HTML from paired tokens let html = ''; for (const token of paired) { if (token.tag) { html += token.tag.toHTML(token, this.inlineToHTML); continue; } html += escapeHtml(token.value); } return html; } /** * Match open/close delimiter pairs in a token stream. Unmatched * openers/closers are converted to text tokens so they render * as literal characters. */ pairDelimiters(tokens) { const openStack = []; const result = [...tokens]; // Track which delimiter types are currently open to prevent // forbidden nesting (e.g.