Skip to content

Commit c75c871

Browse files
authored
Merge pull request #35 from atom-ide-community/code-highlight
2 parents 93f42f0 + bda6ce0 commit c75c871

File tree

3 files changed

+168
-102
lines changed

3 files changed

+168
-102
lines changed

src/renderer.ts

Lines changed: 75 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
// TODO fix types
2-
3-
import { TextEditor, TextEditorElement } from "atom"
4-
import { scopeForFenceName, fenceNameForScope } from "./utils"
1+
import { TextEditor } from "atom"
52
import marked from "marked"
63

74
/**
@@ -12,119 +9,95 @@ import marked from "marked"
129
import DOMPurify from "dompurify"
1310

1411
/**
15-
* iterates over the content of the HTML fragment and replaces any code section
16-
* found with an Atom TextEditor element that is used for syntax highlighting the code
17-
*
18-
* @param {HTMLElement} domFragment the HTML fragment to be analyzed and
19-
* @param {String} grammar the default grammar to be used if the code section doesn't have a specific grammar set
20-
* @return a promise that is resolved when the fragment is ready
21-
*/
22-
async function highlightCodeFragments(domFragment: HTMLElement, grammar: string): Promise<any> {
23-
const defaultLanguage = fenceNameForScope(grammar || "text.plain")
24-
// set editor font family
25-
const fontFamily = atom.config.get("editor.fontFamily")
26-
const fontSize = atom.config.get("editor.fontSize")
27-
if (fontFamily !== null) {
28-
domFragment.querySelectorAll("code").forEach((codeElement) => {
29-
codeElement.style.fontFamily = fontFamily
30-
codeElement.style.fontSize = `${fontSize}`
31-
})
32-
}
33-
34-
const elements: HTMLPreElement[] = [].slice.call(domFragment.querySelectorAll("pre"))
35-
const promises = elements.map(async (preElement) => {
36-
let codeBlock = preElement.firstElementChild ?? preElement
37-
let fenceName =
38-
codeBlock
39-
.getAttribute("class")
40-
?.replace(/^lang-/, "")
41-
.replace(/^language-/, "") ?? defaultLanguage
42-
preElement.classList.add("editor-colors", `lang-${fenceName}`)
43-
44-
let editor = new TextEditor({
45-
readonly: true,
46-
keyboardInputEnabled: false,
47-
softWrapped: true,
48-
softWrapAtPreferredLineLength: true,
49-
preferredLineLength: 80,
50-
})
51-
let editorElement = editor.getElement()
52-
editorElement.setUpdatedSynchronously(true)
53-
54-
preElement.innerHTML = ""
55-
preElement.parentNode?.insertBefore(editorElement, preElement)
56-
57-
editor.setText(codeBlock.textContent?.replace(/\r?\n$/, "") ?? "")
58-
59-
atom.grammars.assignLanguageMode(editor.getBuffer(), scopeForFenceName(fenceName))
60-
editor.setVisible(true)
61-
return await tokenizeEditor(editorElement, preElement)
62-
})
63-
64-
return await Promise.all(promises)
65-
}
66-
67-
/**
68-
* takes an Atom TextEditor element, tokenize the content and move the resulting lines to the pre element given
69-
* @param editorElement the HTML element containing the Atom TextEditor
70-
* @param preElement the HTML pre element that should host the resulting lines
71-
* @return a promise that is triggered as soon as tokenization and moving the content is done
12+
* A function that resolves once the given editor has tokenized
13+
* @param editor
7214
*/
73-
function tokenizeEditor(editorElement: TextEditorElement, preElement: HTMLPreElement): Promise<void> {
74-
let p = new Promise<void>((resolve, reject) => {
75-
let done = () => {
76-
editorElement.querySelectorAll(".line:not(.dummy)").forEach((line) => {
77-
let line2 = document.createElement("div")
78-
line2.className = "line"
79-
line2.innerHTML = line.firstElementChild?.innerHTML ?? ""
80-
preElement.appendChild(line2)
81-
})
82-
editorElement.remove()
83-
resolve()
84-
}
85-
const editor = editorElement.getModel()
15+
export async function editorTokenized(editor: TextEditor) {
16+
return new Promise((resolve) => {
8617
const languageMode = editor.getBuffer().getLanguageMode()
18+
const nextUpdatePromise = editor.component.getNextUpdatePromise()
8719
if ("fullyTokenized" in languageMode || "tree" in languageMode) {
88-
editor.component
89-
.getNextUpdatePromise()
90-
.then(() => {
91-
done()
92-
})
93-
.catch(reject)
20+
resolve(nextUpdatePromise)
9421
} else {
95-
editor.onDidTokenize(() => {
96-
done()
22+
const disp = editor.onDidTokenize(() => {
23+
disp.dispose()
24+
resolve(nextUpdatePromise)
9725
})
9826
}
9927
})
100-
return p
10128
}
10229

10330
/**
104-
* renders markdown to safe HTML
105-
* @param {String} markdownText the markdown text to render
106-
* @return {Node} the html template node containing the result
31+
* Highlights the given code with the given scope name (language)
32+
* @param code the given code as string
33+
* @param scopeName the language to highlight the code for
34+
*/
35+
export async function highlight(code: string, scopeName: string) {
36+
const ed = new TextEditor({
37+
readonly: true,
38+
keyboardInputEnabled: false,
39+
showInvisibles: false,
40+
tabLength: atom.config.get("editor.tabLength"),
41+
})
42+
const el = atom.views.getView(ed)
43+
try {
44+
el.setUpdatedSynchronously(true)
45+
atom.grammars.assignLanguageMode(ed.getBuffer(), scopeName)
46+
ed.setText(code)
47+
ed.scrollToBufferPosition(ed.getBuffer().getEndPosition())
48+
atom.views.getView(atom.workspace).appendChild(el)
49+
await editorTokenized(ed)
50+
return Array.from(el.querySelectorAll(".line:not(.dummy)")).map((x) => x.innerHTML)
51+
} finally {
52+
el.remove()
53+
}
54+
}
55+
56+
marked.setOptions({
57+
breaks: true,
58+
})
59+
60+
/**
61+
* renders markdown to safe HTML asynchronously
62+
* @param markdownText the markdown text to render
63+
* @param scopeName scope name used for highlighting the code
64+
* @return the html string containing the result
10765
*/
108-
function internalRender(markdownText: string): Node {
109-
let html = DOMPurify.sanitize(marked(markdownText, { breaks: true }))
110-
let template = document.createElement("template")
111-
template.innerHTML = html.trim()
112-
return template.content.cloneNode(true)
66+
function internalRender(markdownText: string, scopeName: string = "text.plain"): Promise<string> {
67+
return new Promise((resolve, reject) => {
68+
marked(
69+
markdownText,
70+
{
71+
highlight: function (code, lang, callback) {
72+
highlight(code, scopeName)
73+
.then((codeResult) => {
74+
callback!(null, codeResult.join("\n"))
75+
})
76+
.catch((e) => {
77+
callback!(e)
78+
})
79+
},
80+
},
81+
(e, html) => {
82+
if (e) {
83+
reject(e)
84+
}
85+
// sanitization
86+
html = DOMPurify.sanitize(html)
87+
88+
return resolve(html)
89+
}
90+
)
91+
})
11392
}
11493

11594
/**
11695
* renders the markdown text to html
117-
* @param {string} markdownText the markdown text to render
118-
* @param {string} grammar the default grammar used in code sections that have no specific grammar set
119-
* @return {Promise<string>} the inner HTML text of the rendered section
96+
* @param markdownText the markdown text to render
97+
* @param grammar the default grammar used in code sections that have no specific grammar set
98+
* @return the inner HTML text of the rendered section
12099
*/
121100
export async function render(markdownText: string, grammar: string): Promise<string> {
122-
let node = internalRender(markdownText)
123-
let div = document.createElement("div")
124-
div.appendChild(node)
125-
document.body.appendChild(div)
126-
127-
await highlightCodeFragments(div, grammar)
128-
div.remove()
129-
return div.innerHTML
101+
const html = await internalRender(markdownText, grammar)
102+
return html
130103
}

src/unused/render.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// These functions are deprecated because they edit the markdown output. We instead use a `highlight` hook to tokenize and highlight the code
2+
3+
import { TextEditor, TextEditorElement } from "atom"
4+
import { scopeForFenceName, fenceNameForScope } from "./utils"
5+
6+
/**
7+
* iterates over the content of the HTML fragment and replaces any code section
8+
* found with an Atom TextEditor element that is used for syntax highlighting the code
9+
*
10+
* @param {HTMLElement} domFragment the HTML fragment to be analyzed and
11+
* @param {String} grammar the default grammar to be used if the code section doesn't have a specific grammar set
12+
* @return a promise that is resolved when the fragment is ready
13+
*/
14+
export async function highlightCodeFragments(domFragment: HTMLElement, grammar: string): Promise<any> {
15+
const defaultLanguage = fenceNameForScope(grammar || "text.plain")
16+
// set editor font family
17+
const fontFamily = atom.config.get("editor.fontFamily")
18+
const fontSize = atom.config.get("editor.fontSize")
19+
if (fontFamily !== null) {
20+
domFragment.querySelectorAll("code").forEach((codeElement) => {
21+
codeElement.style.fontFamily = fontFamily
22+
codeElement.style.fontSize = `${fontSize}`
23+
})
24+
}
25+
26+
const elements: HTMLPreElement[] = [].slice.call(domFragment.querySelectorAll("pre"))
27+
const promises = elements.map(async (preElement) => {
28+
let codeBlock = preElement.firstElementChild ?? preElement
29+
let fenceName =
30+
codeBlock
31+
.getAttribute("class")
32+
?.replace(/^lang-/, "")
33+
.replace(/^language-/, "") ?? defaultLanguage
34+
preElement.classList.add("editor-colors", `lang-${fenceName}`)
35+
36+
let editor = new TextEditor({
37+
readonly: true,
38+
keyboardInputEnabled: false,
39+
softWrapped: true,
40+
softWrapAtPreferredLineLength: true,
41+
preferredLineLength: 80,
42+
})
43+
let editorElement = editor.getElement()
44+
editorElement.setUpdatedSynchronously(true)
45+
46+
preElement.innerHTML = ""
47+
preElement.parentNode?.insertBefore(editorElement, preElement)
48+
49+
editor.setText(codeBlock.textContent?.replace(/\r?\n$/, "") ?? "")
50+
51+
atom.grammars.assignLanguageMode(editor.getBuffer(), scopeForFenceName(fenceName))
52+
editor.setVisible(true)
53+
return await tokenizeEditor(editorElement, preElement)
54+
})
55+
56+
return await Promise.all(promises)
57+
}
58+
59+
/**
60+
* takes an Atom TextEditor element, tokenize the content and move the resulting lines to the pre element given
61+
* @param editorElement the HTML element containing the Atom TextEditor
62+
* @param preElement the HTML pre element that should host the resulting lines
63+
* @return a promise that is triggered as soon as tokenization and moving the content is done
64+
*/
65+
export function tokenizeEditor(editorElement: TextEditorElement, preElement: HTMLPreElement): Promise<void> {
66+
let p = new Promise<void>((resolve, reject) => {
67+
let done = () => {
68+
editorElement.querySelectorAll(".line:not(.dummy)").forEach((line) => {
69+
let line2 = document.createElement("div")
70+
line2.className = "line"
71+
line2.innerHTML = line.firstElementChild?.innerHTML ?? ""
72+
preElement.appendChild(line2)
73+
})
74+
editorElement.remove()
75+
resolve()
76+
}
77+
const editor = editorElement.getModel()
78+
const languageMode = editor.getBuffer().getLanguageMode()
79+
if ("fullyTokenized" in languageMode || "tree" in languageMode) {
80+
editor.component
81+
.getNextUpdatePromise()
82+
.then(() => {
83+
done()
84+
})
85+
.catch(reject)
86+
} else {
87+
editor.onDidTokenize(() => {
88+
done()
89+
})
90+
}
91+
})
92+
return p
93+
}
File renamed without changes.

0 commit comments

Comments
 (0)