Skip to content
This repository has been archived by the owner on Dec 15, 2022. It is now read-only.

Stop using roaster for rendering markdown #559

Merged
merged 9 commits into from
Apr 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ module.exports = {
})
},

copyHTML () {
async copyHTML () {
const editor = atom.workspace.getActiveTextEditor()
if (editor == null) {
return
Expand All @@ -188,19 +188,13 @@ module.exports = {
renderer = require('./renderer')
}
const text = editor.getSelectedText() || editor.getText()
return new Promise(function (resolve) {
renderer.toHTML(text, editor.getPath(), editor.getGrammar(), function (
error,
html
) {
if (error) {
console.warn('Copying Markdown as HTML failed', error)
} else {
atom.clipboard.write(html)
resolve()
}
})
})
const html = await renderer.toHTML(
text,
editor.getPath(),
editor.getGrammar()
)

atom.clipboard.write(html)
},

saveAsHTML () {
Expand Down
97 changes: 43 additions & 54 deletions lib/markdown-preview-view.js
Original file line number Diff line number Diff line change
Expand Up @@ -287,40 +287,35 @@ module.exports = class MarkdownPreviewView {
}
}

getHTML (callback) {
return this.getMarkdownSource().then(source => {
if (source == null) {
return
}
async getHTML () {
const source = await this.getMarkdownSource()

return renderer.toHTML(
source,
this.getPath(),
this.getGrammar(),
callback
)
})
if (source == null) {
return
}

return renderer.toHTML(source, this.getPath(), this.getGrammar())
}

renderMarkdownText (text) {
async renderMarkdownText (text) {
const { scrollTop } = this.element
return renderer.toDOMFragment(
text,
this.getPath(),
this.getGrammar(),
(error, domFragment) => {
if (error) {
this.showError(error)
} else {
this.loading = false
this.loaded = true
this.element.textContent = ''
this.element.appendChild(domFragment)
this.emitter.emit('did-change-markdown')
this.element.scrollTop = scrollTop
}
}
)

try {
const domFragment = await renderer.toDOMFragment(
text,
this.getPath(),
this.getGrammar()
)

this.loading = false
this.loaded = true
this.element.textContent = ''
this.element.appendChild(domFragment)
this.emitter.emit('did-change-markdown')
this.element.scrollTop = scrollTop
} catch (error) {
this.showError(error)
}
}

getTitle () {
Expand Down Expand Up @@ -439,7 +434,7 @@ module.exports = class MarkdownPreviewView {
selection.addRange(range)
}

copyToClipboard () {
async copyToClipboard () {
if (this.loading) {
return
}
Expand All @@ -456,16 +451,16 @@ module.exports = class MarkdownPreviewView {
) {
atom.clipboard.write(selectedText)
} else {
return this.getHTML(function (error, html) {
if (error != null) {
atom.notifications.addError('Copying Markdown as HTML failed', {
dismissable: true,
detail: error.message
})
} else {
atom.clipboard.write(html)
}
})
try {
const html = await this.getHTML()

atom.clipboard.write(html)
} catch (error) {
atom.notifications.addError('Copying Markdown as HTML failed', {
dismissable: true,
detail: error.message
})
}
}
}

Expand All @@ -484,7 +479,7 @@ module.exports = class MarkdownPreviewView {
return { defaultPath }
}

saveAs (htmlFilePath) {
async saveAs (htmlFilePath) {
if (this.loading) {
atom.notifications.addWarning(
'Please wait until the Markdown Preview has finished loading before saving'
Expand All @@ -498,13 +493,10 @@ module.exports = class MarkdownPreviewView {
title = path.parse(filePath).name
}

return new Promise((resolve, reject) => {
return this.getHTML((error, htmlBody) => {
if (error != null) {
throw error
} else {
const html =
`\
const htmlBody = await this.getHTML()

const html =
`\
<!DOCTYPE html>
<html>
<head>
Expand All @@ -515,10 +507,7 @@ module.exports = class MarkdownPreviewView {
<body class='markdown-preview' data-use-github-style>${htmlBody}</body>
</html>` + '\n' // Ensure trailing newline

fs.writeFileSync(htmlFilePath, html)
return atom.workspace.open(htmlFilePath).then(resolve)
}
})
})
fs.writeFileSync(htmlFilePath, html)
return atom.workspace.open(htmlFilePath)
}
}
141 changes: 87 additions & 54 deletions lib/renderer.js
Original file line number Diff line number Diff line change
@@ -1,85 +1,117 @@
const { TextEditor } = require('atom')
const path = require('path')
const createDOMPurify = require('dompurify')
const emoji = require('emoji-images')
const fs = require('fs-plus')
let roaster = null // Defer until used
let marked = null // Defer until used
let renderer = null
let cheerio = null
const yamlFrontMatter = require('yaml-front-matter')

const { scopeForFenceName } = require('./extension-helper')
const { resourcePath } = atom.getLoadSettings()
const packagePath = path.dirname(__dirname)

exports.toDOMFragment = function (text, filePath, grammar, callback) {
const emojiFolder = path.join(
path.dirname(require.resolve('emoji-images')),
'pngs'
)

exports.toDOMFragment = async function (text, filePath, grammar, callback) {
if (text == null) {
text = ''
}

render(text, filePath, function (error, domFragment) {
if (error != null) {
return callback(error)
}
highlightCodeBlocks(
domFragment,
grammar,
makeAtomEditorNonInteractive
).then(() => callback(null, domFragment))
})
const domFragment = render(text, filePath)

await highlightCodeBlocks(domFragment, grammar, makeAtomEditorNonInteractive)

return domFragment
}

exports.toHTML = function (text, filePath, grammar, callback) {
exports.toHTML = async function (text, filePath, grammar) {
if (text == null) {
text = ''
}

return render(text, filePath, function (error, domFragment) {
if (error != null) {
return callback(error)
}
const domFragment = render(text, filePath)
const div = document.createElement('div')

const div = document.createElement('div')
div.appendChild(domFragment)
document.body.appendChild(div)

return highlightCodeBlocks(
div,
grammar,
convertAtomEditorToStandardElement
).then(function () {
callback(null, div.innerHTML)
return div.remove()
})
})
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, callback) {
if (roaster == null) {
roaster = require('roaster')
var render = function (text, filePath) {
if (marked == null) {
cheerio = require('cheerio')
marked = require('marked')
renderer = new marked.Renderer()
renderer.listitem = function (text, isTask) {
const listAttributes = isTask ? ' class="task-list-item"' : ''

return `<li ${listAttributes}>${text}</li>\n`
}
}
const options = {

marked.setOptions({
sanitize: false,
breaks: atom.config.get('markdown-preview.breakOnSingleNewline')
}
breaks: atom.config.get('markdown-preview.breakOnSingleNewline'),
renderer
})

// Remove the <!doctype> since otherwise marked will escape it
// https://github.com/chjj/marked/issues/354
text = text.replace(/^\s*<!doctype(\s+.*)?>\s*/i, '')
const { __content, ...vars } = yamlFrontMatter.loadFront(text)

return roaster(text, options, function (error, html) {
if (error != null) {
return callback(error)
}
let html = marked(renderYamlTable(vars) + __content)

html = createDOMPurify().sanitize(html, {
ALLOW_UNKNOWN_PROTOCOLS: atom.config.get(
'markdown-preview.allowUnsafeProtocols'
)
})
// 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'))
)

const template = document.createElement('template')
template.innerHTML = html.trim()
const fragment = template.content.cloneNode(true)
html = $.html()

resolveImagePaths(fragment, filePath)
return callback(null, fragment)
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) {
Expand Down Expand Up @@ -150,7 +182,8 @@ var highlightCodeBlocks = function (domFragment, grammar, editorCallback) {
: preElement
const className = codeBlock.getAttribute('class')
const fenceName =
className != null ? className.replace(/^lang-/, '') : defaultLanguage
className != null ? className.replace(/^language-/, '') : defaultLanguage

const editor = new TextEditor({
readonly: true,
keyboardInputEnabled: false
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@
"atom": "*"
},
"dependencies": {
"cheerio": "^1.0.0-rc.3",
"dompurify": "^1.0.2",
"emoji-images": "^0.1.1",
"fs-plus": "^3.0.0",
"roaster": "^1.2.1",
"underscore-plus": "^1.0.0"
"marked": "^0.6.2",
"underscore-plus": "^1.0.0",
"yaml-front-matter": "^4.0.0"
},
"devDependencies": {
"coffeelint": "^1.9.7",
Expand Down
7 changes: 7 additions & 0 deletions spec/fixtures/subdir/file.markdown
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
---
variable1: value1
array:
- foo
- bar
---

## File.markdown

:cool:
Expand Down
Loading