Skip to content
This repository was archived by the owner on Feb 7, 2024. It is now read-only.

Commit 61794b2

Browse files
committedDec 15, 2023
feat: introduce shikiji-monaco package
1 parent 6018439 commit 61794b2

File tree

8 files changed

+263
-2
lines changed

8 files changed

+263
-2
lines changed
 

‎packages/shikiji-core/src/highlighter.ts

+2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ export async function getHighlighterCore(options: HighlighterCoreOptions = {}):
2020
loadTheme: internal.loadTheme,
2121

2222
getTheme: internal.getTheme,
23+
getLangGrammar: internal.getLangGrammar,
24+
setTheme: internal.setTheme,
2325

2426
getLoadedThemes: internal.getLoadedThemes,
2527
getLoadedLanguages: internal.getLoadedLanguages,

‎packages/shikiji-core/src/types.ts

+11-1
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,19 @@ export interface HighlighterGeneric<BundledLangKeys extends string, BundledTheme
111111
loadLanguage(...langs: (LanguageInput | BundledLangKeys | SpecialLanguage)[]): Promise<void>
112112

113113
/**
114-
* Get the theme registration object
114+
* Get the registered theme object
115115
*/
116116
getTheme(name: string | ThemeRegistration | ThemeRegistrationRaw): ThemeRegistration
117+
/**
118+
* Get the registered language object
119+
*/
120+
getLangGrammar(name: string | LanguageRegistration): Grammar
121+
122+
/**
123+
* Set the current theme and get the resolved theme object and color map.
124+
* @internal
125+
*/
126+
setTheme: ShikiInternal['setTheme']
117127

