Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit ccc8fd9

Browse files
committed
Markdown plugin support code hightlight
1 parent 37ddd04 commit ccc8fd9

File tree

2 files changed

+268
-22
lines changed

2 files changed

+268
-22
lines changed

plugins/markdown.ts

Lines changed: 267 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,217 @@
11
import type { Aleph, LoadInput, LoadOutput, ResolveResult, Plugin } from '../types.d.ts'
2-
import marked from 'https://esm.sh/marked@2.0.1'
2+
import marked from 'https://esm.sh/marked@3.0.4'
33
import { safeLoadFront } from 'https://esm.sh/[email protected]'
44
import util from '../shared/util.ts'
55

6-
export const test = /\.(md|markdown)$/i
6+
const hljsUri = 'https://esm.sh/[email protected]'
7+
const languages = new Set([
8+
'1c',
9+
'abnf',
10+
'accesslog',
11+
'actionscript',
12+
'ada',
13+
'angelscript',
14+
'apache',
15+
'applescript',
16+
'arcade',
17+
'arduino',
18+
'armasm',
19+
'xml',
20+
'asciidoc',
21+
'aspectj',
22+
'autohotkey',
23+
'autoit',
24+
'avrasm',
25+
'awk',
26+
'axapta',
27+
'bash',
28+
'basic',
29+
'bnf',
30+
'brainfuck',
31+
'c',
32+
'cal',
33+
'capnproto',
34+
'ceylon',
35+
'clean',
36+
'clojure',
37+
'clojure-repl',
38+
'cmake',
39+
'coffeescript',
40+
'coq',
41+
'cos',
42+
'cpp',
43+
'crmsh',
44+
'crystal',
45+
'csharp',
46+
'csp',
47+
'css',
48+
'd',
49+
'markdown',
50+
'dart',
51+
'delphi',
52+
'diff',
53+
'django',
54+
'dns',
55+
'dockerfile',
56+
'dos',
57+
'dsconfig',
58+
'dts',
59+
'dust',
60+
'ebnf',
61+
'elixir',
62+
'elm',
63+
'ruby',
64+
'erb',
65+
'erlang-repl',
66+
'erlang',
67+
'excel',
68+
'fix',
69+
'flix',
70+
'fortran',
71+
'fsharp',
72+
'gams',
73+
'gauss',
74+
'gcode',
75+
'gherkin',
76+
'glsl',
77+
'gml',
78+
'go',
79+
'golo',
80+
'gradle',
81+
'groovy',
82+
'haml',
83+
'handlebars',
84+
'haskell',
85+
'haxe',
86+
'hsp',
87+
'http',
88+
'hy',
89+
'inform7',
90+
'ini',
91+
'irpf90',
92+
'isbl',
93+
'java',
94+
'javascript',
95+
'jboss-cli',
96+
'json',
97+
'julia',
98+
'julia-repl',
99+
'kotlin',
100+
'lasso',
101+
'latex',
102+
'ldif',
103+
'leaf',
104+
'less',
105+
'lisp',
106+
'livecodeserver',
107+
'livescript',
108+
'llvm',
109+
'lsl',
110+
'lua',
111+
'makefile',
112+
'mathematica',
113+
'matlab',
114+
'maxima',
115+
'mel',
116+
'mercury',
117+
'mipsasm',
118+
'mizar',
119+
'perl',
120+
'mojolicious',
121+
'monkey',
122+
'moonscript',
123+
'n1ql',
124+
'nestedtext',
125+
'nginx',
126+
'nim',
127+
'nix',
128+
'node-repl',
129+
'nsis',
130+
'objectivec',
131+
'ocaml',
132+
'openscad',
133+
'oxygene',
134+
'parser3',
135+
'pf',
136+
'pgsql',
137+
'php',
138+
'php-template',
139+
'plaintext',
140+
'pony',
141+
'powershell',
142+
'processing',
143+
'profile',
144+
'prolog',
145+
'properties',
146+
'protobuf',
147+
'puppet',
148+
'purebasic',
149+
'python',
150+
'python-repl',
151+
'q',
152+
'qml',
153+
'r',
154+
'reasonml',
155+
'rib',
156+
'roboconf',
157+
'routeros',
158+
'rsl',
159+
'ruleslanguage',
160+
'rust',
161+
'sas',
162+
'scala',
163+
'scheme',
164+
'scilab',
165+
'scss',
166+
'shell',
167+
'smali',
168+
'smalltalk',
169+
'sml',
170+
'sqf',
171+
'sql',
172+
'stan',
173+
'stata',
174+
'step21',
175+
'stylus',
176+
'subunit',
177+
'swift',
178+
'taggerscript',
179+
'yaml',
180+
'tap',
181+
'tcl',
182+
'thrift',
183+
'tp',
184+
'twig',
185+
'typescript',
186+
'vala',
187+
'vbnet',
188+
'vbscript',
189+
'vbscript-html',
190+
'verilog',
191+
'vhdl',
192+
'vim',
193+
'wasm',
194+
'wren',
195+
'x86asm',
196+
'xl',
197+
'xquery',
198+
'zephir',
199+
])
200+
const languageAlias = {
201+
'js': 'javascript',
202+
'jsx': 'javascript',
203+
'ts': 'typescript',
204+
'tsx': 'typescript',
205+
'py': 'python',
206+
'make': 'makefile',
207+
'md': 'markdown',
208+
'ps': 'powershell',
209+
'rs': 'rust',
210+
'styl': 'stylus',
211+
}
212+
213+
const test = /\.(md|markdown)$/i
214+
const reCodeLanguage = /<code class="language\-([^"]+)"/g
7215

