const { TextEditor } = require('atom') const path = require('path') const createDOMPurify = require('dompurify') const emoji = require('emoji-images') const fs = require('fs-plus') let commonmark = null // Defer until used let renderer = null let cheerio = null let yamlFrontMatter = null const { scopeForFenceName } = require('./extension-helper') const { resourcePath } = atom.getLoadSettings() const packagePath = path.dirname(__dirname) const emojiFolder = path.join( path.dirname(require.resolve('emoji-images')), 'pngs' ) exports.toDOMFragment = async function (text, filePath, grammar, callback) { if (text == null) { text = '' } const domFragment = render(text, filePath) await highlightCodeBlocks(domFragment, grammar, makeAtomEditorNonInteractive) return domFragment } exports.toHTML = async function (text, filePath, grammar) { if (text == null) { text = '' } const domFragment = render(text, filePath) const div = document.createElement('div') div.appendChild(domFragment) document.body.appendChild(div) await highlightCodeBlocks(div, grammar, convertAtomEditorToStandardElement) const result = div.innerHTML div.remove() return result } var render = function (text, filePath) { if (commonmark == null || yamlFrontMatter == null || cheerio == null) { commonmark = require('commonmark') yamlFrontMatter = require('yaml-front-matter') cheerio = require('cheerio') renderer = new commonmark.Renderer() renderer.listitem = function (text, isTask) { const listAttributes = isTask ? ' class="task-list-item"' : '' return `
  • ${text}
  • \n` } } // commonmark.setOptions({ // sanitize: false, // breaks: atom.config.get('markdown-preview.breakOnSingleNewline'), // renderer // }) const { __content, ...vars } = yamlFrontMatter.loadFront(text) const parser = new commonmark.Parser() const writer = new commonmark.HtmlRenderer() const parsed = parser.parse(renderYamlTable(vars) + __content) let html = writer.render(parsed) // emoji-images is too aggressive, so replace images in monospace tags with the actual emoji text. const $ = cheerio.load(emoji(html, emojiFolder, 20)) $('pre img').each((index, element) => $(element).replaceWith($(element).attr('title')) ) $('code img').each((index, element) => $(element).replaceWith($(element).attr('title')) ) html = $.html() html = createDOMPurify().sanitize(html, { ALLOW_UNKNOWN_PROTOCOLS: atom.config.get( 'markdown-preview.allowUnsafeProtocols' ) }) const template = document.createElement('template') template.innerHTML = html.trim() const fragment = template.content.cloneNode(true) resolveImagePaths(fragment, filePath) return fragment } function renderYamlTable (variables) { const entries = Object.entries(variables) if (!entries.length) { return '' } const markdownRows = [ entries.map(entry => entry[0]), entries.map(entry => '--'), entries.map(entry => entry[1]) ] return ( markdownRows.map(row => '| ' + row.join(' | ') + ' |').join('\n') + '\n' ) } var resolveImagePaths = function (element, filePath) { const [rootDirectory] = atom.project.relativizePath(filePath) const result = [] for (const img of element.querySelectorAll('img')) { // We use the raw attribute instead of the .src property because the value // of the property seems to be transformed in some cases. let src if ((src = img.getAttribute('src'))) { if (src.match(/^(https?|atom):\/\//)) { continue } if (src.startsWith(process.resourcesPath)) { continue } if (src.startsWith(resourcePath)) { continue } if (src.startsWith(packagePath)) { continue } if (src[0] === '/') { if (!fs.isFileSync(src)) { if (rootDirectory) { result.push((img.src = path.join(rootDirectory, src.substring(1)))) } else { result.push(undefined) } } else { result.push(undefined) } } else { result.push((img.src = path.resolve(path.dirname(filePath), src))) } } else { result.push(undefined) } } return result } var highlightCodeBlocks = function (domFragment, grammar, editorCallback) { let defaultLanguage, fontFamily if ( (grammar != null ? grammar.scopeName : undefined) === 'source.litcoffee' ) { defaultLanguage = 'coffee' } else { defaultLanguage = 'text' } if ((fontFamily = atom.config.get('editor.fontFamily'))) { for (const codeElement of domFragment.querySelectorAll('code')) { codeElement.style.fontFamily = fontFamily } } const promises = [] for (const preElement of domFragment.querySelectorAll('pre')) { const codeBlock = preElement.firstElementChild != null ? preElement.firstElementChild : preElement const className = codeBlock.getAttribute('class') const fenceName = className != null ? className.replace(/^language-/, '') : defaultLanguage const editor = new TextEditor({ readonly: true, keyboardInputEnabled: false }) const editorElement = editor.getElement() preElement.classList.add('editor-colors', `lang-${fenceName}`) editorElement.setUpdatedSynchronously(true) preElement.innerHTML = '' preElement.parentNode.insertBefore(editorElement, preElement) editor.setText(codeBlock.textContent.replace(/\r?\n$/, '')) atom.grammars.assignLanguageMode(editor, scopeForFenceName(fenceName)) editor.setVisible(true) promises.push(editorCallback(editorElement, preElement)) } return Promise.all(promises) } var makeAtomEditorNonInteractive = function (editorElement, preElement) { preElement.remove() editorElement.setAttributeNode(document.createAttribute('gutter-hidden')) // Hide gutter editorElement.removeAttribute('tabindex') // Make read-only // Remove line decorations from code blocks. for (const cursorLineDecoration of editorElement.getModel() .cursorLineDecorations) { cursorLineDecoration.destroy() } } var convertAtomEditorToStandardElement = (editorElement, preElement) => { return new Promise(function (resolve) { const editor = editorElement.getModel() const done = () => editor.component.getNextUpdatePromise().then(function () { for (const line of editorElement.querySelectorAll( '.line:not(.dummy)' )) { const line2 = document.createElement('div') line2.className = 'line' line2.innerHTML = line.firstChild.innerHTML preElement.appendChild(line2) } editorElement.remove() resolve() }) const languageMode = editor.getBuffer().getLanguageMode() if (languageMode.fullyTokenized || languageMode.tree) { done() } else { editor.onDidTokenize(done) } }) }