1
- // TODO fix types
2
-
3
- import { TextEditor , TextEditorElement } from "atom"
4
- import { scopeForFenceName , fenceNameForScope } from "./utils"
1
+ import { TextEditor } from "atom"
5
2
import marked from "marked"
6
3
7
4
/**
@@ -12,119 +9,95 @@ import marked from "marked"
12
9
import DOMPurify from "dompurify"
13
10
14
11
/**
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 ( / ^ l a n g - / , "" )
41
- . replace ( / ^ l a n g u a g e - / , "" ) ?? 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
72
14
*/
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 ) => {
86
17
const languageMode = editor . getBuffer ( ) . getLanguageMode ( )
18
+ const nextUpdatePromise = editor . component . getNextUpdatePromise ( )
87
19
if ( "fullyTokenized" in languageMode || "tree" in languageMode ) {
88
- editor . component
89
- . getNextUpdatePromise ( )
90
- . then ( ( ) => {
91
- done ( )
92
- } )
93
- . catch ( reject )
20
+ resolve ( nextUpdatePromise )
94
21
} else {
95
- editor . onDidTokenize ( ( ) => {
96
- done ( )
22
+ const disp = editor . onDidTokenize ( ( ) => {
23
+ disp . dispose ( )
24
+ resolve ( nextUpdatePromise )
97
25
} )
98
26
}
99
27
} )
100
- return p
101
28
}
102
29
103
30
/**
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
107
65
*/
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
+ } )
113
92
}
114
93
115
94
/**
116
95
* 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
120
99
*/
121
100
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
130
103
}
0 commit comments