Skip to content

Commit

Permalink
Markdoc - improve syntax highlighting support (#7209)
Browse files Browse the repository at this point in the history
* feat: prism and shiki support, with better exports!

* chore: update tests

* chore: fix lock

* chore: add prism test

* chore: remove `async` from prism

* docs: update syntax highlight readme

* chore: changeset

* edit: remove `await` from prism docs

* chore: update old changest with new shiki instructions

* fix: add trailing newline on ts-expect-error

* refactor: resolve promises internally

* docs: remove `await` from shiki examples
  • Loading branch information
bholmesdev committed May 25, 2023
1 parent 223e013 commit 16b8364
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 114 deletions.
8 changes: 5 additions & 3 deletions .changeset/eleven-tables-speak.md
Expand Up @@ -2,14 +2,16 @@
'@astrojs/markdoc': patch
---

Add support for syntax highlighting with Shiki. Install `shiki` in your project with `npm i shiki`, and apply to your Markdoc config using the `extends` option:
Add support for syntax highlighting with Shiki. Apply to your Markdoc config using the `extends` property:

```js
// markdoc.config.mjs
import { defineMarkdocConfig, shiki } from '@astrojs/markdoc/config';
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
import shiki from '@astrojs/markdoc/shiki';

export default defineMarkdocConfig({
extends: [
await shiki({ /** Shiki config options */ }),
shiki({ /** Shiki config options */ }),
],
})
```
Expand Down
17 changes: 17 additions & 0 deletions .changeset/popular-berries-travel.md
@@ -0,0 +1,17 @@
---
'@astrojs/markdoc': patch
---

Add a built-in extension for syntax highlighting with Prism. Apply to your Markdoc config using the `extends` property:

```js
// markdoc.config.mjs
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
import prism from '@astrojs/markdoc/prism';

export default defineMarkdocConfig({
extends: [prism()],
})
```

Learn more in the [`@astrojs/markdoc` README.](https://docs.astro.build/en/guides/integrations-guide/markdoc/#syntax-highlighting)
31 changes: 22 additions & 9 deletions packages/integrations/markdoc/README.md
Expand Up @@ -205,23 +205,20 @@ export default defineMarkdocConfig({

### Syntax highlighting

`@astrojs/markdoc` provides a [Shiki](https://github.com/shikijs/shiki) extension to highlight your code blocks.
`@astrojs/markdoc` provides [Shiki](https://github.com/shikijs/shiki) and [Prism](https://github.com/PrismJS) extensions to highlight your code blocks.

To use this extension, you must separately install `shiki` as a dependency:
#### Shiki

```bash
npm i shiki
```

Then, apply the `shiki()` extension to your Markdoc config using the `extends` property. You can optionally pass a shiki configuration object:
Apply the `shiki()` extension to your Markdoc config using the `extends` property. You can optionally pass a shiki configuration object:

```js
// markdoc.config.mjs
import { defineMarkdocConfig, shiki } from '@astrojs/markdoc/config';
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
import shiki from '@astrojs/markdoc/shiki';

export default defineMarkdocConfig({
extends: [
await shiki({
shiki({
// Choose from Shiki's built-in themes (or add your own)
// Default: 'github-dark'
// https://github.com/shikijs/shiki/blob/main/docs/themes.md
Expand All @@ -238,6 +235,22 @@ export default defineMarkdocConfig({
})
```

#### Prism

Apply the `prism()` extension to your Markdoc config using the `extends` property.

```js
// markdoc.config.mjs
import { defineMarkdocConfig } from '@astrojs/markdoc/config';
import prism from '@astrojs/markdoc/prism';

export default defineMarkdocConfig({
extends: [prism()],
})
```

📚 To learn about configuring Prism stylesheets, [see our syntax highlighting guide.](https://docs.astro.build/en/guides/markdown-content/#prism-configuration)

### Access frontmatter and content collection information from your templates

You can access content collection information from your Markdoc templates using the `$entry` variable. This includes the entry `slug`, `collection` name, and frontmatter `data` parsed by your content collection schema (if any). This example renders the `title` frontmatter property as a heading:
Expand Down
15 changes: 6 additions & 9 deletions packages/integrations/markdoc/package.json
Expand Up @@ -19,6 +19,8 @@
"bugs": "https://github.com/withastro/astro/issues",
"homepage": "https://docs.astro.build/en/guides/integrations-guide/markdoc/",
"exports": {
"./prism": "./dist/extensions/prism.js",
"./shiki": "./dist/extensions/shiki.js",
".": "./dist/index.js",
"./components": "./components/index.ts",
"./runtime": "./dist/runtime.js",
Expand All @@ -39,21 +41,17 @@
"test:match": "mocha --timeout 20000 -g"
},
"dependencies": {
"@markdoc/markdoc": "^0.2.2",
"shiki": "^0.14.1",
"@astrojs/prism": "^2.1.2",
"@markdoc/markdoc": "^0.3.0",
"esbuild": "^0.17.12",
"github-slugger": "^2.0.0",
"gray-matter": "^4.0.3",
"kleur": "^4.1.5",
"zod": "^3.17.3"
},
"peerDependencies": {
"astro": "workspace:^2.5.5",
"shiki": "^0.14.1"
},
"peerDependenciesMeta": {
"shiki": {
"optional": true
}
"astro": "workspace:^2.5.5"
},
"devDependencies": {
"@astrojs/markdown-remark": "^2.2.1",
Expand All @@ -67,7 +65,6 @@
"linkedom": "^0.14.12",
"mocha": "^9.2.2",
"rollup": "^3.20.1",
"shiki": "^0.14.1",
"vite": "^4.3.1"
},
"engines": {
Expand Down
1 change: 0 additions & 1 deletion packages/integrations/markdoc/src/config.ts
Expand Up @@ -12,7 +12,6 @@ export type ResolvedAstroMarkdocConfig = Omit<AstroMarkdocConfig, 'extends'>;

export const Markdoc = _Markdoc;
export const nodes = { ...Markdoc.nodes, heading };
export { shiki } from './extensions/shiki.js';

export function defineMarkdocConfig(config: AstroMarkdocConfig): AstroMarkdocConfig {
return config;
Expand Down
24 changes: 24 additions & 0 deletions packages/integrations/markdoc/src/extensions/prism.ts
@@ -0,0 +1,24 @@
// leave space, so organize imports doesn't mess up comments
// @ts-expect-error Cannot find module 'astro/runtime/server/index.js' or its corresponding type declarations.
import { unescapeHTML } from 'astro/runtime/server/index.js';

import { runHighlighterWithAstro } from '@astrojs/prism/dist/highlighter';
import { Markdoc, type AstroMarkdocConfig } from '../config.js';

export default function prism(): AstroMarkdocConfig {
return {
nodes: {
fence: {
attributes: Markdoc.nodes.fence.attributes!,
transform({ attributes: { language, content } }) {
const { html, classLanguage } = runHighlighterWithAstro(language, content);

// Use `unescapeHTML` to return `HTMLString` for Astro renderer to inline as HTML
return unescapeHTML(
`<pre class="${classLanguage}"><code class="${classLanguage}">${html}</code></pre>`
);
},
},
},
};
}
14 changes: 3 additions & 11 deletions packages/integrations/markdoc/src/extensions/shiki.ts
Expand Up @@ -2,11 +2,11 @@
// @ts-expect-error Cannot find module 'astro/runtime/server/index.js' or its corresponding type declarations.
import { unescapeHTML } from 'astro/runtime/server/index.js';

import Markdoc from '@markdoc/markdoc';
import type { ShikiConfig } from 'astro';
import type * as shikiTypes from 'shiki';
import type { AstroMarkdocConfig } from '../config.js';
import { MarkdocError } from '../utils.js';
import Markdoc from '@markdoc/markdoc';
import { getHighlighter } from 'shiki';

// Map of old theme names to new names to preserve compatibility when we upgrade shiki
const compatThemes: Record<string, string> = {
Expand Down Expand Up @@ -51,19 +51,11 @@ const INLINE_STYLE_SELECTOR = /style="(.*?)"/;
*/
const highlighterCache = new Map<string, shikiTypes.Highlighter>();

export async function shiki({
export default async function shiki({
langs = [],
theme = 'github-dark',
wrap = false,
}: ShikiConfig = {}): Promise<AstroMarkdocConfig> {
let getHighlighter: (options: shikiTypes.HighlighterOptions) => Promise<shikiTypes.Highlighter>;
try {
getHighlighter = (await import('shiki')).getHighlighter;
} catch {
throw new MarkdocError({
message: 'Shiki is not installed. Run `npm install shiki` to use the `shiki` extension.',
});
}
theme = normalizeTheme(theme);

const cacheID: string = typeof theme === 'string' ? theme : theme.name;
Expand Down
26 changes: 17 additions & 9 deletions packages/integrations/markdoc/src/index.ts
Expand Up @@ -32,7 +32,19 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration
name: '@astrojs/markdoc',
hooks: {
'astro:config:setup': async (params) => {
const { config: astroConfig, addContentEntryType } = params as SetupHookParams;
const {
config: astroConfig,
updateConfig,
addContentEntryType,
} = params as SetupHookParams;

updateConfig({
vite: {
ssr: {
external: ['@astrojs/markdoc/prism', '@astrojs/markdoc/shiki'],
},
},
});

markdocConfigResult = await loadMarkdocConfig(astroConfig);
const userMarkdocConfig = markdocConfigResult?.config ?? {};
Expand All @@ -52,11 +64,7 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration
async getRenderModule({ entry, viteId }) {
const ast = Markdoc.parse(entry.body);
const pluginContext = this;
const markdocConfig = setupConfig(
userMarkdocConfig,
entry,
markdocConfigResult?.fileUrl.pathname
);
const markdocConfig = await setupConfig(userMarkdocConfig, entry);

const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => {
return (
Expand Down Expand Up @@ -94,7 +102,7 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration

const res = `import { jsx as h } from 'astro/jsx-runtime';
import { Renderer } from '@astrojs/markdoc/components';
import { collectHeadings, setupConfig, Markdoc } from '@astrojs/markdoc/runtime';
import { collectHeadings, setupConfig, setupConfigSync, Markdoc } from '@astrojs/markdoc/runtime';
import * as entry from ${JSON.stringify(viteId + '?astroContentCollectionEntry')};
${
markdocConfigResult
Expand All @@ -118,13 +126,13 @@ export function getHeadings() {
''
}
const headingConfig = userConfig.nodes?.heading;
const config = setupConfig(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry);
const config = setupConfigSync(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry);
const ast = Markdoc.Ast.fromJSON(stringifiedAst);
const content = Markdoc.transform(ast, config);
return collectHeadings(Array.isArray(content) ? content : content.children);
}
export async function Content (props) {
const config = setupConfig({
const config = await setupConfig({
...userConfig,
variables: { ...userConfig.variables, ...props },
}, entry);
Expand Down
30 changes: 18 additions & 12 deletions packages/integrations/markdoc/src/runtime.ts
Expand Up @@ -13,26 +13,19 @@ export { default as Markdoc } from '@markdoc/markdoc';
* Called on each file's individual transform.
* TODO: virtual module to merge configs per-build instead of per-file?
*/
export function setupConfig(
export async function setupConfig(
userConfig: AstroMarkdocConfig,
entry: ContentEntryModule,
markdocConfigPath?: string
): Omit<AstroMarkdocConfig, 'extends'> {
entry: ContentEntryModule
): Promise<Omit<AstroMarkdocConfig, 'extends'>> {
let defaultConfig: AstroMarkdocConfig = {
...setupHeadingConfig(),
variables: { entry },
};

if (userConfig.extends) {
for (const extension of userConfig.extends) {
for (let extension of userConfig.extends) {
if (extension instanceof Promise) {
throw new MarkdocError({
message: 'An extension passed to `extends` in your markdoc config returns a Promise.',
hint: 'Call `await` for async extensions. Example: `extends: [await myExtension()]`',
location: {
file: markdocConfigPath,
},
});
extension = await extension;
}

defaultConfig = mergeConfig(defaultConfig, extension);
Expand All @@ -42,6 +35,19 @@ export function setupConfig(
return mergeConfig(defaultConfig, userConfig);
}

/** Used for synchronous `getHeadings()` function */
export function setupConfigSync(
userConfig: AstroMarkdocConfig,
entry: ContentEntryModule
): Omit<AstroMarkdocConfig, 'extends'> {
let defaultConfig: AstroMarkdocConfig = {
...setupHeadingConfig(),
variables: { entry },
};

return mergeConfig(defaultConfig, userConfig);
}

/** Merge function from `@markdoc/markdoc` internals */
function mergeConfig(configA: AstroMarkdocConfig, configB: AstroMarkdocConfig): AstroMarkdocConfig {
return {
Expand Down

0 comments on commit 16b8364

Please sign in to comment.