118128
/**
119129
* Get the names of loaded languages

‎packages/shikiji-monaco/README.md

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# shikiji-monaco
2+
3+
Use [shikiji](https://github.com/antfu/shikiji) to highlight [Monaco Editor](https://microsoft.github.io/monaco-editor/).
4+
5+
Monaco's built-in highlighter does not use the full TextMate grammar, which in some cases is not accurate enough. This package allows you to use Shikiji's syntax highlighting engine to highlight Monaco, with shared grammars and themes from Shikiji.
6+
7+
> [!IMPORTANT]
8+
> This package is experimental. Breaking changes may happend without following semver.
9+
10+
Heavily inspired by [`monaco-editor-textmate`](https://github.com/zikaari/monaco-editor-textmate).
11+
12+
## Install
13+
14+
```bash
15+
npm i -D shikiji-monaco
16+
```
17+
18+
```ts
19+
import { getHighlighter } from 'shikiji'
20+
import { shikijiToMonaco } from 'shikiji-monaco'
21+
import * as monaco from 'monaco-editor-core'
22+
23+
// Create the highlighter, it can be reused
24+
const highlighter = await getHighlighter({
25+
themes: [
26+
'vitesse-dark',
27+
'vitesse-light',
28+
],
29+
langs: [
30+
'javascript',
31+
'typescript',
32+
'vue'
33+
],
34+
})
35+
36+
// Register the languageIds first. Only registered languages will be highlighted.
37+
monaco.languages.register({ id: 'vue' })
38+
monaco.languages.register({ id: 'typescript' })
39+
monaco.languages.register({ id: 'javascript' })
40+
41+
// Register the themes from Shikiji, and provide syntax highlighting for Monaco.
42+
shikijiToMonaco(highlighter, monaco)
43+
44+
// Create the editor
45+
const editor = monaco.editor.create(document.getElementById('container'), {
46+
value: 'const a = 1',
47+
language: 'javascript',
48+
theme: 'vitesse-dark',
49+
})
50+
51+
// ...As you use the editor normally
52+
```
53+
54+
## License
55+
56+
MIT
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { defineBuildConfig } from 'unbuild'
2+
3+
export default defineBuildConfig({
4+
entries: [
5+
'src/index.ts',
6+
],
7+
declaration: true,
8+
rollup: {
9+
emitCJS: false,
10+
},
11+
externals: [
12+
'monaco-editor-core',
13+
],
14+
})

‎packages/shikiji-monaco/package.json

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"name": "shikiji-monaco",
3+
"type": "module",
4+
"version": "0.9.2",
5+
"description": "Use Shikiji for Monaco Editor",
6+
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
7+
"license": "MIT",
8+
"homepage": "https://github.com/antfu/shikiji#readme",
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/antfu/shikiji.git",
12+
"directory": "packages/shikiji-monaco"
13+
},
14+
"bugs": "https://github.com/antfu/shikiji/issues",
15+
"keywords": [
16+
"shiki",
17+
"monaco-editor"
18+
],
19+
"sideEffects": false,
20+
"exports": {
21+
".": {
22+
"types": "./dist/index.d.mts",
23+
"default": "./dist/index.mjs"
24+
},
25+
"./*": "./dist/*"
26+
},
27+
"main": "./dist/index.mjs",
28+
"module": "./dist/index.mjs",
29+
"types": "./dist/index.d.mts",
30+
"typesVersions": {
31+
"*": {
32+
"*": [
33+
"./dist/*",
34+
"./*"
35+
]
36+
}
37+
},
38+
"files": [
39+
"dist"
40+
],
41+
"scripts": {
42+
"build": "unbuild",
43+
"dev": "unbuild --stub",
44+
"prepublishOnly": "nr build",
45+
"test": "vitest"
46+
},
47+
"dependencies": {
48+
"shikiji-core": "workspace:*"
49+
},
50+
"devDependencies": {
51+
"monaco-editor-core": "^0.45.0"
52+
}
53+
}

‎packages/shikiji-monaco/src/index.ts

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type * as monaco from 'monaco-editor-core'
2+
import type { HighlighterGeneric, ShikiInternal } from 'shikiji-core'
3+
import type { StateStack } from 'shikiji-core/textmate'
4+
import { INITIAL, StackElementMetadata } from 'shikiji-core/textmate'
5+
6+
type Monaco = typeof monaco
7+
type AcceptableShiki = HighlighterGeneric<any, any> | ShikiInternal
8+
9+
export interface MonacoInferface {
10+
editor: Monaco['editor']
11+
languages: Monaco['languages']
12+
}
13+
14+
export interface HighlighterInterface {
15+
getLoadedThemes: AcceptableShiki['getLoadedThemes']
16+
getTheme: AcceptableShiki['getTheme']
17+
getLoadedLanguages: AcceptableShiki['getLoadedLanguages']
18+
getLangGrammar: AcceptableShiki['getLangGrammar']
19+
setTheme: AcceptableShiki['setTheme']
20+
}
21+
22+
export function shikijiToMonaco(
23+
highlighter: HighlighterInterface,
24+
monaco: MonacoInferface,
25+
) {
26+
const themes = highlighter.getLoadedThemes()
27+
for (const theme of themes)
28+
monaco.editor.defineTheme(theme, highlighter.getTheme(theme) as any)
29+
30+
let currentTheme = themes[0]
31+
32+
const _setTheme = monaco.editor.setTheme.bind(monaco.editor)
33+
monaco.editor.setTheme = (theme: string) => {
34+
_setTheme(theme)
35+
currentTheme = theme
36+
}
37+
38+
for (const lang of highlighter.getLoadedLanguages()) {
39+
if (monaco.languages.getLanguages().some(l => l.id === lang)) {
40+
monaco.languages.setTokensProvider(lang, {
41+
getInitialState() {
42+
return new TokenizerState(INITIAL, highlighter)
43+
},
44+
tokenize(line, state: TokenizerState) {
45+
const grammar = state.highlighter.getLangGrammar(lang)
46+
const { theme, colorMap } = state.highlighter.setTheme(currentTheme)
47+
const result = grammar.tokenizeLine2(line, state.ruleStack)
48+
49+
const colorToScopeMap = new Map<string, string>()
50+
51+
// @ts-expect-error - 'rules' is presented on resolved theme
52+
theme?.rules.forEach((rule) => {
53+
const c = rule.foreground?.replace('#', '').toLowerCase()
54+
if (c && !colorToScopeMap.has(c))
55+
colorToScopeMap.set(c, rule.token)
56+
})
57+
58+
function findScopeByColor(color: string) {
59+
return colorToScopeMap.get(color)
60+
}
61+
62+
const tokensLength = result.tokens.length / 2
63+
const tokens: any[] = []
64+
for (let j = 0; j < tokensLength; j++) {
65+
const startIndex = result.tokens[2 * j]
66+
const metadata = result.tokens[2 * j + 1]
67+
const color = (colorMap[StackElementMetadata.getForeground(metadata)] || '').replace('#', '').toLowerCase()
68+
// Because Monaco only support one scope per token,
69+
// we workaround this to use color to trace back the scope
70+
const scope = findScopeByColor(color) || ''
71+
tokens.push({
72+
startIndex,
73+
scopes: scope,
74+
})
75+
}
76+
77+
return {
78+
endState: new TokenizerState(result.ruleStack, state.highlighter),
79+
tokens,
80+
}
81+
},
82+
})
83+
}
84+
}
85+
}
86+
87+
class TokenizerState implements monaco.languages.IState {
88+
constructor(
89+
private _ruleStack: StateStack,
90+
public highlighter: HighlighterInterface,
91+
) { }
92+
93+
public get ruleStack(): StateStack {
94+
return this._ruleStack
95+
}
96+
97+
public clone(): TokenizerState {
98+
return new TokenizerState(this._ruleStack, this.highlighter)
99+
}
100+
101+
public equals(other: monaco.languages.IState): boolean {
102+
if (!other
103+
|| !(other instanceof TokenizerState)
104+
|| other !== this
105+
|| other._ruleStack !== this._ruleStack
106+
)
107+
return false
108+
109+
return true
110+
}
111+
}

‎pnpm-lock.yaml

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎vitest.config.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ export default defineConfig({
4141
'**/dist/**',
4242
'**/scripts/**',
4343
'**/vendor/**',
44-
'**/stackElementMetadata.ts',
44+
'**/stack-element-metadata.ts',
45+
'**/shikiji-monaco/**',
4546
],
4647
},
4748
poolOptions: {

0 commit comments

Comments
 (0)
This repository has been archived.