Skip to content

Commit 8c0e209

Browse files
authored
Merge pull request #43 from lierdakil/various-tweaks
2 parents dc672f8 + 6f2d4ca commit 8c0e209

File tree

9 files changed

+160
-82
lines changed

9 files changed

+160
-82
lines changed

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ coverage
77
build
88
dist
99
lib
10+
modules
1011
.rollup.cache

package.json

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
"test": "atom --test spec",
2828
"clean": "shx rm -rf dist modules",
2929
"babel": "npm run clean && shx cp -r lib dist && cross-env NODE_ENV=development cross-env BABEL_KEEP_MODULES=false babel dist --out-dir dist",
30-
"tsc.cjs": "tsc -p ./src/tsconfig.json --outDir dist || echo done",
31-
"tsc.es": "tsc --module esnext -p ./src/tsconfig.json --outDir modules || echo done",
30+
"tsc.cjs": "tsc -p ./src/tsconfig.json",
31+
"tsc.es": "tsc --module esnext -p ./src/tsconfig.es.json",
3232
"tsc": "npm run tsc.cjs && npm run tsc.es",
3333
"dev": "npm run clean && npm run tsc && cross-env NODE_ENV=development cross-env BABEL_KEEP_MODULES=true rollup -c -w",
3434
"build": "npm run clean && npm run tsc && cross-env NODE_ENV=production cross-env BABEL_KEEP_MODULES=true rollup -c ",
@@ -43,31 +43,32 @@
4343
"devDependencies": {
4444
"@types/atom": "1.40.7",
4545
"@types/dompurify": "2.2.1",
46+
"@types/jasmine": "^3.6.3",
4647
"@types/marked": "^1.2.2",
4748
"@types/node": "^14.14.25",
4849
"atom-ide-base": "^2.2.0",
49-
"atom-languageclient": "^1.0.6",
50-
"@types/jasmine": "^3.6.3",
5150
"atom-jasmine3-test-runner": "^5.1.8",
52-
"typescript": "^4.1.3",
53-
"tslib": "^2.1.0",
54-
"prettier": "^2.2.1",
51+
"atom-languageclient": "^1.0.6",
52+
"build-commit": "0.1.4",
53+
"cross-env": "^7.0.3",
5554
"eslint": "7.19.0",
5655
"eslint-config-atomic": "^1.9.0",
56+
"npm-check-updates": "11.1.1",
57+
"prettier": "^2.2.1",
5758
"rollup": "^2.38.4",
5859
"rollup-plugin-atomic": "^2.0.1",
5960
"shx": "^0.3.3",
60-
"cross-env": "^7.0.3",
61-
"npm-check-updates": "11.1.1",
62-
"build-commit": "0.1.4"
61+
"tslib": "^2.1.0",
62+
"typescript": "^4.1.3"
6363
},
6464
"activationHooks": [
6565
"core:loaded-shell-environment"
6666
],
6767
"providedServices": {
6868
"markdown-renderer": {
6969
"versions": {
70-
"1.0.0": "provideMarkdownRenderer"
70+
"1.0.0": "provideMarkdownRenderer",
71+
"1.1.0": "provideMarkdownRenderer"
7172
}
7273
}
7374
},

rollup.config.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createPlugins } from "rollup-plugin-atomic"
22

3-
const plugins = createPlugins(["ts", "babel"])
3+
const plugins = createPlugins([["ts", { tsconfig: "./src/tsconfig.json" }, true], "babel"])
4+
const pluginsEs = createPlugins([["ts", { tsconfig: "./src/tsconfig.es.json" }, true], "babel"])
45

56
export default [
67
{
@@ -42,6 +43,6 @@ export default [
4243
],
4344
// loaded externally
4445
external: ["atom"],
45-
plugins: plugins,
46+
plugins: pluginsEs,
4647
},
4748
]

