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

Always use the text editor to highlight code blocks #544

Merged
merged 1 commit into from
Aug 9, 2018
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
12 changes: 7 additions & 5 deletions lib/main.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,13 @@ module.exports =

renderer ?= require './renderer'
text = editor.getSelectedText() or editor.getText()
renderer.toHTML text, editor.getPath(), editor.getGrammar(), (error, html) ->
if error
console.warn('Copying Markdown as HTML failed', error)
else
atom.clipboard.write(html)
new Promise (resolve) ->
renderer.toHTML text, editor.getPath(), editor.getGrammar(), (error, html) ->
if error
console.warn('Copying Markdown as HTML failed', error)
else
atom.clipboard.write(html)
resolve()

saveAsHTML: ->
activePaneItem = atom.workspace.getActivePaneItem()
Expand Down
35 changes: 18 additions & 17 deletions lib/markdown-preview-view.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -330,20 +330,21 @@ class MarkdownPreviewView
if filePath
title = path.parse(filePath).name

@getHTML (error, htmlBody) =>
if error?
throw error
else
html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>#{title}</title>
<style>#{@getMarkdownPreviewCSS()}</style>
</head>
<body class='markdown-preview' data-use-github-style>#{htmlBody}</body>
</html>""" + "\n" # Ensure trailing newline

fs.writeFileSync(htmlFilePath, html)
atom.workspace.open(htmlFilePath)
new Promise (resolve, reject) =>
@getHTML (error, htmlBody) =>
if error?
throw error
else
html = """
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>#{title}</title>
<style>#{@getMarkdownPreviewCSS()}</style>
</head>
<body class='markdown-preview' data-use-github-style>#{htmlBody}</body>
</html>""" + "\n" # Ensure trailing newline

fs.writeFileSync(htmlFilePath, html)
atom.workspace.open(htmlFilePath).then(resolve)
139 changes: 67 additions & 72 deletions lib/renderer.coffee
Original file line number Diff line number Diff line change
@@ -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,22 @@ 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)
div.remove()

render = (text, filePath, callback) ->
roaster ?= require 'roaster'
Expand All @@ -45,14 +42,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 +68,55 @@ 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()

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})
editorElement = editor.getElement()
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))
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) ->
new Promise (resolve) ->
done = ->
for line in editorElement.querySelectorAll('.line:not(.dummy)')
line2 = document.createElement('div')
line2.className = 'line'
line2.innerHTML = line.firstChild.innerHTML
preElement.appendChild(line2)
editorElement.remove()
resolve()
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
if editor.getBuffer().getLanguageMode().fullyTokenized
done()
else
editor.onDidTokenize(done)
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
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
2 changes: 1 addition & 1 deletion spec/fixtures/saved-html.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
pre.editor-colors .hr { background: url('data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAYAAACtBE5DAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OENDRjNBN0E2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OENDRjNBN0I2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4Q0NGM0E3ODY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4Q0NGM0E3OTY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqqezsUAAAAfSURBVHjaYmRABcYwBiM2QSA4y4hNEKYDQxAEAAIMAHNGAzhkPOlYAAAAAElFTkSuQmCC'); }</style>
</head>
<body class='markdown-preview' data-use-github-style><h1 id="code-block">Code Block</h1>
<pre class="editor-colors lang-javascript"><div class="line"><span class="syntax--source syntax--js"><span class="syntax--keyword syntax--control syntax--js"><span>if</span></span><span>&nbsp;a&nbsp;</span><span class="syntax--keyword syntax--operator syntax--comparison syntax--js"><span>===</span></span><span>&nbsp;</span><span class="syntax--constant syntax--numeric syntax--decimal syntax--js"><span>3</span></span><span>&nbsp;</span><span class="syntax--meta syntax--brace syntax--curly syntax--js"><span>{</span></span></span></div><div class="line"><span class="syntax--source syntax--js"><span>&nbsp;&nbsp;b&nbsp;</span><span class="syntax--keyword syntax--operator syntax--assignment syntax--js"><span>=</span></span><span>&nbsp;</span><span class="syntax--constant syntax--numeric syntax--decimal syntax--js"><span>5</span></span></span></div><div class="line"><span class="syntax--source syntax--js"><span class="syntax--meta syntax--brace syntax--curly syntax--js"><span>}</span></span></span></div></pre>
<pre class="editor-colors lang-javascript"><div class="line"><span class="syntax--source syntax--js"><span class="syntax--keyword syntax--control syntax--js">if</span> a <span class="syntax--keyword syntax--operator syntax--comparison syntax--js">===</span> <span class="syntax--constant syntax--numeric syntax--decimal syntax--js">3</span> <span class="syntax--meta syntax--brace syntax--curly syntax--js">{</span></span></div><div class="line"><span class="syntax--source syntax--js"><span class="leading-whitespace"> </span>b <span class="syntax--keyword syntax--operator syntax--assignment syntax--js">=</span> <span class="syntax--constant syntax--numeric syntax--decimal syntax--js">5</span></span></div><div class="line"><span class="syntax--source syntax--js"><span class="syntax--meta syntax--brace syntax--curly syntax--js">}</span></span></div></pre>
<p>encoding → issue</p></body>
</html>
18 changes: 14 additions & 4 deletions spec/markdown-preview-spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ path = require 'path'
fs = require 'fs-plus'
temp = require('temp').track()
MarkdownPreviewView = require '../lib/markdown-preview-view'
{TextEditor} = require 'atom'
TextMateLanguageMode = new TextEditor().getBuffer().getLanguageMode().constructor

describe "Markdown Preview", ->
preview = null
Expand All @@ -12,6 +14,8 @@ describe "Markdown Preview", ->
fs.copySync(fixturesPath, tempPath)
atom.project.setPaths([tempPath])

jasmine.unspy(TextMateLanguageMode.prototype, 'tokenizeInBackground')

jasmine.useRealClock()
jasmine.attachToDOM(atom.views.getView(atom.workspace))

Expand Down Expand Up @@ -348,23 +352,27 @@ 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>
"""

