diff --git a/lib/main.coffee b/lib/main.coffee index c700829..40bc785 100644 --- a/lib/main.coffee +++ b/lib/main.coffee @@ -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() diff --git a/lib/markdown-preview-view.coffee b/lib/markdown-preview-view.coffee index 6b3d7fb..9e6105d 100644 --- a/lib/markdown-preview-view.coffee +++ b/lib/markdown-preview-view.coffee @@ -330,20 +330,21 @@ class MarkdownPreviewView if filePath title = path.parse(filePath).name - @getHTML (error, htmlBody) => - if error? - throw error - else - html = """ - - - - - #{title} - - - #{htmlBody} - """ + "\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 = """ + + + + + #{title} + + + #{htmlBody} + """ + "\n" # Ensure trailing newline + + fs.writeFileSync(htmlFilePath, html) + atom.workspace.open(htmlFilePath).then(resolve) diff --git a/lib/renderer.coffee b/lib/renderer.coffee index 5ee28d0..382a021 100644 --- a/lib/renderer.coffee +++ b/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' @@ -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' @@ -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') @@ -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) diff --git a/package.json b/package.json index 767b7c5..5612bd5 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/spec/fixtures/saved-html.html b/spec/fixtures/saved-html.html index 682365b..eb592b4 100644 --- a/spec/fixtures/saved-html.html +++ b/spec/fixtures/saved-html.html @@ -11,6 +11,6 @@ pre.editor-colors .hr { background: url('data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAYAAACtBE5DAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OENDRjNBN0E2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OENDRjNBN0I2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4Q0NGM0E3ODY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4Q0NGM0E3OTY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqqezsUAAAAfSURBVHjaYmRABcYwBiM2QSA4y4hNEKYDQxAEAAIMAHNGAzhkPOlYAAAAAElFTkSuQmCC'); }

Code Block

-
if a === 3 {
  b = 5
}
+
if a === 3 {
b = 5
}

encoding → issue

diff --git a/spec/markdown-preview-spec.coffee b/spec/markdown-preview-spec.coffee index ef4765b..8877902 100644 --- a/spec/markdown-preview-spec.coffee +++ b/spec/markdown-preview-spec.coffee @@ -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 @@ -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)) @@ -348,8 +352,10 @@ 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 """

italic

bold

@@ -357,14 +363,16 @@ describe "Markdown Preview", -> """ 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 """

italic

""" describe "code block tokenization", -> - preview = null - beforeEach -> waitsForPromise -> atom.packages.activatePackage('language-ruby') @@ -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() diff --git a/spec/markdown-preview-view-spec.coffee b/spec/markdown-preview-view-spec.coffee index 1bb470d..f2da015 100644 --- a/spec/markdown-preview-view-spec.coffee +++ b/spec/markdown-preview-view-spec.coffee @@ -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 @@ -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') @@ -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) @@ -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 -> @@ -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 """

Code Block

-
if a === 3 {
  b = 5
}
-

encoding \u2192 issue

+
if a === 3 {
b = 5
}
+

encoding → issue

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