import MathMl2LaTeX from 'mathml2latex'; import type { ContextStash, SerializerContext, TokenHandler, } from '../MarkdownSerializer.js'; import type { Token } from '../types.js'; import { fixCharacters } from '../utils.js'; export function escapeMarkdown( token: Token, current: SerializerContext, ): Array<[string, Token]> { const markdownChars = [ { char: '\\', escape: '\\\\' }, // { char: '*', escape: '\\*' }, // { char: '_', escape: '\\_' }, { char: '#', escape: '\\#' }, { char: '$', escape: '\\$' }, // { char: '[', escape: '\\[' }, // { char: ']', escape: '\\]' }, // { char: '(', escape: '\\(' }, // { char: ')', escape: '\\)' }, // { char: '`', escape: '\\`' }, // { char: '>', escape: '\\>' }, { char: '<', escape: '\\<' }, // { char: '.', escape: '\\.' }, // { char: '!', escape: '\\!' }, // { char: '|', escape: '\\|' }, // { char: '{', escape: '\\{' }, // { char: '}', escape: '\\}' }, { char: '…', escape: '...' }, { char: '©', escape: '(c)' }, { char: '®', escape: '(r)' }, { char: '™', escape: '(tm)' }, { char: '±', escape: '+-' }, { char: '—', escape: '---' }, ]; const escapeChars: string[] = (current.meta.escapeChars || '').split(''); const startPos = token.map && token.map.length > 0 ? token.map[0] : 0; if (!startPos) { let escapedText = fixCharacters(token.content); for (const { char, escape } of markdownChars) { if (escapeChars.includes(char)) { escapedText = escapedText.replaceAll(char, escape); } } return [[escapedText, token]]; } const charArr = fixCharacters(token.content).split(''); const retVal: Array<[string, Token]> = []; const markdownCharsMap: Record = Object.fromEntries( markdownChars.map((c) => [c.char, c.escape]), ); let offset = 0; let inStr = ''; let currentOutStr = ''; const flush = () => { if (currentOutStr.length > 0) { const tok = structuredClone(token); tok.map = [startPos + offset]; retVal.push([ currentOutStr, tok, ]); offset = inStr.length; } currentOutStr = ''; }; for (let idx = 0; idx < charArr.length; idx++) { const char = charArr[idx]; inStr += char; if (!escapeChars.includes(char)) { currentOutStr += char; continue; } if (markdownCharsMap[char]) { flush(); } currentOutStr += markdownCharsMap[char] || char; if (markdownCharsMap[char]) { flush(); } } flush(); return retVal; } function getLinkTokensHandlers(): Record> { return { 'text': [ (token: Token, ctx: ContextStash) => { ctx.current.meta['link_text'] += token.content; if (token.content && !ctx.current.meta['link_token_token']) { ctx.current.meta['link_token_token'] = token; } }, ], 'link_close': [ (token: Token, ctx: ContextStash) => { { const lastStackToken: Token = ctx.current.meta['link_open_token']; const href: string = lastStackToken.attrGet('href') || ''; const title: string = lastStackToken.attrGet('title') || ''; const origUrl: string = lastStackToken.attrGet('origUrl') || href; let link_text = ctx.current.meta['link_text']; if (origUrl === link_text) { link_text = href; } let mdTemplate = lastStackToken.attrGet('mdTemplate'); if (mdTemplate) { const basename = href.split('/').pop(); mdTemplate = mdTemplate.replaceAll('$href', href); mdTemplate = mdTemplate.replaceAll('$basename', basename || ''); mdTemplate = mdTemplate.replaceAll( '$label', ctx.current.meta['link_text'], ); mdTemplate = mdTemplate.replaceAll('$title', title || ''); ctx.current.log(mdTemplate, token); } else { if (!title && link_text === href) { ctx.current.log(href, token); } else if (!title && !href) { ctx.current.log('[', token); ctx.current.log( link_text, ctx.current.meta['link_token_token'], ); ctx.current.log(`]${title}`, token); } else { ctx.current.log('[', token); ctx.current.log( link_text, ctx.current.meta['link_token_token'], ); if (title) { ctx.current.log(`](${href} ${title})`, token); } else { ctx.current.log(`](${href})`, token); } } } ctx.unstash('getLinkTokensHandlers.link_close'); } }, ], default: [ (token: Token, ctx: ContextStash) => { // Ignore other stuff // TODO: images? }, ], }; } export function getInlineTokensHandlers(): Record> { return { 'text': [ (token: Token, ctx: ContextStash) => { if (token.meta === 'noEscText') { ctx.current.log(token.content); } else { for (const pair of escapeMarkdown(token, ctx.current)) { ctx.current.log(pair[0], pair[1]); } } }, ], 'entity': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.content); }, ], 'strong_open': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.markup || '**', token); }, ], 'strong_close': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.markup || '**', token); }, ], 'em_open': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.markup || '*', token); }, ], 'em_close': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.markup || '*', token); }, ], 'underline_open': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.markup || '_', token); }, ], 'underline_close': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.markup || '_', token); }, ], 'strike_open': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.markup || '~', token); }, ], 'strike_close': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.markup || '~', token); }, ], 'link_open': [ (token: Token, ctx: ContextStash) => { ctx.stash('getInlineTokensHandlers.link_open'); ctx.current.handlers = getLinkTokensHandlers(); ctx.current.meta['link_open_token'] = token; ctx.current.meta['link_text'] = ''; ctx.current.meta['link_token_token'] = null; }, ], 'code_open': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.markup || '`', token); }, ], 'code_close': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.markup || '`', token); }, ], 'code_inline': [ (token: Token, ctx: ContextStash) => { ctx.current.log('`' + token.content + '`', token); }, ], 'math': [ (token: Token, ctx: ContextStash) => { let content = token.content; if (token.attrGet('lang') === 'mathml') { const cleaned = content.replace( //g, '', ); content = MathMl2LaTeX.convert(cleaned); } ctx.current.log('$' + content + '$', token); }, ], 'hardbreak': [ (token: Token, ctx: ContextStash) => { ctx.current.log(ctx.current.lineBreak + '\n', token); }, ], 'softbreak': [ (token: Token, ctx: ContextStash) => { ctx.current.log('\n', token); }, ], 'image': [ (token: Token, ctx: ContextStash) => { { const src = token.attrGet('src'); const altAttr = token.attrGet('alt'); let alt = altAttr || ''; if (token.children) { for (const child of token.children) { if (child.type === 'text') { alt += child.content; } } } let mdTemplate = '![$alt]'; const title = token.attrGet('title'); if (src) { if (title) { mdTemplate += '($src $title)'; } else { mdTemplate += '($src)'; } } mdTemplate = token.attrGet('mdTemplate') || mdTemplate; mdTemplate = mdTemplate.replaceAll('$alt', alt); mdTemplate = mdTemplate.replaceAll('$label', alt); mdTemplate = mdTemplate.replaceAll('$src', src || ''); mdTemplate = mdTemplate.replaceAll('$title', title || ''); ctx.current.log( mdTemplate, token, ); } }, ], 'html_block': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.content, token); }, ], 'footnote_ref': [ (token: Token, ctx: ContextStash) => { if (token.meta.label) { ctx.current.log(`[^${token.meta.label}]`, token); } else { ctx.current.log(`[^footnote_${token.meta.id}]`, token); } }, ], 'shortcode_inline': [ (token: Token, ctx: ContextStash) => { ctx.current.log('{{' + token.content + '}}', token); }, ], 'bookmark': [ (token: Token, ctx: ContextStash) => { ctx.current.log(``, token); }, ], }; } function escapeHtml(text: string) { return text; // TODO } export function getHtmlInlineFormatTokensHandlers(): Record< string, Array > { return { 'strong_open': [ (token: Token, ctx: ContextStash) => { const tag = token.tag || 'strong'; ctx.current.log(`<${tag}>`, token); }, ], 'strong_close': [ (token: Token, ctx: ContextStash) => { const tag = token.tag || 'strong'; ctx.current.log(``, token); }, ], 'em_open': [ (token: Token, ctx: ContextStash) => { const tag = token.tag || 'em'; ctx.current.log(`<${tag}>`, token); }, ], 'em_close': [ (token: Token, ctx: ContextStash) => { const tag = token.tag || 'em'; ctx.current.log(``, token); }, ], 'strike_open': [ (token: Token, ctx: ContextStash) => { const tag = token.tag || 'strike'; ctx.current.log(`<${tag}>`, token); }, ], 'strike_close': [ (token: Token, ctx: ContextStash) => { const tag = token.tag || 'strike'; ctx.current.log(``, token); }, ], 'underline_open': [ (token: Token, ctx: ContextStash) => { const tag = token.tag || 'u'; ctx.current.log(`<${tag}>`, token); }, ], 'underline_close': [ (token: Token, ctx: ContextStash) => { const tag = token.tag || 'u'; ctx.current.log(``, token); }, ], }; } export function getHtmlInlineTokensHandlers(): Record< string, Array > { return { 'in_html': [], 'text': [ (token: Token, ctx: ContextStash) => { ctx.current.log(escapeHtml(token.content), token); }, ], 'entity': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.content, token); }, ], ...getHtmlInlineFormatTokensHandlers(), 'link_open': [ (token: Token, ctx: ContextStash) => { ctx.stash('getHtmlInlineTokensHandlers.link_open'); const href = token.attrGet('href') || ''; const titleValue = token.attrGet('title'); const title = titleValue ? ' "' + titleValue + '"' : ''; ctx.current.log(``, token); }, ], 'link_close': [ (token: Token, ctx: ContextStash) => { { ctx.current.log('', token); ctx.unstash('getHtmlInlineTokensHandlers.link_close'); } }, ], 'code_open': [ (token: Token, ctx: ContextStash) => { const tag = token.tag || 'code'; ctx.current.log(`<${tag}>`, token); }, ], 'code_close': [ (token: Token, ctx: ContextStash) => { const tag = token.tag || 'code'; ctx.current.log(`<${tag}>`, token); }, ], // 'code_inline': [ // (token: Token, ctx: ContextStash) => { // const tag = token.tag || 'code'; // ctx.current.log(`<${tag}>`, token); // ctx.current.log(token.content || '', token); // ctx.current.log(``, token); // }, // ], 'math': [ (token: Token, ctx: ContextStash) => { ctx.current.log( '', token, ); ctx.current.log(token.content, token); ctx.current.log('', token); }, ], 'hardbreak': [ (token: Token, ctx: ContextStash) => { const tag = token.tag || 'br'; ctx.current.log(`<${tag} />\n`, token); }, ], 'softbreak': [ (token: Token, ctx: ContextStash) => { const tag = token.tag || 'wbr'; ctx.current.log(`<${tag} />\n`, token); }, ], 'image': [ (token: Token, ctx: ContextStash) => { { // const src = token.attrGet('src'); // const alt = token.attrGet('alt'); let alt = ''; if (token.children) { for (const child of token.children) { if (child.type === 'text') { alt += child.content; } } } // const title = token.attrGet('title'); // ctx.current.log(`![${alt}]`, token); // if (src) { // ctx.current.log( // `(${src}${title ? ' "' + title + '"' : ''})`, token // ); // } // TODO const tag = token.tag || 'img'; ctx.current.log(`<${tag} />`, token); } }, ], 'html_block': [ (token: Token, ctx: ContextStash) => { ctx.current.log(token.content, token); }, ], 'footnote_ref': [ (token: Token, ctx: ContextStash) => { }, ], 'shortcode_inline': [ (token: Token, ctx: ContextStash) => { ctx.current.log('{{' + token.content + '}}', token); }, ], 'bookmark': [ (token: Token, ctx: ContextStash) => { ctx.current.log(``, token); }, ], }; }