describe "code block tokenization", ->
preview = null

beforeEach ->
waitsForPromise ->
atom.packages.activatePackage('language-ruby')
Expand All @@ -375,8 +383,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
19 changes: 12 additions & 7 deletions spec/markdown-preview-view-spec.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ path = require 'path'
fs = require 'fs-plus'
temp = require('temp').track()
url = require 'url'
{TextEditor} = require 'atom'
MarkdownPreviewView = require '../lib/markdown-preview-view'
TextMateLanguageMode = new TextEditor().getBuffer().getLanguageMode().constructor

describe "MarkdownPreviewView", ->
preview = null
Expand All @@ -11,6 +13,8 @@ describe "MarkdownPreviewView", ->
# Makes _.debounce work
jasmine.useRealClock()

jasmine.unspy(TextMateLanguageMode.prototype, 'tokenizeInBackground')

spyOn(atom.packages, 'hasActivatedInitialPackages').andReturn true

filePath = atom.project.getDirectories()[0].resolve('subdir/file.markdown')
Expand Down Expand Up @@ -306,12 +310,11 @@ describe "MarkdownPreviewView", ->
"atom-text-editor .hr { background: url(atom://markdown-preview/assets/hr.png); }"
]

expect(fs.isFileSync(outputPath)).toBe false

waitsForPromise ->
preview.renderMarkdown()

runs ->
expect(fs.isFileSync(outputPath)).toBe false
spyOn(preview, 'getSaveDialogOptions').andReturn({defaultPath: outputPath})
spyOn(atom.applicationDelegate, 'showSaveDialog').andCallFake (options, callback) ->
callback?(options.defaultPath)
Expand All @@ -320,6 +323,8 @@ describe "MarkdownPreviewView", ->
return options.defaultPath
spyOn(preview, 'getDocumentStyleSheets').andReturn(markdownPreviewStyles)
spyOn(preview, 'getTextEditorStyles').andReturn(atomTextEditorStyles)

waitsForPromise ->
atom.commands.dispatch preview.element, 'core:save-as'

waitsFor ->
Expand Down Expand Up @@ -365,16 +370,16 @@ describe "MarkdownPreviewView", ->

describe "when there is no text selected", ->
it "copies the rendered HTML of the entire Markdown document to the clipboard", ->
atom.commands.dispatch preview.element, 'core:copy'
expect(atom.clipboard.read()).toBe("initial clipboard content")

waitsFor ->
atom.clipboard.read() isnt "initial clipboard content"
waitsForPromise ->
atom.commands.dispatch preview.element, 'core:copy'

runs ->
expect(atom.clipboard.read()).toBe """
<h1 id="code-block">Code Block</h1>
<pre class="editor-colors lang-javascript"><div class="line"><span class="syntax--source syntax--js"><span class="syntax--keyword syntax--control syntax--js"><span>if</span></span><span>&nbsp;a&nbsp;</span><span class="syntax--keyword syntax--operator syntax--comparison syntax--js"><span>===</span></span><span>&nbsp;</span><span class="syntax--constant syntax--numeric syntax--decimal syntax--js"><span>3</span></span><span>&nbsp;</span><span class="syntax--meta syntax--brace syntax--curly syntax--js"><span>{</span></span></span></div><div class="line"><span class="syntax--source syntax--js"><span>&nbsp;&nbsp;b&nbsp;</span><span class="syntax--keyword syntax--operator syntax--assignment syntax--js"><span>=</span></span><span>&nbsp;</span><span class="syntax--constant syntax--numeric syntax--decimal syntax--js"><span>5</span></span></span></div><div class="line"><span class="syntax--source syntax--js"><span class="syntax--meta syntax--brace syntax--curly syntax--js"><span>}</span></span></span></div></pre>
<p>encoding \u2192 issue</p>
<pre class="editor-colors lang-javascript"><div class="line"><span class="syntax--source syntax--js"><span class="syntax--keyword syntax--control syntax--js">if</span> a <span class="syntax--keyword syntax--operator syntax--comparison syntax--js">===</span> <span class="syntax--constant syntax--numeric syntax--decimal syntax--js">3</span> <span class="syntax--meta syntax--brace syntax--curly syntax--js">{</span></span></div><div class="line"><span class="syntax--source syntax--js"><span class="leading-whitespace"> </span>b <span class="syntax--keyword syntax--operator syntax--assignment syntax--js">=</span> <span class="syntax--constant syntax--numeric syntax--decimal syntax--js">5</span></span></div><div class="line"><span class="syntax--source syntax--js"><span class="syntax--meta syntax--brace syntax--curly syntax--js">}</span></span></div></pre>
<p>encoding issue</p>
"""

describe "when there is a text selection", ->
Expand Down