-
-
Notifications
You must be signed in to change notification settings - Fork 657
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(shiki): expose highlighting utils (#1727)
- Loading branch information
Showing
8 changed files
with
184 additions
and
143 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,115 +1,2 @@ | ||
import { visit } from 'unist-util-visit' | ||
import { MarkdownNode } from '../../types' | ||
import { defineTransformer } from '../utils' | ||
import { useShikiHighlighter } from './highlighter' | ||
|
||
export default defineTransformer({ | ||
name: 'highlight', | ||
extensions: ['.md'], | ||
transform: async (content, options = {}) => { | ||
const shikiHighlighter = useShikiHighlighter(options) | ||
const tokenColors: Record<string, {colors: any, className: string}> = {} | ||
const codeBlocks: any[] = [] | ||
const inlineCodes: any = [] | ||
visit( | ||
content.body, | ||
(node: any) => (node.tag === 'code' && node?.props.code) || (node.tag === 'code-inline' && (node.props?.lang || node.props?.language)), | ||
(node) => { | ||
if (node.tag === 'code') { | ||
codeBlocks.push(node) | ||
} else if (node.tag === 'code-inline') { | ||
inlineCodes.push(node) | ||
} | ||
} | ||
) | ||
|
||
await Promise.all(codeBlocks.map(highlightBlock)) | ||
await Promise.all(inlineCodes.map(highlightInline)) | ||
|
||
// Inject token colors at the end of the document | ||
if (Object.values(tokenColors).length) { | ||
const colors: string[] = [] | ||
for (const colorClass of Object.values(tokenColors)) { | ||
Object.entries(colorClass.colors).forEach(([variant, color]) => { | ||
if (variant === 'default') { | ||
colors.unshift(`.${colorClass.className}{color:${color}}`) | ||
} else { | ||
colors.push(`.${variant} .${colorClass.className}{color:${color}}`) | ||
} | ||
}) | ||
} | ||
|
||
content.body.children.push({ | ||
type: 'element', | ||
tag: 'style', | ||
children: [{ type: 'text', value: colors.join('') }] | ||
}) | ||
} | ||
|
||
return content | ||
|
||
/** | ||
* Highlight inline code | ||
*/ | ||
async function highlightInline (node: MarkdownNode) { | ||
const code = node.children![0].value! | ||
|
||
// Fetch highlighted tokens | ||
const lines = await shikiHighlighter.getHighlightedTokens(code, node.props!.lang || node.props!.language, options.theme) | ||
|
||
// Generate highlighted children | ||
node.children = lines[0].map(tokenSpan) | ||
|
||
node.props = node.props || {} | ||
node.props.class = 'colored' | ||
|
||
return node | ||
} | ||
|
||
/** | ||
* Highlight a code block | ||
*/ | ||
async function highlightBlock (node: MarkdownNode) { | ||
const { code, language: lang, highlights = [] } = node.props! | ||
|
||
// Fetch highlighted tokens | ||
const lines = await shikiHighlighter.getHighlightedTokens(code, lang, options.theme) | ||
|
||
// Generate highlighted children | ||
const innerCodeNode = node.children![0].children![0] | ||
innerCodeNode.children = lines.map((line, lineIndex) => ({ | ||
type: 'element', | ||
tag: 'span', | ||
props: { class: ['line', highlights.includes(lineIndex + 1) ? 'highlight' : ''].join(' ').trim() }, | ||
children: line.map(tokenSpan) | ||
})) | ||
return node | ||
} | ||
|
||
function getColorProps (token: { color?: string | object }) { | ||
if (!token.color) { | ||
return {} | ||
} | ||
if (typeof token.color === 'string') { | ||
return { style: { color: token.color } } | ||
} | ||
const key = Object.values(token.color).join('') | ||
if (!tokenColors[key]) { | ||
tokenColors[key] = { | ||
colors: token.color, | ||
className: 'ct-' + Math.random().toString(16).substring(2, 8) // hash(key) | ||
} | ||
} | ||
return { class: tokenColors[key].className } | ||
} | ||
|
||
function tokenSpan (token: { content: string, color?: string | object }) { | ||
return { | ||
type: 'element', | ||
tag: 'span', | ||
props: getColorProps(token), | ||
children: [{ type: 'text', value: token.content }] | ||
} | ||
} | ||
} | ||
}) | ||
export { default } from './shiki' | ||
export * from './highlighter' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { visit } from 'unist-util-visit' | ||
import { defineTransformer } from '../utils' | ||
import { useShikiHighlighter } from './highlighter' | ||
import type { TokenColorMap, MarkdownNode } from './types' | ||
|
||
export default defineTransformer({ | ||
name: 'highlight', | ||
extensions: ['.md'], | ||
transform: async (content, options = {}) => { | ||
const shikiHighlighter = useShikiHighlighter(options) | ||
const colorMap: TokenColorMap = {} | ||
const codeBlocks: any[] = [] | ||
const inlineCodes: any = [] | ||
visit( | ||
content.body, | ||
(node: any) => (node.tag === 'code' && node?.props.code) || (node.tag === 'code-inline' && (node.props?.lang || node.props?.language)), | ||
(node) => { | ||
if (node.tag === 'code') { | ||
codeBlocks.push(node) | ||
} else if (node.tag === 'code-inline') { | ||
inlineCodes.push(node) | ||
} | ||
} | ||
) | ||
|
||
await Promise.all(codeBlocks.map(highlightBlock)) | ||
await Promise.all(inlineCodes.map(highlightInline)) | ||
|
||
// Inject token colors at the end of the document | ||
if (Object.values(colorMap).length) { | ||
content.body.children.push({ | ||
type: 'element', | ||
tag: 'style', | ||
children: [{ type: 'text', value: shikiHighlighter.generateStyles(colorMap) }] | ||
}) | ||
} | ||
|
||
return content | ||
|
||
/** | ||
* Highlight inline code | ||
*/ | ||
async function highlightInline (node: MarkdownNode) { | ||
const code = node.children![0].value! | ||
|
||
// Fetch highlighted tokens | ||
const lines = await shikiHighlighter.getHighlightedAST(code, node.props!.lang || node.props!.language, options.theme, { colorMap }) | ||
|
||
// Generate highlighted children | ||
node.children = lines[0].children | ||
node.props = Object.assign(node.props || {}, { class: 'colored' }) | ||
|
||
return node | ||
} | ||
|
||
/** | ||
* Highlight a code block | ||
*/ | ||
async function highlightBlock (node: MarkdownNode) { | ||
const { code, language: lang, highlights = [] } = node.props! | ||
|
||
const innerCodeNode = node.children![0].children![0] | ||
innerCodeNode.children = await shikiHighlighter.getHighlightedAST(code, lang, options.theme, { colorMap, highlights }) | ||
|
||
return node | ||
} | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { Theme as ShikiTheme } from 'shiki-es' | ||
export type { MarkdownNode } from '../../types' | ||
|
||
export type Theme = ShikiTheme | Record<string, ShikiTheme> | ||
|
||
export type TokenColorMap = Record<string, {colors: any, className: string}> | ||
|
||
export interface HighlightParams { | ||
code: string | ||
lang: string | ||
theme: Theme | ||
} | ||
|
||
export interface HighlighterOptions { | ||
colorMap: TokenColorMap | ||
highlights: Array<number> | ||
} | ||
|
||
export interface HighlightThemedToken { | ||
content: string | ||
color?: string | Record<string, string> | ||
} | ||
|
||
export interface HighlightThemedTokenLine { | ||
key: string | ||
tokens: HighlightThemedToken[] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.