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

Commit

Permalink
Always use the text editor to highlight code blocks
Browse files Browse the repository at this point in the history
When rendering HTML to be viewed outside of Atom, convert the text 
editor into standard elements using direct DOM manipulation. This will 
allow for greater consistency between the styling of highlighted code 
blocks and Atom's usual rendering.
  • Loading branch information
maxbrunsfeld committed Aug 8, 2018
1 parent 6a502af commit 5ed1b10
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 76 deletions.
137 changes: 64 additions & 73 deletions lib/renderer.coffee
@@ -1,8 +1,8 @@
{TextEditor} = require 'atom'
path = require 'path'
cheerio = require 'cheerio'
createDOMPurify = require 'dompurify'
fs = require 'fs-plus'
Highlights = require 'highlights'
roaster = null # Defer until used
{scopeForFenceName} = require './extension-helper'

Expand All @@ -11,25 +11,21 @@ highlighter = null
packagePath = path.dirname(__dirname)

exports.toDOMFragment = (text='', filePath, grammar, callback) ->
render text, filePath, (error, html) ->
render text, filePath, (error, domFragment) ->
return callback(error) if error?

template = document.createElement('template')
template.innerHTML = html
domFragment = template.content.cloneNode(true)

# Default code blocks to be coffee in Literate CoffeeScript files
defaultCodeLanguage = 'coffee' if grammar?.scopeName is 'source.litcoffee'
convertCodeBlocksToAtomEditors(domFragment, defaultCodeLanguage)
callback(null, domFragment)
highlightCodeBlocks(domFragment, grammar, makeAtomEditorNonInteractive).then ->
callback(null, domFragment)

exports.toHTML = (text='', filePath, grammar, callback) ->
render text, filePath, (error, html) ->
render text, filePath, (error, domFragment) ->
return callback(error) if error?
# Default code blocks to be coffee in Literate CoffeeScript files
defaultCodeLanguage = 'coffee' if grammar?.scopeName is 'source.litcoffee'
html = tokenizeCodeBlocks(html, defaultCodeLanguage)
callback(null, html)

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

highlightCodeBlocks(div, grammar, convertAtomEditorToStandardElement).then ->
callback(null, div.innerHTML)

render = (text, filePath, callback) ->
roaster ?= require 'roaster'
Expand All @@ -45,14 +41,17 @@ render = (text, filePath, callback) ->
return callback(error) if error?

html = createDOMPurify().sanitize(html, {ALLOW_UNKNOWN_PROTOCOLS: atom.config.get('markdown-preview.allowUnsafeProtocols')})
html = resolveImagePaths(html, filePath)
callback(null, html.trim())

resolveImagePaths = (html, filePath) ->
template = document.createElement('template')
template.innerHTML = html.trim()
fragment = template.content.cloneNode(true)

resolveImagePaths(fragment, filePath)
callback(null, fragment)

resolveImagePaths = (element, filePath) ->
[rootDirectory] = atom.project.relativizePath(filePath)
o = document.createElement('div')
o.innerHTML = html
for img in o.querySelectorAll('img')
for img in 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.
if src = img.getAttribute('src')
Expand All @@ -68,60 +67,52 @@ resolveImagePaths = (html, filePath) ->
else
img.src = path.resolve(path.dirname(filePath), src)

o.innerHTML
highlightCodeBlocks = (domFragment, grammar, editorCallback) ->
if grammar?.scopeName is 'source.litcoffee'
defaultLanguage = 'coffee'
else
defaultLanguage = 'text'

convertCodeBlocksToAtomEditors = (domFragment, defaultLanguage='text') ->
if fontFamily = atom.config.get('editor.fontFamily')
for codeElement in domFragment.querySelectorAll('code')
codeElement.style.fontFamily = fontFamily

promises = []
for preElement in domFragment.querySelectorAll('pre')
codeBlock = preElement.firstElementChild ? preElement
fenceName = codeBlock.getAttribute('class')?.replace(/^lang-/, '') ? defaultLanguage

editorElement = document.createElement('atom-text-editor')

preElement.parentNode.insertBefore(editorElement, preElement)
preElement.remove()

editor = editorElement.getModel()
lastNewlineIndex = codeBlock.textContent.search(/\r?\n$/)
editor.setText(codeBlock.textContent.substring(0, lastNewlineIndex)) # Do not include a trailing newline
editorElement.setAttributeNode(document.createAttribute('gutter-hidden')) # Hide gutter
editorElement.removeAttribute('tabindex') # Make read-only

