Skip to content

Commit 12fab5a

Browse files
authored
feat: add new highlight plugin (Milkdown#2067)
* feat: add new highlight plugin * feat: add highlight plugin entries * feat: add new highlight plugin * test: add unit test * chore: update version * chore: update version * chore: f
1 parent 33abad9 commit 12fab5a

16 files changed

Lines changed: 507 additions & 10 deletions

File tree

‎dev/src/vite.ts‎

Lines changed: 50 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ function mergeDeep<T>(target: T, ...sources: T[]): T {
3131
return mergeDeep(target, ...sources)
3232
}
3333

34-
function viteBuild(path: string, options: BuildOptions = {}): BuildOptions {
34+
function viteBuild(
35+
path: string,
36+
options: BuildOptions = {},
37+
userExternal: CustomOptions['external'] = []
38+
): BuildOptions {
3539
const dir = dirname(fileURLToPath(path))
3640
const packageDirName = basename(dir)
3741

@@ -44,20 +48,52 @@ function viteBuild(path: string, options: BuildOptions = {}): BuildOptions {
4448
...packageJson.peerDependencies,
4549
...globalPackageJson.devDependencies,
4650
}
51+
52+
// Auto-detect multiple entry points from package.json exports
53+
const exports = packageJson.exports || {}
54+
const entries: Record<string, string> = {}
55+
56+
for (const [key, value] of Object.entries(exports)) {
57+
if (typeof value === 'object' && value !== null && 'import' in value) {
58+
const importPath = (value as any).import as string
59+
if (importPath.startsWith('./src/')) {
60+
const entryName = key === '.' ? 'index' : key.replace('./', '')
61+
entries[entryName] = resolve(dir, importPath.replace('./', ''))
62+
}
63+
}
64+
}
65+
66+
// If no entries found from exports, fall back to default index.ts
67+
if (Object.keys(entries).length === 0) {
68+
entries.index = resolve(dir, 'src', 'index.ts')
69+
}
70+
71+
const isMultiEntry = Object.keys(entries).length > 1
72+
4773
return mergeDeep<BuildOptions>(
4874
{
4975
sourcemap: true,
5076
emptyOutDir: false,
51-
lib: {
52-
entry: resolve(dir, 'src', 'index.ts'),
53-
name: `milkdown_${packageDirName}`,
54-
fileName: 'index',
55-
formats: ['es'],
56-
},
77+
lib: isMultiEntry
78+
? {
79+
entry: entries,
80+
formats: ['es'],
81+
}
82+
: {
83+
entry: entries.index as string,
84+
name: `milkdown_${packageDirName}`,
85+
fileName: 'index',
86+
formats: ['es'],
87+
},
5788
rollupOptions: {
58-
external: Array.from(new Set([...Object.keys(deps), ...external])),
89+
external: Array.from(
90+
new Set([...Object.keys(deps), ...external, ...userExternal])
91+
),
5992
output: {
6093
dir: resolve(dir, 'lib'),
94+
...(isMultiEntry && {
95+
entryFileNames: '[name].js',
96+
}),
6197
},
6298
},
6399
minify: false,
@@ -67,6 +103,10 @@ function viteBuild(path: string, options: BuildOptions = {}): BuildOptions {
67103
)
68104
}
69105

106+
type CustomOptions = {
107+
external?: (string | RegExp)[]
108+
}
109+
70110
/**
71111
* Config for plugins
72112
*
@@ -76,10 +116,10 @@ function viteBuild(path: string, options: BuildOptions = {}): BuildOptions {
76116
*/
77117
export function pluginViteConfig(
78118
packageDirName: string,
79-
options: UserConfig = {}
119+
options: UserConfig & CustomOptions = {}
80120
) {
81121
return defineConfig({
82122
...options,
83-
build: viteBuild(packageDirName, options.build),
123+
build: viteBuild(packageDirName, options.build, options.external),
84124
})
85125
}

‎docs/api/plugin-highlight.md‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# @milkdown/plugin-highlight
2+
3+
Highlight plugin for [milkdown](https://milkdown.dev).
4+
Built on top of [prosemirror-highlight](https://github.com/ocavue/prosemirror-highlight).
5+
6+
Supports:
7+
8+
- [Shiki](https://github.com/shikijs/shiki)
9+
- [Lowlight](https://github.com/robertknight/lowlight.js) (based on [Highlight.js](https://github.com/highlightjs/highlight.js))
10+
- [Refractor](https://github.com/wooorm/refractor) (based on [Prism.js](https://github.com/PrismJS/prism))
11+
- [Sugar high](https://github.com/huozhi/sugar-high)
12+
13+
@highlightPluginConfig
14+
@highlightPlugin
15+
16+
## Usage
17+
18+
```typescript
19+
// For shiki
20+
import { getSingletonHighlighter } from 'shiki'
21+
import { createParser } from '@milkdown/plugin-highlight/shiki'
22+
const highlighter = await getSingletonHighlighter({
23+
themes: ['github-light'],
24+
langs: ['javascript', 'typescript', 'python'],
25+
})
26+
const parser = createParser(highlighter)
27+
28+
// For lowlight
29+
import 'highlight.js/styles/default.css'
30+
import { common, createLowlight } from 'lowlight'
31+
import { createParser } from '@milkdown/plugin-highlight/lowlight'
32+
const lowlight = createLowlight(common)
33+
const parser = createParser(lowlight)
34+
35+
// For refractor
36+
import { refractor } from 'refractor/all'
37+
import { createParser } from '@milkdown/plugin-highlight/refractor'
38+
const parser = createParser(refractor)
39+
40+
// For sugar high
41+
import { createParser } from '@milkdown/plugin-highlight/sugar-high'
42+
const parser = createParser()
43+
44+
// Setup
45+
import { highlight, highlightPluginConfig } from '@milkdown/plugin-highlight'
46+
Editor.make()
47+
.config((ctx) => {
48+
ctx.set(highlightPluginConfig.key, { parser })
49+
})
50+
.use(highlight)
51+
.create()
52+
```
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# @milkdown/plugin-highlight
2+
3+
The highlight plugin of [milkdown](https://milkdown.dev/).
4+
5+
# Official Documentation
6+
7+
Documentation can be found on the [Milkdown website](https://milkdown.dev/).
8+
9+
# License
10+
11+
Milkdown is open sourced software licensed under [MIT license](https://github.com/Milkdown/milkdown/blob/main/LICENSE).
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"name": "@milkdown/plugin-highlight",
3+
"type": "module",
4+
"version": "7.15.5",
5+
"license": "MIT",
6+
"repository": {
7+
"type": "git",
8+
"url": "git+https://github.com/Milkdown/milkdown.git",
9+
"directory": "packages/plugins/plugin-highlight"
10+
},
11+
"keywords": [
12+
"milkdown",
13+
"milkdown plugin"
14+
],
15+
"sideEffects": false,
16+
"exports": {
17+
".": {
18+
"import": "./src/index.ts"
19+
},
20+
"./lowlight": {
21+
"import": "./src/lowlight.ts"
22+
},
23+
"./refractor": {
24+
"import": "./src/refractor.ts"
25+
},
26+
"./shiki": {
27+
"import": "./src/shiki.ts"
28+
},
29+
"./sugar-high": {
30+
"import": "./src/sugar-high.ts"
31+
}
32+
},
33+
"main": "./src/index.ts",
34+
"publishConfig": {
35+
"main": "./lib/index.js",
36+
"types": "./lib/index.d.ts",
37+
"exports": {
38+
".": {
39+
"import": "./lib/index.js",
40+
"types": "./lib/index.d.ts"
41+
},
42+
"./lowlight": {
43+
"import": "./lib/lowlight.js",
44+
"types": "./lib/lowlight.d.ts"
45+
},
46+
"./refractor": {
47+
"import": "./lib/refractor.js",
48+
"types": "./lib/refractor.d.ts"
49+
},
50+
"./shiki": {
51+
"import": "./lib/shiki.js",
52+
"types": "./lib/shiki.d.ts"
53+
},
54+
"./sugar-high": {
55+
"import": "./lib/sugar-high.js",
56+
"types": "./lib/sugar-high.d.ts"
57+
}
58+
}
59+
},
60+
"scripts": {
61+
"build": "vite build"
62+
},
63+
"files": [
64+
"lib",
65+
"src"
66+
],
67+
"dependencies": {
68+
"@milkdown/core": "workspace:*",
69+
"@milkdown/ctx": "workspace:*",
70+
"@milkdown/prose": "workspace:*",
71+
"@milkdown/utils": "workspace:*",
72+
"prosemirror-highlight": "^0.13.0",
73+
"tslib": "^2.8.1"
74+
},
75+
"devDependencies": {
76+
"@milkdown/preset-commonmark": "workspace:*",
77+
"sugar-high": "^0.9.3"
78+
}
79+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import '@testing-library/jest-dom/vitest'
2+
import { defaultValueCtx, Editor, editorViewCtx } from '@milkdown/core'
3+
import { commonmark } from '@milkdown/preset-commonmark'
4+
import { createParser } from 'prosemirror-highlight/sugar-high'
5+
import { expect, it } from 'vitest'
6+
7+
import { highlight, highlightPluginConfig } from '..'
8+
9+
function createEditor() {
10+
const parser = createParser()
11+
const editor = Editor.make()
12+
editor
13+
.config((ctx) => {
14+
ctx.set(highlightPluginConfig.key, { parser })
15+
})
16+
.use(commonmark)
17+
.use(highlight)
18+
return editor
19+
}
20+
21+
const defaultValue = `
22+
\`\`\`js
23+
console.log('Hello, world!');
24+
\`\`\`
25+
`
26+
27+
it('should render highlighted code', async () => {
28+
const editor = createEditor()
29+
editor.config((ctx) => {
30+
ctx.set(defaultValueCtx, defaultValue)
31+
})
32+
33+
await editor.create()
34+
35+
const view = editor.ctx.get(editorViewCtx)
36+
const dom = view.dom
37+
38+
const spans = dom.querySelectorAll('span')
39+
expect(spans).toMatchInlineSnapshot(`
40+
NodeList [
41+
<span
42+
class="sh__token--identifier"
43+
style="color: var(--sh-identifier);"
44+
>
45+
console
46+
</span>,
47+
<span
48+
class="sh__token--sign"
49+
style="color: var(--sh-sign);"
50+
>
51+
.
52+
</span>,
53+
<span
54+
class="sh__token--property"
55+
style="color: var(--sh-property);"
56+
>
57+
log
58+
</span>,
59+
<span
60+
class="sh__token--sign"
61+
style="color: var(--sh-sign);"
62+
>
63+
(
64+
</span>,
65+
<span
66+
class="sh__token--string"
67+
style="color: var(--sh-string);"
68+
>
69+
'
70+
</span>,
71+
<span
72+
class="sh__token--string"
73+
style="color: var(--sh-string);"
74+
>
75+
Hello, world!
76+
</span>,
77+
<span
78+
class="sh__token--string"
79+
style="color: var(--sh-string);"
80+
>
81+
'
82+
</span>,
83+
<span
84+
class="sh__token--sign"
85+
style="color: var(--sh-sign);"
86+
>
87+
)
88+
</span>,
89+
<span
90+
class="sh__token--sign"
91+
style="color: var(--sh-sign);"
92+
>
93+
;
94+
</span>,
95+
]
96+
`)
97+
})

0 commit comments

Comments
 (0)