Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 49 additions & 35 deletions server/utils/docs/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,14 @@ export async function renderDocNodes(
symbolLookup: SymbolLookup,
): Promise<string> {
const grouped = groupMergedByKind(symbols)
const sections: string[] = []

for (const kind of KIND_DISPLAY_ORDER) {
const sectionPromises = KIND_DISPLAY_ORDER.map(async kind => {
const kindSymbols = grouped[kind]
if (!kindSymbols || kindSymbols.length === 0) continue
if (!kindSymbols || kindSymbols.length === 0) return ''
return renderKindSection(kind, kindSymbols, symbolLookup)
})

sections.push(await renderKindSection(kind, kindSymbols, symbolLookup))
}

return sections.join('\n')
const sections = await Promise.all(sectionPromises)
return sections.filter(Boolean).join('\n')
}

/**
Expand All @@ -79,13 +77,13 @@ async function renderKindSection(
): Promise<string> {
const title = KIND_TITLES[kind] || kind
const lines: string[] = []
const renderedSymbols = await Promise.all(
symbols.map(symbol => renderMergedSymbol(symbol, symbolLookup)),
)

lines.push(`<section class="docs-section" id="section-${kind}">`)
lines.push(`<h2 class="docs-section-title">${title}</h2>`)

for (const symbol of symbols) {
lines.push(await renderMergedSymbol(symbol, symbolLookup))
}
lines.push(...renderedSymbols)

lines.push(`</section>`)

Expand Down Expand Up @@ -129,9 +127,21 @@ async function renderMergedSymbol(
.map(n => getNodeSignature(n))
.filter(Boolean) as string[]

if (signatures.length > 0) {
const signatureCode = signatures.join('\n')
const highlightedSignature = await highlightCodeBlock(signatureCode, 'typescript')
const description = symbol.jsDoc?.doc?.trim()
const signaturePromise =
signatures.length > 0 ? highlightCodeBlock(signatures.join('\n'), 'typescript') : null
const descriptionPromise = description ? renderMarkdown(description, symbolLookup) : null
const jsDocTagsPromise =
symbol.jsDoc?.tags && symbol.jsDoc.tags.length > 0
? renderJsDocTags(symbol.jsDoc.tags, symbolLookup)
: null
const [highlightedSignature, renderedDescription, renderedJsDocTags] = await Promise.all([
signaturePromise,
descriptionPromise,
jsDocTagsPromise,
])

if (highlightedSignature) {
lines.push(`<div class="docs-signature">${highlightedSignature}</div>`)

if (symbol.nodes.length > MAX_OVERLOAD_SIGNATURES) {
Expand All @@ -141,16 +151,13 @@ async function renderMergedSymbol(
}

// Description
if (symbol.jsDoc?.doc) {
const description = symbol.jsDoc.doc.trim()
lines.push(
`<div class="docs-description">${await renderMarkdown(description, symbolLookup)}</div>`,
)
if (renderedDescription) {
lines.push(`<div class="docs-description">${renderedDescription}</div>`)
}

// JSDoc tags
if (symbol.jsDoc?.tags && symbol.jsDoc.tags.length > 0) {
lines.push(await renderJsDocTags(symbol.jsDoc.tags, symbolLookup))
if (renderedJsDocTags) {
lines.push(renderedJsDocTags)
}

// Type-specific members
Expand Down Expand Up @@ -179,15 +186,30 @@ async function renderJsDocTags(tags: JsDocTag[], symbolLookup: SymbolLookup): Pr
const deprecated = tags.find(t => t.kind === 'deprecated')
const see = tags.filter(t => t.kind === 'see')

const deprecatedMessagePromise = deprecated?.doc
? renderMarkdown(deprecated.doc.replace(/\n/g, ' '), symbolLookup)
: null
const examplePromises = examples.map(async example => {
if (!example.doc) return ''
const langMatch = example.doc.match(/```(\w+)?/)
const lang = langMatch?.[1] || 'typescript'
const code = example.doc.replace(/```\w*\n?/g, '').trim()
return highlightCodeBlock(code, lang)
})

const [renderedDeprecatedMessage, ...renderedExamples] = await Promise.all([
deprecatedMessagePromise,
...examplePromises,
])

// Deprecated warning
if (deprecated) {
lines.push(`<div class="docs-deprecated">`)
lines.push(`<strong>Deprecated</strong>`)
if (deprecated.doc) {
if (renderedDeprecatedMessage) {
// We remove new lines because they look weird when rendered into the deprecated block
// I think markdown is actually supposed to collapse single new lines automatically but this function doesn't do that so if that changes remove this
const renderedMessage = await renderMarkdown(deprecated.doc.replace(/\n/g, ' '), symbolLookup)
lines.push(`<div class="docs-deprecated-message">${renderedMessage}</div>`)
lines.push(`<div class="docs-deprecated-message">${renderedDeprecatedMessage}</div>`)
}
lines.push(`</div>`)
}
Expand Down Expand Up @@ -218,18 +240,10 @@ async function renderJsDocTags(tags: JsDocTag[], symbolLookup: SymbolLookup): Pr
}

// Examples (with syntax highlighting)
if (examples.length > 0) {
if (examples.length > 0 && renderedExamples.some(Boolean)) {
lines.push(`<div class="docs-examples">`)
lines.push(`<h4>Example${examples.length > 1 ? 's' : ''}</h4>`)
for (const example of examples) {
if (example.doc) {
const langMatch = example.doc.match(/```(\w+)?/)
const lang = langMatch?.[1] || 'typescript'
const code = example.doc.replace(/```\w*\n?/g, '').trim()
const highlighted = await highlightCodeBlock(code, lang)
lines.push(highlighted)
}
}
lines.push(...renderedExamples.filter(Boolean))
lines.push(`</div>`)
}

Expand Down
12 changes: 7 additions & 5 deletions server/utils/docs/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,13 @@ export async function renderMarkdown(text: string, symbolLookup: SymbolLookup):
.replace(/\n/g, '<br>')

// Highlight and restore code blocks
for (let i = 0; i < codeBlockData.length; i++) {
const { lang, code } = codeBlockData[i]!
const highlighted = await highlightCodeBlock(code, lang)
result = result.replace(`__CODE_BLOCK_${i}__`, highlighted)
}
const highlightedCodeBlocks = await Promise.all(
codeBlockData.map(({ lang, code }) => highlightCodeBlock(code, lang)),
)

highlightedCodeBlocks.forEach((highlighted, i) => {
result = result.replace(`__CODE_BLOCK_${i}__`, () => highlighted)
})

return result
}
120 changes: 66 additions & 54 deletions server/utils/shiki.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createHighlighterCore, type HighlighterCore } from 'shiki/core'
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'

let highlighter: HighlighterCore | null = null
let highlighterPromise: Promise<HighlighterCore> | null = null

function replaceThemeColors(
theme: ThemeRegistration,
Expand All @@ -18,65 +19,76 @@ function replaceThemeColors(
}

export async function getShikiHighlighter(): Promise<HighlighterCore> {
if (!highlighter) {
highlighter = await createHighlighterCore({
themes: [
import('@shikijs/themes/github-dark'),
import('@shikijs/themes/github-light').then(t =>
replaceThemeColors(t.default ?? t, {
'#22863A': '#227436', // green
'#E36209': '#BA4D02', // orange
'#D73A49': '#CD3443', // red
'#B31D28': '#AC222F', // red
}),
),
],
langs: [
// Core web languages
import('@shikijs/langs/javascript'),
import('@shikijs/langs/typescript'),
import('@shikijs/langs/json'),
import('@shikijs/langs/jsonc'),
import('@shikijs/langs/html'),
import('@shikijs/langs/css'),
import('@shikijs/langs/scss'),
import('@shikijs/langs/less'),
if (highlighter) {
return highlighter
}

highlighterPromise ??= createHighlighterCore({
themes: [
import('@shikijs/themes/github-dark'),
import('@shikijs/themes/github-light').then(t =>
replaceThemeColors(t.default ?? t, {
'#22863A': '#227436', // green
'#E36209': '#BA4D02', // orange
'#D73A49': '#CD3443', // red
'#B31D28': '#AC222F', // red
}),
),
],
langs: [
// Core web languages
import('@shikijs/langs/javascript'),
import('@shikijs/langs/typescript'),
import('@shikijs/langs/json'),
import('@shikijs/langs/jsonc'),
import('@shikijs/langs/html'),
import('@shikijs/langs/css'),
import('@shikijs/langs/scss'),
import('@shikijs/langs/less'),

// Frameworks
import('@shikijs/langs/vue'),
import('@shikijs/langs/jsx'),
import('@shikijs/langs/tsx'),
import('@shikijs/langs/svelte'),
import('@shikijs/langs/astro'),
import('@shikijs/langs/glimmer-js'),
import('@shikijs/langs/glimmer-ts'),
// Frameworks
import('@shikijs/langs/vue'),
import('@shikijs/langs/jsx'),
import('@shikijs/langs/tsx'),
import('@shikijs/langs/svelte'),
import('@shikijs/langs/astro'),
import('@shikijs/langs/glimmer-js'),
import('@shikijs/langs/glimmer-ts'),

// Shell/CLI
import('@shikijs/langs/bash'),
import('@shikijs/langs/shell'),
// Shell/CLI
import('@shikijs/langs/bash'),
import('@shikijs/langs/shell'),

// Config/Data formats
import('@shikijs/langs/yaml'),
import('@shikijs/langs/toml'),
import('@shikijs/langs/xml'),
import('@shikijs/langs/markdown'),
// Config/Data formats
import('@shikijs/langs/yaml'),
import('@shikijs/langs/toml'),
import('@shikijs/langs/xml'),
import('@shikijs/langs/markdown'),

// Other languages
import('@shikijs/langs/diff'),
import('@shikijs/langs/sql'),
import('@shikijs/langs/graphql'),
import('@shikijs/langs/python'),
import('@shikijs/langs/rust'),
import('@shikijs/langs/go'),
],
langAlias: {
gjs: 'glimmer-js',
gts: 'glimmer-ts',
},
engine: createJavaScriptRegexEngine(),
// Other languages
import('@shikijs/langs/diff'),
import('@shikijs/langs/sql'),
import('@shikijs/langs/graphql'),
import('@shikijs/langs/python'),
import('@shikijs/langs/rust'),
import('@shikijs/langs/go'),
],
langAlias: {
gjs: 'glimmer-js',
gts: 'glimmer-ts',
},
engine: createJavaScriptRegexEngine(),
})
.then(createdHighlighter => {
highlighter = createdHighlighter
return createdHighlighter
})
}
return highlighter
.catch(error => {
highlighterPromise = null
throw error
})

return highlighterPromise
}

/**
Expand Down
61 changes: 61 additions & 0 deletions test/unit/server/utils/docs/render.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,37 @@ function createClassSymbol(classDef: DenoDocNode['classDef']): MergedSymbol {
}
}

function createFunctionSymbol(name: string): MergedSymbol {
const node: DenoDocNode = {
name,
kind: 'function',
functionDef: {
params: [],
returnType: { repr: 'void', kind: 'keyword', keyword: 'void' },
},
}

return {
name,
kind: 'function',
nodes: [node],
}
}

function createInterfaceSymbol(name: string): MergedSymbol {
const node: DenoDocNode = {
name,
kind: 'interface',
interfaceDef: {},
}

return {
name,
kind: 'interface',
nodes: [node],
}
}

describe('issue #1943 - class getters separated from methods', () => {
it('renders getters under a "Getters" heading, not "Methods"', async () => {
const symbol = createClassSymbol({
Expand Down Expand Up @@ -131,3 +162,33 @@ describe('issue #1943 - class getters separated from methods', () => {
expect(html).toContain('static get instance')
})
})

describe('renderDocNodes ordering', () => {
it('preserves kind display order while rendering sections in parallel', async () => {
const html = await renderDocNodes(
[createInterfaceSymbol('Config'), createFunctionSymbol('run')],
new Map(),
)

const functionsIndex = html.indexOf('id="section-function"')
const interfacesIndex = html.indexOf('id="section-interface"')

expect(functionsIndex).toBeGreaterThanOrEqual(0)
expect(interfacesIndex).toBeGreaterThanOrEqual(0)
expect(functionsIndex).toBeLessThan(interfacesIndex)
})

it('preserves symbol order within a section while rendering symbols in parallel', async () => {
const html = await renderDocNodes(
[createFunctionSymbol('alpha'), createFunctionSymbol('beta')],
new Map(),
)

const alphaIndex = html.indexOf('id="function-alpha"')
const betaIndex = html.indexOf('id="function-beta"')

expect(alphaIndex).toBeGreaterThanOrEqual(0)
expect(betaIndex).toBeGreaterThanOrEqual(0)
expect(alphaIndex).toBeLessThan(betaIndex)
})
})
Loading