Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"name": "purestack-repo",
"version": "1.1.10",
"version": "1.1.11",
"packageManager": "yarn@4.18.1",
"type": "module",
"sideEffects": false,
Expand Down
2 changes: 1 addition & 1 deletion packages/purestack/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "purestack",
"version": "1.1.10",
"version": "1.1.11",
"packageManager": "yarn@4.9.2",
"module": "./dist/purestack.mjs",
"types": "./dist/purestack.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/purestack/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const version: string = '1.1.10'
export const version: string = '1.1.11'

export * from '@purestack/ts-ssg'
2 changes: 1 addition & 1 deletion packages/ts-common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-common",
"version": "1.1.10",
"version": "1.1.11",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-common.mjs",
"types": "./dist/ts-common.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-components",
"version": "1.1.10",
"version": "1.1.11",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-components.mjs",
"types": "./dist/ts-components.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-css/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-css",
"version": "1.1.10",
"version": "1.1.11",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-css.mjs",
"types": "./dist/ts-css.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-html/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-html",
"version": "1.1.10",
"version": "1.1.11",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-html.mjs",
"types": "./dist/ts-html.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-minidom/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-minidom",
"version": "1.1.10",
"version": "1.1.11",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-minidom.mjs",
"types": "./dist/ts-minidom.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-page-scripts/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-page-scripts",
"version": "1.1.10",
"version": "1.1.11",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-page-scripts.mjs",
"types": "./dist/ts-page-scripts.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-render/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-render",
"version": "1.1.10",
"version": "1.1.11",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-render.mjs",
"types": "./dist/ts-render.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-ssg-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "purestack-component-navigation",
"displayName": "PureStack Component Tools",
"description": "Component navigation, prop IntelliSense, lit-html syntax highlighting, and template formatting for Markdown, Regor MDX, and TypeScript templates.",
"version": "1.1.10",
"version": "1.1.11",
"private": true,
"publisher": "purestack",
"license": "MIT",
Expand Down
67 changes: 66 additions & 1 deletion packages/ts-ssg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,71 @@ Plugins apply in order:

Each plugin is checked when the build starts. An unknown field or hook name, such as `onPageRender`, a value of the wrong type, two plugins sharing a name, or two plugins defining the same skin, component, template, or generated page fails the build with the plugin's name. For a single hook, pass an inline plugin: `{ name: 'site', hooks: { … } }`.

### Code highlighting

Use `markdown.resolveHighlighter(lang)` to select a code-to-HTML callback for a language:

```ts
import { definePlugin } from '@purestack/ts-ssg'
import { renderCustomCode } from './custom-code'

export const customCodePlugin = definePlugin({
name: 'custom-code',
markdown: {
resolveHighlighter: (lang) => lang === 'custom' ? renderCustomCode : null,
},
})
```

The resolver can be synchronous or asynchronous and receives the trimmed, lowercase language, or `''` when absent. Its callback accepts `(code, lang?)` and returns the HTML as `string | Promise<string>`. Resolvers are awaited in plugin order; the first callback returned handles the block. If all return `null`, the configured default highlighter handles it, including its existing behavior for unknown languages.

Custom callbacks also work when `mdx.disableHighlighter` is `true`. In that case, if all resolvers return `null`, the source is HTML-escaped and displayed in a plain `<pre><code>` block without syntax highlighting.

#### Backend factories

Use `markdown.createShiki(options)` or `markdown.createHighlightJs()` to create the default backend instance. Both factories can return an instance synchronously or asynchronously. Only the selected backend's factory runs, once when a build or dev server initializes its highlighter. If no factory is provided, PureStack uses its built-in factory. Factories are skipped when `mdx.disableHighlighter` is `true`. Two plugins providing a factory for the selected backend fail with both plugin names.

The Shiki factory receives the native creation options containing PureStack's default languages and light/dark themes. Extend those options to add a TextMate grammar:

```ts
import { definePlugin } from '@purestack/ts-ssg'
import { createHighlighter } from 'shiki'
import grammar from './mylang.tmLanguage.json' with { type: 'json' }

export const languagePlugin = definePlugin({
name: 'mylang',
markdown: {
createShiki: (options) => createHighlighter({
...options,
langs: [...options.langs, grammar],
}),
},
})
```