src/highlighter.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { TextBuffer, LanguageMode } from "atom"
2+
import { eventLoopYielder, maxTimeError } from "./utils/event-loop-yielder"
3+
4+
declare module "atom" {
5+
interface GrammarRegistry {
6+
grammarForId(id: string): Grammar
7+
languageModeForGrammarAndBuffer(g: Grammar, b: TextBuffer): LanguageMode
8+
}
9+
interface LanguageMode {
10+
readonly fullyTokenized?: boolean
11+
readonly tree?: boolean
12+
onDidTokenize(cb: () => void): Disposable
13+
buildHighlightIterator(): HighlightIterator
14+
classNameForScopeId(id: ScopeId): string
15+
startTokenizing?(): void
16+
}
17+
interface HighlightIterator {
18+
seek(pos: { row: number; column: number }): void
19+
getPosition(): { row: number; column: number }
20+
getOpenScopeIds?(): ScopeId[]
21+
getCloseScopeIds?(): ScopeId[]
22+
moveToSuccessor(): void
23+
}
24+
interface ScopeId {}
25+
}
26+
27+
export async function highlightTreeSitter(sourceCode: string, scopeName: string) {
28+
const yielder = eventLoopYielder(100, 5000)
29+
const buf = new TextBuffer()
30+
try {
31+
const grammar = atom.grammars.grammarForId(scopeName)
32+
const lm = atom.grammars.languageModeForGrammarAndBuffer(grammar, buf)
33+
buf.setLanguageMode(lm)
34+
buf.setText(sourceCode)
35+
const end = buf.getEndPosition()
36+
if (lm.startTokenizing) lm.startTokenizing()
37+
await tokenized(lm)
38+
const iter = lm.buildHighlightIterator()
39+
if (iter.getOpenScopeIds && iter.getCloseScopeIds) {
40+
let pos = { row: 0, column: 0 }
41+
iter.seek(pos)
42+
const res = []
43+
while (pos.row < end.row || (pos.row === end.row && pos.column <= end.column)) {
44+
res.push(
45+
...iter.getCloseScopeIds().map(() => "</span>"),
46+
...iter.getOpenScopeIds().map((x) => `<span class="${lm.classNameForScopeId(x)}">`)
47+
)
48+
iter.moveToSuccessor()
49+
const nextPos = iter.getPosition()
50+
res.push(escapeHTML(buf.getTextInRange([pos, nextPos])))
51+
52+
if (!(await yielder())) {
53+
console.error(maxTimeError("Atom-IDE-Markdown-Service: Highlighter", 5))
54+
break
55+
}
56+
pos = nextPos
57+
}
58+
return res.join("")
59+
} else {
60+
return sourceCode
61+
}
62+
} finally {
63+
buf.destroy()
64+
}
65+
}
66+
67+
async function tokenized(lm: LanguageMode) {
68+
return new Promise((resolve) => {
69+
if (lm.fullyTokenized || lm.tree) {
70+
resolve(undefined)
71+
} else if (lm.onDidTokenize) {
72+
const disp = lm.onDidTokenize(() => {
73+
disp.dispose()
74+
resolve(undefined)
75+
})
76+
} else {
77+
resolve(undefined) // null language mode
78+
}
79+
})
80+
}
81+
82+
function escapeHTML(str: string) {
83+
return str
84+
.replace(/&/g, "&amp;")
85+
.replace(/</g, "&lt;")
86+
.replace(/>/g, "&gt;")
87+
.replace(/"/g, "&quot;")
88+
.replace(/'/g, "&#039;")
89+
}

src/renderer.ts

Lines changed: 13 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { TextEditor } from "atom"
21
import marked from "marked"
32

43
/**
@@ -7,71 +6,34 @@ import marked from "marked"
76
* @type {DOMPurify}
87
*/
98
import DOMPurify from "dompurify"
10-
11-
/**
12-
* A function that resolves once the given editor has tokenized
13-
* @param editor
14-
*/
15-
export async function editorTokenized(editor: TextEditor) {
16-
return new Promise((resolve) => {
17-
const languageMode = editor.getBuffer().getLanguageMode()
18-
const nextUpdatePromise = editor.component.getNextUpdatePromise()
19-
if ("fullyTokenized" in languageMode || "tree" in languageMode) {
20-
resolve(nextUpdatePromise)
21-
} else {
22-
const disp = editor.onDidTokenize(() => {
23-
disp.dispose()
24-
resolve(nextUpdatePromise)
25-
})
26-
}
27-
})
28-
}
29-
30-
/**
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-
}
9+
import { highlightTreeSitter } from "./highlighter"
5510

5611
marked.setOptions({
5712
breaks: true,
5813
})
5914

15+
export type DOMPurifyConfig = Omit<DOMPurify.Config, "RETURN_DOM" | "RETURN_DOM_FRAGMENT" | "RETURN_TRUSTED_TYPE">
16+
6017
/**
6118
* renders markdown to safe HTML asynchronously
6219
* @param markdownText the markdown text to render
6320
* @param scopeName scope name used for highlighting the code
21+
* @param purifyConfig (optional) configuration object for DOMPurify
6422
* @return the html string containing the result
6523
*/
66-
function internalRender(markdownText: string, scopeName: string = "text.plain"): Promise<string> {
24+
export async function render(
25+
markdownText: string,
26+
scopeName: string = "text.plain",
27+
domPurifyConfig?: DOMPurifyConfig
28+
): Promise<string> {
6729
return new Promise((resolve, reject) => {
6830
marked(
6931
markdownText,
7032
{
71-
highlight: function (code, lang, callback) {
72-
highlight(code, scopeName)
33+
highlight: function (code, _lang, callback) {
34+
highlightTreeSitter(code, scopeName)
7335
.then((codeResult) => {
74-
callback!(null, codeResult.join("\n"))
36+
callback!(null, codeResult)
7537
})
7638
.catch((e) => {
7739
callback!(e)
@@ -83,21 +45,10 @@ function internalRender(markdownText: string, scopeName: string = "text.plain"):
8345
reject(e)
8446
}
8547
// sanitization
86-
html = DOMPurify.sanitize(html)
48+
html = domPurifyConfig ? DOMPurify.sanitize(html, domPurifyConfig) : DOMPurify.sanitize(html)
8749

8850
return resolve(html)
8951
}
9052
)
9153
})
9254
}
93-
94-
/**
95-
* renders the markdown text to html
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
99-
*/
100-
export async function render(markdownText: string, grammar: string): Promise<string> {
101-
const html = await internalRender(markdownText, grammar)
102-
return html
103-
}

src/tsconfig.es.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"module": "esnext",
5+
"outDir": "../modules"
6+
}
7+
}

src/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
"module": "commonjs",
2727
"moduleResolution": "node",
2828
"importHelpers": false,
29-
"outDir": "../dist"
29+
"outDir": "../dist",
30+
"skipLibCheck": true
3031
},
3132
"compileOnSave": false
3233
}

src/utils/event-loop-yielder.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* A helper to allow the JavaScript event loop continue for a given interval between each
3+
* iteration of a CPU intensive loop. If the time spent in the loop reaches the given
4+
* maxTime, the operation is killed.
5+
*
6+
* @returns An async function to call inside your heavy loop. It will return `false` if
7+
* the operation has exceeded the given max time (`true` otherwise).
8+
*/
9+
export function eventLoopYielder(delayMs: number, maxTimeMs: number) {
10+
const started = performance.now()
11+
let lastYield = started
12+
return async function (): Promise<boolean> {
13+
const now = performance.now()
14+
if (now - lastYield > delayMs) {
15+
await new Promise(setImmediate)
16+
lastYield = now
17+
}
18+
return now - started <= maxTimeMs
19+
}
20+
}
21+
22+
/** Throws maximum time reached error */
23+
export function maxTimeError(name: string, timeS: number) {
24+
const err = new Error("Max time reached")
25+
atom.notifications.addError(`${name} took more than ${timeS} seconds to complete`, {
26+
dismissable: true,
27+
description: `${name} took too long to complete and was terminated.`,
28+
stack: err.stack,
29+
})
30+
return err
31+
}

src/utils/utils.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const scopesByFenceName = {
1+
const scopesByFenceName: { [key: string]: string | undefined } = {
22
bash: "source.shell",
33
sh: "source.shell",
44
powershell: "source.powershell",
@@ -42,11 +42,7 @@ const scopesByFenceName = {
4242

4343
export function scopeForFenceName(fenceName: string): string {
4444
fenceName = fenceName.toLowerCase()
45-
let result = `source.${fenceName}`
46-
if (scopesByFenceName[fenceName] != null) {
47-
result = scopesByFenceName[fenceName]
48-
}
49-
return result
45+
return scopesByFenceName[fenceName] ?? `source.${fenceName}`
5046
}
5147

5248
export function fenceNameForScope(scope: string): string {

0 commit comments

Comments
 (0)