Skip to content

Commit

Permalink
chore: refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Aug 22, 2024
1 parent 61ec879 commit 4d81712
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 112 deletions.
17 changes: 11 additions & 6 deletions docs/packages/rehype.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,12 @@ console.log('4') // highlighted

### Inline Code

You can also highlight inline codes with the `code{:language}` syntax.
You can also highlight inline codes with the `inline` option.

| Option | Example | Description |
| ----------------------- | ---------------- | ----------------------------------------------------------- |
| `false` | - | Disable inline code highlighting (default) |
| `'tailing-curly-colon'` | `let a = 1{:js}` | Highlight with a `{:language}` marker inside the code block |

Enable `inline` on the Rehype plugin:

Expand All @@ -116,15 +121,15 @@ const file = await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypeShiki, {
inline: 'tailing-curly-colon',
// other options
inline: 'tailing-curly-colon', // or other options
// ...
})
.use(rehypeStringify)
.process(await fs.readFile('./input.md'))
```

Then you can use inline code in markdown:

```md
`console.log("Hello World"){:js}`
This code `console.log("Hello World"){:js}` will be highlighted.
```

Will give you a pretty looking JavaScript inline code.
124 changes: 19 additions & 105 deletions packages/rehype/src/core.ts
Original file line number Diff line number Diff line change
@@ -1,90 +1,17 @@
import type {
CodeOptionsMeta,
CodeOptionsThemes,
CodeToHastOptions,
CodeToHastOptionsCommon,
HighlighterGeneric,
TransformerOptions,
} from 'shiki/core'
import type { Element, Root } from 'hast'
import type { BuiltinTheme } from 'shiki'
import type { Transformer } from 'unified'
import { toString } from 'hast-util-to-string'
import { visit } from 'unist-util-visit'
import { InlineCodeProcessors } from './inline'
import type { RehypeShikiCoreOptions } from './types'

export interface MapLike<K = any, V = any> {
get: (key: K) => V | undefined
set: (key: K, value: V) => this
}

export interface RehypeShikiExtraOptions {
/**
* Add `language-*` class to code element
*
* @default false
*/
addLanguageClass?: boolean

/**
* The default language to use when is not specified
*/
defaultLanguage?: string

/**
* The fallback language to use when specified language is not loaded
*/
fallbackLanguage?: string

/**
* `mdast-util-to-hast` adds a newline to the end of code blocks
*
* This option strips that newline from the code block
*
* @default true
* @see https://github.com/syntax-tree/mdast-util-to-hast/blob/f511a93817b131fb73419bf7d24d73a5b8b0f0c2/lib/handlers/code.js#L22
*/
stripEndNewline?: boolean

/**
* Custom meta string parser
* Return an object to merge with `meta`
*/
parseMetaString?: (
metaString: string,
node: Element,
tree: Root,
) => Record<string, any> | undefined | null

/**
* Highlight inline code blocks
*
* @default false
*/
inline?: false | 'tailing-curly-colon'

/**
* Custom map to cache transformed codeToHast result
*
* @default undefined
*/
cache?: MapLike<string, Root>

/**
* Chance to handle the error
* If not provided, the error will be thrown
*/
onError?: (error: unknown) => void
}

export type RehypeShikiCoreOptions =
& CodeOptionsThemes<BuiltinTheme>
& TransformerOptions
& CodeOptionsMeta
& RehypeShikiExtraOptions
& Omit<CodeToHastOptionsCommon, 'lang'>
export * from './types'

const languagePrefix = 'language-'
const inlineCodeSuffix = /(.+)\{:([\w-]+)\}$/

function rehypeShikiFromHighlighter(
highlighter: HighlighterGeneric<any, any>,
Expand All @@ -109,11 +36,15 @@ function rehypeShikiFromHighlighter(
function getLanguage(lang = defaultLanguage): string | undefined {
if (lang && fallbackLanguage && !langs.includes(lang))
return fallbackLanguage

return lang
}

function processCode(lang: string, metaString: string, meta: Record<string, unknown>, code: string): Root | undefined {
function highlight(
lang: string,
code: string,
metaString: string = '',
meta: Record<string, unknown> = {},
): Root | undefined {
const cacheKey = `${lang}:${metaString}:${code}`
const cachedValue = cache?.get(cacheKey)

Expand Down Expand Up @@ -156,7 +87,8 @@ function rehypeShikiFromHighlighter(
catch (error) {
if (onError)
onError(error)

Check warning on line 89 in packages/rehype/src/core.ts

View check run for this annotation

Codecov / codecov/patch

packages/rehype/src/core.ts#L87-L89

Added lines #L87 - L89 were not covered by tests
else throw error
else
throw error
}

Check warning on line 92 in packages/rehype/src/core.ts

View check run for this annotation

Codecov / codecov/patch

packages/rehype/src/core.ts#L91-L92

Added lines #L91 - L92 were not covered by tests
}

Expand All @@ -179,37 +111,20 @@ function rehypeShikiFromHighlighter(
)
: undefined

const lang = getLanguage(typeof languageClass === 'string' ? languageClass.slice(languagePrefix.length) : undefined)
const lang = getLanguage(
typeof languageClass === 'string'
? languageClass.slice(languagePrefix.length)
: undefined,
)

if (!lang)
return

const code = toString(head)
const metaString
= head.data?.meta ?? head.properties.metastring?.toString() ?? ''
const metaString = head.data?.meta ?? head.properties.metastring?.toString() ?? ''
const meta = parseMetaString?.(metaString, node, tree) || {}

return processCode(lang, metaString, meta, code)
}

function processInlineCode(node: Element): Root | undefined {
const raw = toString(node)
const result = inlineCodeSuffix.exec(raw)
const lang = getLanguage(result?.[2])
if (!lang)
return

const code = result?.[1] ?? raw
const fragment = processCode(lang, '', {}, code)
if (!fragment)
return

const head = fragment.children[0]
if (head.type === 'element' && head.tagName === 'pre') {
head.tagName = 'span'
}

return fragment
return highlight(lang, code, metaString, meta)
}

return function (tree) {
Expand All @@ -230,8 +145,7 @@ function rehypeShikiFromHighlighter(
}

if (node.tagName === 'code' && inline) {
const result = processInlineCode(node)

const result = InlineCodeProcessors[inline]?.({ node, getLanguage, highlight })
if (result) {
parent.children.splice(index, 1, ...result.children)
}
Expand Down
2 changes: 1 addition & 1 deletion packages/rehype/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { bundledLanguages, getSingletonHighlighter } from 'shiki'
import type { Plugin } from 'unified'
import type { Root } from 'hast'
import rehypeShikiFromHighlighter from './core'
import type { RehypeShikiCoreOptions } from './core'
import type { RehypeShikiCoreOptions } from './types'

export type RehypeShikiOptions = RehypeShikiCoreOptions
& {
Expand Down
42 changes: 42 additions & 0 deletions packages/rehype/src/inline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Element, Root } from 'hast'
import { toString } from 'hast-util-to-string'
import type { RehypeShikiCoreOptions } from './types'

interface InlineCodeProcessorContext {
node: Element
getLanguage: (lang?: string) => string | undefined
highlight: (
lang: string,
code: string,
metaString?: string,
meta?: Record<string, unknown>
) => Root | undefined
}

type InlineCodeProcessor = (context: InlineCodeProcessorContext) => Root | undefined

type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T

export const InlineCodeProcessors: Record<Truthy<RehypeShikiCoreOptions['inline']>, InlineCodeProcessor> = {
'tailing-curly-colon': ({ node, getLanguage, highlight }) => {
const raw = toString(node)
const match = raw.match(/(.+)\{:([\w-]+)\}$/)
if (!match)
return
const lang = getLanguage(match[2])
if (!lang)
return

const code = match[1] ?? raw
const fragment = highlight(lang, code)
if (!fragment)
return

const head = fragment.children[0]
if (head.type === 'element' && head.tagName === 'pre') {
head.tagName = 'span'
}

return fragment
},
}
83 changes: 83 additions & 0 deletions packages/rehype/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import type { Element, Root } from 'hast'
import type {
BuiltinTheme,
CodeOptionsMeta,
CodeOptionsThemes,
CodeToHastOptionsCommon,
TransformerOptions,
} from 'shiki'

export interface MapLike<K = any, V = any> {
get: (key: K) => V | undefined
set: (key: K, value: V) => this
}

export interface RehypeShikiExtraOptions {
/**
* Add `language-*` class to code element
*
* @default false
*/
addLanguageClass?: boolean

/**
* The default language to use when is not specified
*/
defaultLanguage?: string

/**
* The fallback language to use when specified language is not loaded
*/
fallbackLanguage?: string

/**
* `mdast-util-to-hast` adds a newline to the end of code blocks
*
* This option strips that newline from the code block
*
* @default true
* @see https://github.com/syntax-tree/mdast-util-to-hast/blob/f511a93817b131fb73419bf7d24d73a5b8b0f0c2/lib/handlers/code.js#L22
*/
stripEndNewline?: boolean

/**
* Custom meta string parser
* Return an object to merge with `meta`
*/
parseMetaString?: (
metaString: string,
node: Element,
tree: Root
) => Record<string, any> | undefined | null

/**
* Highlight inline code blocks
*
* - `false`: disable inline code block highlighting
* - `tailing-curly-colon`: highlight with `\`code{:lang}\``
*
* @see https://shiki.style/packages/rehype#inline-code
* @default false
*/
inline?: false | 'tailing-curly-colon'

/**
* Custom map to cache transformed codeToHast result
*
* @default undefined
*/
cache?: MapLike<string, Root>

/**
* Chance to handle the error
* If not provided, the error will be thrown
*/
onError?: (error: unknown) => void
}

export type RehypeShikiCoreOptions =
& CodeOptionsThemes<BuiltinTheme>
& TransformerOptions
& CodeOptionsMeta
& RehypeShikiExtraOptions
& Omit<CodeToHastOptionsCommon, 'lang'>

0 comments on commit 4d81712

Please sign in to comment.