/*
* hopdown.ts
* - Configurable markdown <=> HTML converter.
*/
import { defaultTags } from "./defaults";
import { Tokenizer } from "./tokenizer";
export 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 === (
* "" +
* "**" +
* "bold" +
* "" +
* "
"
* );
*/
export class HopDown {
tags;
referenceLinks = new Map();
tokenizer;
constructor(tags) {
this.tags = tags || defaultTags;
this.tokenizer = new Tokenizer(this.tags);
}
/**
* Convert a markdown string to tokens.
*/
tokenize(source) {
let text = source;
// Resolve reference links before tokenizing
text = this.resolveReferenceLinks(text);
// Normalize _ emphasis to *
text = this.normalizeUnderscores(text);
const tokens = this.tokenizer.tokenize(text);
return tokens;
}
/**
* Convert an HTML string or a DocumentFragment to markdown.
*/
toMarkdown = (html) => {
var md = '';
if (typeof (html) == 'string') {
const container = document.createElement('div');
container.innerHTML = html;
md = this.nodeToMarkdown(container);
}
else {
md = this.nodeToMarkdown(html);
}
return md.replace(/\n{3,}/g, '\n\n').trim();
};
/**
* Convert a markdown string to an HTML string.
*/
toHTML = (markdown) => {
const output = [];
const preprocessed = this.preprocessMarkdown(markdown);
if (!preprocessed.lines) {
return '';
}
const blockTags = this.tags.ordered.filter(t => {
return t.isBlock && t.constructor.name !== 'Paragraph';
});
const pTag = this.tags.getByElementName('p');
if (pTag === undefined) {
throw new Error("Cannot parse without a paragraph tag!");
}
let lineIndex = 0;
while (lineIndex < preprocessed.lines.length) {
let context;
let matched = false;
let token;
for (const tag of blockTags) {
context = {
lines: preprocessed.lines,
index: lineIndex,
text: '',
offset: 0,
};
token = tag.match(context, this.tags);
if (!token) {
continue;
}
output.push(tag.toHTML(token, this.inlineToHTML));
lineIndex += token.consumed;
matched = true;
break;
}
if (matched) {
continue;
}
/* always check the paragraph tag last */
token = pTag.match(context, this.tags);
if (token) {
output.push(pTag.toHTML(token, this.inlineToHTML));
lineIndex += token.consumed;
continue;
}
lineIndex++;
}
return output.join('\n');
};
/**
* Convert a markdown string to a DocumentFragment consisting of wysiwyg editor nodes.
*/
toEditorNode = (markdown) => {
const tokens = this.tokenize(markdown);
return this.tokensToEditorNodes(tokens);
};
/**
* 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);
};
/**
* Process a markdown string by splitting it into lines, stripping empty lines, and capturing reference links.
*/
preprocessMarkdown(markdown) {
const blankLine = /^\s*$/;
const refDefinition = /^\[(?