The highlight.js factory returns a configured `HLJSApi` instance. A fresh instance starts without languages, so register the languages your site needs:

```ts
import { definePlugin } from '@purestack/ts-ssg'
import hljs from 'highlight.js'
import typescript from 'highlight.js/lib/languages/typescript'
import myLanguage from './mylang'

export const languagePlugin = definePlugin({
name: 'mylang',
markdown: {
createHighlightJs: () => {
const instance = hljs.newInstance()
instance.registerLanguage('typescript', typescript)
instance.registerLanguage('mylang', myLanguage)
return instance
},
},
})
```

These factories provide native backend instances for the default HTML adapters. `resolveHighlighter(lang)` still runs first for each code block; its returned callback takes precedence over the configured backend.

### Hooks

Every page render, in a full build or a dev server re-render, runs the page hooks:
Expand Down Expand Up @@ -618,7 +683,7 @@ Types:
- Templates: `PageTemplateMap`, `PageTemplate`, `PageTemplateInput`, `PageInfo`, `PageFrontmatter`
- Config and components: `SiteConfig`, `SiteConfigInput`, `PageContext`
- Dev server: `DevServerInput`, `DevServerOptions`, `DevServerHandle`
- Highlighting: `MdxCodeHighlighter`, `MdxCodeLangs`, `MdxCodeThemes`
- Highlighting: `MdxCodeHighlighter`, `MdxCodeLangs`, `MdxCodeThemes`, `MdxShikiFactory`

Related packages: `@purestack/ts-html` builds template markup (`h`), `@purestack/ts-style` provides skins (`themeSkins`, `ThemeSkin`), and `@purestack/ts-components` provides the built-in components.