8216
export const markdownResovler = (specifier: string): ResolveResult => {
9217
let pagePath = util.trimPrefix(specifier.replace(/\.(md|markdown)$/i, ''), '/pages')
@@ -17,43 +225,83 @@ export const markdownResovler = (specifier: string): ResolveResult => {
17225
return { asPage: { path: pagePath, isIndex } }
18226
}
19227

20-
export const markdownLoader = async ({ specifier }: LoadInput, aleph: Aleph): Promise<LoadOutput> => {
228+
export const markdownLoader = async ({ specifier }: LoadInput, aleph: Aleph, { highlight }: Options = {}): Promise<LoadOutput> => {
21229
const { framework } = aleph.config
22230
const { content } = await aleph.fetchModule(specifier)
23231
const { __content, ...meta } = safeLoadFront((new TextDecoder).decode(content))
24232
const html = marked.parse(__content)
25233
const props = {
26234
id: util.isString(meta.id) ? meta.id : undefined,
27-
className: util.isString(meta.className) ? meta.className : undefined,
28-
style: util.isPlainObject(meta.style) ? meta.style : undefined,
235+
className: util.isString(meta.className) ? meta.className.trim() : undefined,
236+
style: util.isPlainObject(meta.style) ? Object.entries(meta.style).reduce((prev, [key, value]) => {
237+
prev[key.replaceAll(/\-[a-z]/g, m => m.slice(1).toUpperCase())] = value
238+
return prev
239+
}, {} as Record<string, any>) : undefined,
240+
}
241+
const code = [
242+
`import { createElement, useEffect, useRef } from 'https://esm.sh/react'`,
243+
`import HTMLPage from 'https://deno.land/x/aleph/framework/react/components/HTMLPage.ts'`,
244+
`export default function MarkdownPage(props) {`,
245+
` return createElement(HTMLPage, {`,
246+
` ...${JSON.stringify(props)},`,
247+
` ...props,`,
248+
` html: ${JSON.stringify(html)}`,
249+
` })`,
250+
`}`,
251+
`MarkdownPage.meta = ${JSON.stringify(meta)}`,
252+
]
253+
if (highlight) {
254+
const extra: string[] = [`import hljs from '${hljsUri}/lib/core'`]
255+
const activated: Set<string> = new Set()
256+
const hooks = [
257+
` const ref = useRef()`,
258+
` useEffect(() => ref.current && ref.current.querySelectorAll('code').forEach(el => hljs.highlightElement(el)), [])`
259+
]
260+
for (const m of html.matchAll(reCodeLanguage)) {
261+
let lang = m[1]
262+
if (lang === 'jsx' || lang === 'tsx') {
263+
activated.add('xml')
264+
}
265+
if (lang in languageAlias) {
266+
lang = (languageAlias as any)[lang]
267+
}
268+
if (languages.has(lang)) {
269+
activated.add(lang)
270+
}
271+
}
272+
activated.forEach(lang => {
273+
extra.push(
274+
`import ${lang} from '${hljsUri}/lib/languages/${lang}'`,
275+
`hljs.registerLanguage('${lang}', ${lang})`
276+
)
277+
})
278+
code.splice(6, 0, ` ref,`)
279+
code.splice(3, 0, ...hooks)
280+
code.unshift(...extra, `import '${hljsUri}/styles/${highlight.theme || 'default'}.css'`)
29281
}
30282

31283
if (framework === 'react') {
32284
return {
33-
code: [
34-
`import { createElement } from 'https://esm.sh/react'`,
35-
`import HTMLPage from 'https://deno.land/x/aleph/framework/react/components/HTMLPage.ts'`,
36-
`export default function MarkdownPage(props) {`,
37-
` return createElement(HTMLPage, {`,
38-
` ...${JSON.stringify(props)},`,
39-
` ...props,`,
40-
` html: ${JSON.stringify(html)}`,
41-
` })`,
42-
`}`,
43-
`MarkdownPage.meta = ${JSON.stringify(meta)}`,
44-
].join('\n')
285+
code: code.join('\n')
45286
}
46287
}
47288

48289
throw new Error(`markdown-loader: don't support framework '${framework}'`)
49290
}
50291

51-
export default (): Plugin => {
292+
export type Options = {
293+
highlight?: {
294+
provider: 'highlight.js', // todo: support prism and other libs
295+
theme?: string
296+
}
297+
}
298+
299+
export default (options?: Options): Plugin => {
52300
return {
53301
name: 'markdown-loader',
54302
setup: aleph => {
55303
aleph.onResolve(test, markdownResovler)
56-
aleph.onLoad(test, input => markdownLoader(input, aleph))
304+
aleph.onLoad(test, input => markdownLoader(input, aleph, options))
57305
}
58306
}
59307
}

plugins/markdown_test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { join } from 'std/path/mod.ts'
22
import { assert, assertEquals } from 'std/testing/asserts.ts'
33
import { Aleph } from '../server/aleph.ts'
44
import { ensureTextFile } from '../shared/fs.ts'
5-
import { markdownResovler, markdownLoader, test } from './markdown.ts'
5+
import { markdownResovler, markdownLoader } from './markdown.ts'
66

77
Deno.test('plugin: markdown loader', async () => {
88
Deno.env.set('DENO_TESTING', 'true')
@@ -25,8 +25,6 @@ Deno.test('plugin: markdown loader', async () => {
2525
)
2626
const { code } = await markdownLoader({ specifier: '/pages/docs/index.md', }, aleph)
2727

28-
assert(test.test('/test.md'))
29-
assert(test.test('/test.markdown'))
3028
assertEquals(markdownResovler('/pages/docs/index.md').asPage, { path: '/docs', isIndex: true })
3129
assertEquals(markdownResovler('/pages/docs/get-started.md').asPage, { path: '/docs/get-started', isIndex: false })
3230
assert(code.includes('html: "<h1 id=\\"alephjs\\">Aleph.js</h1>\\n<p>The Full-stack Framework in Deno.</p>\\n"'))

0 commit comments

Comments
 (0)