Skip to content

Commit

Permalink
feat(core): new structure: inline option for codeToHast (#653)
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Apr 9, 2024
1 parent 71257dd commit ae516ae
Show file tree
Hide file tree
Showing 35 changed files with 9,674 additions and 10,550 deletions.
48 changes: 32 additions & 16 deletions packages/core/src/code-to-hast.ts
Expand Up @@ -77,11 +77,15 @@ export function tokensToHast(
const transformers = getTransformers(options)

const lines: (Element | Text)[] = []
const tree: Root = {
const root: Root = {
type: 'root',
children: [],
}

const {
structure = 'classic',
} = options

let preNode: Element = {
type: 'element',
tagName: 'pre',
Expand Down Expand Up @@ -110,6 +114,7 @@ export function tokensToHast(

const context: ShikiTransformerContext = {
...transformerContext,
structure,
addClassToHast,
get source() {
return transformerContext.source
Expand All @@ -121,7 +126,7 @@ export function tokensToHast(
return options
},
get root() {
return tree
return root
},
get pre() {
return preNode
Expand All @@ -135,8 +140,12 @@ export function tokensToHast(
}

tokens.forEach((line, idx) => {
if (idx)
lines.push({ type: 'text', value: '\n' })
if (idx) {
if (structure === 'inline')
root.children.push({ type: 'element', tagName: 'br', properties: {}, children: [] })
else if (structure === 'classic')
lines.push({ type: 'text', value: '\n' })
}

let lineNode: Element = {
type: 'element',
Expand All @@ -162,28 +171,35 @@ export function tokensToHast(
for (const transformer of transformers)
tokenNode = transformer?.span?.call(context, tokenNode, idx + 1, col, lineNode) || tokenNode

lineNode.children.push(tokenNode)
if (structure === 'inline')
root.children.push(tokenNode)
else if (structure === 'classic')
lineNode.children.push(tokenNode)
col += token.content.length
}

for (const transformer of transformers)
lineNode = transformer?.line?.call(context, lineNode, idx + 1) || lineNode
if (structure === 'classic') {
for (const transformer of transformers)
lineNode = transformer?.line?.call(context, lineNode, idx + 1) || lineNode

lineNodes.push(lineNode)
lines.push(lineNode)
lineNodes.push(lineNode)
lines.push(lineNode)
}
})

for (const transformer of transformers)
codeNode = transformer?.code?.call(context, codeNode) || codeNode
if (structure === 'classic') {
for (const transformer of transformers)
codeNode = transformer?.code?.call(context, codeNode) || codeNode

preNode.children.push(codeNode)
preNode.children.push(codeNode)

for (const transformer of transformers)
preNode = transformer?.pre?.call(context, preNode) || preNode
for (const transformer of transformers)
preNode = transformer?.pre?.call(context, preNode) || preNode

tree.children.push(preNode)
root.children.push(preNode)
}

let result = tree
let result = root
for (const transformer of transformers)
result = transformer?.root?.call(context, result) || result

Expand Down
10 changes: 10 additions & 0 deletions packages/core/src/types/options.ts
Expand Up @@ -132,6 +132,16 @@ export interface CodeToHastOptionsCommon<Languages extends string = string>
* @default true
*/
mergeWhitespaces?: boolean | 'never'

/**
* The structure of the generated HAST and HTML.
*
* - `classic`: The classic structure with `<pre>` and `<code>` elements, each line wrapped with a `<span class="line">` element.
* - `inline`: All tokens are rendered as `<span>`, line breaks are rendered as `<br>`. No `<pre>` or `<code>` elements. Default forground and background colors are not applied.
*
* @default 'classic'
*/
structure?: 'classic' | 'inline'
}

export interface CodeOptionsMeta {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types/transformers.ts
Expand Up @@ -35,6 +35,8 @@ export interface ShikiTransformerContext extends ShikiTransformerContextSource {
readonly code: Element
readonly lines: Element[]

readonly structure: CodeToHastOptions['structure']

/**
* Utility to append class to a hast node
*
Expand Down
16 changes: 16 additions & 0 deletions packages/shiki/test/hast.test.ts
Expand Up @@ -22,6 +22,22 @@ describe('should', () => {
<span class="line"><span style="color:#B07D48">foo</span><span style="color:#999999">.</span><span style="color:#B07D48">bar</span></span></code></pre>"
`)
})

it('structure inline', async () => {
const shiki = await getHighlighter({
themes: ['vitesse-light'],
langs: ['javascript'],
})

const hast = shiki.codeToHast('console.log\nfoo.bar', {
lang: 'js',
theme: 'vitesse-light',
structure: 'inline',
})

expect(toHtml(hast))
.toMatchInlineSnapshot(`"<span style="color:#B07D48">console</span><span style="color:#999999">.</span><span style="color:#B07D48">log</span><br><span style="color:#B07D48">foo</span><span style="color:#999999">.</span><span style="color:#B07D48">bar</span>"`)
})
})

it('hasfocus support', async () => {
Expand Down
26 changes: 16 additions & 10 deletions packages/twoslash/src/renderer-rich.ts
Expand Up @@ -223,16 +223,22 @@ export function rendererRich(options: RendererRichOptions = {}): TwoslashRendere

const popupContents: ElementContent[] = []

const typeCode = ((this.codeToHast(
content,
{
...this.options,
transformers: [],
lang: (this.options.lang === 'tsx' || this.options.lang === 'jsx')
? 'tsx'
: 'ts',
},
).children[0] as Element).children as Element[])[0]
const typeCode: Element = {
type: 'element',
tagName: 'code',
properties: {},
children: this.codeToHast(
content,
{
...this.options,
transformers: [],
lang: (this.options.lang === 'tsx' || this.options.lang === 'jsx')
? 'tsx'
: 'ts',
structure: 'inline',
},
).children as ElementContent[],
}
typeCode.properties.class = 'twoslash-popup-code'

popupContents.push(
Expand Down
4 changes: 2 additions & 2 deletions packages/twoslash/test/out/completion-end-multifile-2.ts.html

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit ae516ae

Please sign in to comment.