Expand Down
2 changes: 1 addition & 1 deletion packages/ts-ssg/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@purestack/ts-ssg",
"version": "1.1.10",
"version": "1.1.11",
"packageManager": "yarn@4.9.2",
"module": "./dist/ts-ssg.mjs",
"types": "./dist/ts-ssg.d.mts",
Expand Down
2 changes: 1 addition & 1 deletion packages/ts-ssg/src/build/incremental/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ async function createIncrementalRuntime(
const minifyScripts = publishOptions.enabled === true
const failOnAssetError = publishOptions.enabled === true
const mdx = {
...(await resolveMdxBuildOptions(config.mdx)),
...(await resolveMdxBuildOptions(config.mdx, plugins)),
contentProcessor: resolvePluginContentProcessor(plugins),
}
themes.setOptions(config.style.theme)
Expand Down
17 changes: 14 additions & 3 deletions packages/ts-ssg/src/build/incremental/support.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ import {
} from '../../mdx/highlight'
import { createHljsHighlighter } from '../../mdx/highlightjs'
import { generatePluginPages } from '../../plugins/generated-pages'
import type { PureStackPlugin } from '../../plugins/plugin'
import {
type PureStackPlugin,
resolvePluginHighlighter,
resolvePluginHighlighterFactory,
} from '../../plugins/plugin'
import {
assertUniqueContentRoutes,
resolveRouteInfo,
Expand Down Expand Up @@ -128,8 +132,12 @@ export class ManifestContentIndex {

export async function resolveMdxBuildOptions(
mdx: Partial<SiteMdxConfig> | undefined,
plugins: readonly PureStackPlugin[] = [],
): Promise<MdxRenderOptions> {
const highlighter = await resolveHighlighter(mdx)
const highlighter = resolvePluginHighlighter(
plugins,
await resolveHighlighter(mdx, plugins),
)
return {
highlighter,
compileMdAsMdx: mdx?.compileMdAsMdx ?? true,
Expand All @@ -138,14 +146,17 @@ export async function resolveMdxBuildOptions(

async function resolveHighlighter(
mdx: Partial<SiteMdxConfig> | undefined,
plugins: readonly PureStackPlugin[],
): Promise<MdxCodeHighlighter | undefined> {
if (mdx?.disableHighlighter) return undefined
if (mdx?.highlighter === 'highlightjs') {
return createHljsHighlighter()
const create = resolvePluginHighlighterFactory(plugins, 'createHighlightJs')
return createHljsHighlighter(await create?.())
}
return await createMdxHighlighter(
DEFAULT_MDX_CODE_THEMES,
DEFAULT_MDX_CODE_LANGS,
resolvePluginHighlighterFactory(plugins, 'createShiki'),
)
}

Expand Down
2 changes: 1 addition & 1 deletion packages/ts-ssg/src/config/head.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export function getHead(config?: BasicHeadConfig) {
const head = createHead(getHeadConfig(merge(DEFAULTS, config))).push(
h('meta').attr({
name: 'generator',
content: 'PureStack v1.1.10',
content: 'PureStack v1.1.11',
}),
h('meta').attr({
name: 'color-scheme',
Expand Down
1 change: 1 addition & 0 deletions packages/ts-ssg/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export {
type MdxCodeHighlighter,
type MdxCodeLangs,
type MdxCodeThemes,
type MdxShikiFactory,
} from './mdx/highlight'
export {
buildNavigation,
Expand Down
113 changes: 113 additions & 0 deletions packages/ts-ssg/src/mdx/codeHighlighting.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { describe, expect, it } from 'vitest'
import { createHljsHighlighter } from './highlightjs'
import { compileMarkdown } from './md'
import { compileMdx } from './mdx'

describe('code highlighting', () => {
it.each([compileMarkdown, compileMdx])(
'awaits arbitrary block HTML through the existing hook',
async (compile) => {
const html =
'<figure r-pre><svg viewBox="0 0 10 10"><title>{{ literal }}</title></svg></figure><p>Caption</p>'
const result = await compile('```diagram\nsource\n```', {
highlighter: { codeToHtml: async () => html },
})
expect(result.bodyHtml).toBe(html)
},
)

it('awaits the same hook inside component markup', async () => {
const result = await compileMdx(
'<Panel>\n```diagram\nsource\n```\n</Panel>',
{
highlighter: {
codeToHtml: async (code) => `<figure r-pre>${code}</figure>`,
},
},
)
expect(result.bodyHtml).toContain(
'<Panel>\n<figure r-pre>source</figure>\n</Panel>',
)
expect(result.bodyHtml).not.toContain('<pre')
})

it('preserves callback order across separate component markup segments', async () => {
const calls: string[] = []
await compileMdx(
[
'<Panel>',
'```custom',
'first',
'```',
'</Panel>',
'',
'Between components.',
'',
'<Panel>',
'```custom',
'second',
'```',
'</Panel>',
].join('\n'),
{
highlighter: {
codeToHtml: async (code) => {
calls.push(`start:${code}`)
await Promise.resolve()
calls.push(`end:${code}`)
return `<figure>${code}</figure>`
},
},
},
)
expect(calls).toEqual([
'start:first',
'end:first',
'start:second',
'end:second',
])
})

it.each([compileMarkdown, compileMdx])(
'honors empty custom HTML without restoring a code block',
async (compile) => {
const source =
compile === compileMdx
? '<Panel>\n```custom\nsource\n```\n</Panel>'
: '```custom\nsource\n```'
const result = await compile(source, {
highlighter: { codeToHtml: () => '' },
})
expect(result.bodyHtml).not.toContain('source')
expect(result.bodyHtml).not.toContain('<pre')
},
)

it('preserves built-in block and inline highlighting with an asynchronous wrapper', async () => {
const fallback = createHljsHighlighter()
const source =
'```ts\nconst answer = 42\n```\n\n`value`\n\n<Panel>\n```ts\nconst nested = 1\n```\n`nested`\n</Panel>'
const original = await compileMdx(source, { highlighter: fallback })
const result = await compileMdx(source, {
highlighter: {
codeToHtml: async (code, lang) => fallback.codeToHtml(code, lang),
},
})
expect(result).toEqual(original)
})

it.each([compileMarkdown, compileMdx])(
'propagates asynchronous highlighter errors',
async (compile) => {
await expect(
compile('```diagram\nbroken\n```', {
highlighter: {
codeToHtml: async () => {
throw new Error('invalid diagram')
},
},
}),
).rejects.toThrow('invalid diagram')
},
)
})
Loading
Loading