if grammar = atom.grammars.grammarForScopeName(scopeForFenceName(fenceName))
editor.setGrammar(grammar)

# Remove line decorations from code blocks.
for cursorLineDecoration in editor.cursorLineDecorations
cursorLineDecoration.destroy()

domFragment

tokenizeCodeBlocks = (html, defaultLanguage='text') ->
o = document.createElement('div')
o.innerHTML = html

if fontFamily = atom.config.get('editor.fontFamily')
for codeElement in o.querySelectorAll('code')
codeElement.style['font-family'] = fontFamily

for preElement in o.querySelectorAll("pre")
codeBlock = preElement.children[0]
fenceName = codeBlock.className?.replace(/^lang-/, '') ? defaultLanguage

highlighter ?= new Highlights(registry: atom.grammars, scopePrefix: 'syntax--')
highlightedHtml = highlighter.highlightSync
fileContents: codeBlock.textContent
scopeName: scopeForFenceName(fenceName)

highlightedBlock = document.createElement('pre')
highlightedBlock.innerHTML = highlightedHtml
# The `editor` class messes things up as `.editor` has absolutely positioned lines
highlightedBlock.children[0].classList.remove('editor')
highlightedBlock.children[0].classList.add("lang-#{fenceName}")

preElement.outerHTML = highlightedBlock.innerHTML

o.innerHTML
do (preElement) ->
codeBlock = preElement.firstElementChild ? preElement
fenceName = codeBlock.getAttribute('class')?.replace(/^lang-/, '') ? defaultLanguage
preElement.classList.add('editor-colors', "lang-#{fenceName}")
editor = new TextEditor({readonly: true, keyboardInputEnabled: false})
editor.setText(codeBlock.textContent.replace(/\r?\n$/, ''))
editorElement = editor.getElement()
editorElement.setUpdatedSynchronously(true)
preElement.innerHTML = ''
preElement.parentNode.insertBefore(editorElement, preElement)
promises.push(editorCallback(editorElement, preElement))
atom.grammars.assignLanguageMode(editor, scopeForFenceName(fenceName))
Promise.all(promises)

makeAtomEditorNonInteractive = (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 cursorLineDecoration in editorElement.getModel().cursorLineDecorations
cursorLineDecoration.destroy()
return

convertAtomEditorToStandardElement = (editorElement, preElement) ->
editor = editorElement.getModel()
result = new Promise (resolve) ->
editor.onDidTokenize ->
for line in editorElement.querySelectorAll('.line:not(.dummy)')
line2 = document.createElement('div')
line2.className = line.className
line2.innerHTML = line.innerHTML
preElement.appendChild(line2)
editorElement.remove()
resolve()
editor.setVisible(true)
result
1 change: 0 additions & 1 deletion package.json
Expand Up @@ -11,7 +11,6 @@
"dependencies": {
"dompurify": "^1.0.2",
"fs-plus": "^3.0.0",
"highlights": "^3.1.1",
"roaster": "^1.2.1",
"underscore-plus": "^1.0.0"
},
Expand Down
12 changes: 10 additions & 2 deletions spec/markdown-preview-spec.coffee
Expand Up @@ -348,16 +348,22 @@ describe "Markdown Preview", ->
waitsForPromise ->
atom.workspace.open("subdir/simple.md")

runs ->
waitsForPromise ->
atom.commands.dispatch atom.workspace.getActiveTextEditor().getElement(), 'markdown-preview:copy-html'

runs ->
expect(atom.clipboard.read()).toBe """
<p><em>italic</em></p>
<p><strong>bold</strong></p>
<p>encoding \u2192 issue</p>
"""

atom.workspace.getActiveTextEditor().setSelectedBufferRange [[0, 0], [1, 0]]

waitsForPromise ->
atom.commands.dispatch atom.workspace.getActiveTextEditor().getElement(), 'markdown-preview:copy-html'

runs ->
expect(atom.clipboard.read()).toBe """
<p><em>italic</em></p>
"""
Expand All @@ -375,8 +381,10 @@ describe "Markdown Preview", ->
waitsForPromise ->
atom.workspace.open("subdir/file.markdown")

runs ->
waitsForPromise ->
atom.commands.dispatch atom.workspace.getActiveTextEditor().getElement(), 'markdown-preview:copy-html'

runs ->
preview = document.createElement('div')
preview.innerHTML = atom.clipboard.read()

Expand Down

0 comments on commit 5ed1b10

Please sign in to comment.