diff options
Diffstat (limited to 'packages')
17 files changed, 204 insertions, 49 deletions
diff --git a/packages/astro/package.json b/packages/astro/package.json index 28b3b3f56..ebec27310 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -52,7 +52,6 @@ "@babel/generator": "^7.13.9", "@babel/parser": "^7.13.15", "@babel/traverse": "^7.13.15", - "@silvenon/remark-smartypants": "^1.0.0", "@snowpack/plugin-postcss": "^1.4.3", "@snowpack/plugin-sass": "^1.4.0", "acorn": "^7.4.0", @@ -67,14 +66,12 @@ "fast-xml-parser": "^3.19.0", "fdir": "^5.0.0", "find-up": "^5.0.0", - "gray-matter": "^4.0.2", + "unified": "^9.2.1", "gzip-size": "^6.0.0", "hast-to-hyperscript": "~9.0.0", "kleur": "^4.1.4", "locate-character": "^2.0.5", "magic-string": "^0.25.3", - "mdast-util-mdx": "^0.1.1", - "micromark-extension-mdxjs": "^0.3.0", "mime": "^2.5.2", "moize": "^6.0.1", "node-fetch": "^2.6.1", @@ -82,10 +79,6 @@ "postcss": "^8.2.15", "postcss-icss-keyframes": "^0.2.1", "prismjs": "^1.23.0", - "remark-footnotes": "^3.0.0", - "remark-gfm": "^1.0.0", - "remark-parse": "^9.0.0", - "remark-rehype": "^8.1.0", "resolve": "^1.20.0", "rollup": "^2.43.1", "rollup-plugin-terser": "^7.0.2", diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 1fbdd6282..bfb338cdc 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1,4 +1,5 @@ import type { ImportSpecifier, ImportDefaultSpecifier, ImportNamespaceSpecifier } from '@babel/types'; +import type { AstroMarkdownOptions } from '@astrojs/markdown-support' export interface AstroConfigRaw { dist: string; @@ -9,12 +10,7 @@ export interface AstroConfigRaw { jsx?: string; } -export interface AstroMarkdownOptions { - /** Enable or disable footnotes syntax extension */ - footnotes: boolean; - /** Enable or disable GitHub-flavored Markdown syntax extension */ - gfm: boolean; -} +export { AstroMarkdownOptions } export interface AstroConfig { dist: string; projectRoot: URL; diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts index 0b9780e16..b34077269 100644 --- a/packages/astro/src/compiler/codegen/index.ts +++ b/packages/astro/src/compiler/codegen/index.ts @@ -543,7 +543,6 @@ async function compileHtml(enterNode: TemplateNode, state: CodegenState, compile const { $scope: scopedClassName } = state.markers.insideMarkdown as Record<'$scope', any>; let { content: rendered } = await renderMarkdown(dedent(md), { ...(markdownOptions as AstroMarkdownOptions), - mode: 'astro-md', $: { scopedClassName: scopedClassName && scopedClassName.slice(1, -1) }, }); diff --git a/packages/astro/src/compiler/index.ts b/packages/astro/src/compiler/index.ts index 0f12cc7f0..f612e6165 100644 --- a/packages/astro/src/compiler/index.ts +++ b/packages/astro/src/compiler/index.ts @@ -3,7 +3,7 @@ import type { CompileResult, TransformResult } from '../@types/astro'; import type { CompileOptions } from '../@types/compiler.js'; import path from 'path'; -import { renderMarkdownWithFrontmatter } from '@astrojs/markdown-support'; +import { MarkdownRenderingOptions, renderMarkdownWithFrontmatter } from '@astrojs/markdown-support'; import { parse } from '@astrojs/parser'; import { transform } from './transform/index.js'; @@ -42,12 +42,12 @@ export async function convertAstroToJsx(template: string, opts: ConvertAstroOpti /** * .md -> .astro source */ -export async function convertMdToAstroSource(contents: string, { filename }: { filename: string }): Promise<string> { +export async function convertMdToAstroSource(contents: string, { filename }: { filename: string }, opts?: MarkdownRenderingOptions): Promise<string> { let { content, frontmatter: { layout, ...frontmatter }, ...data - } = await renderMarkdownWithFrontmatter(contents); + } = await renderMarkdownWithFrontmatter(contents, opts); if (frontmatter['astro'] !== undefined) { throw new Error(`"astro" is a reserved word but was used as a frontmatter value!\n\tat ${filename}`); @@ -75,7 +75,7 @@ async function convertMdToJsx( contents: string, { compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string } ): Promise<TransformResult> { - const raw = await convertMdToAstroSource(contents, { filename }); + const raw = await convertMdToAstroSource(contents, { filename }, compileOptions.astroConfig.markdownOptions); const convertOptions = { compileOptions, filename, fileID }; return await convertAstroToJsx(raw, convertOptions); } diff --git a/packages/astro/test/astro-markdown-plugins.test.js b/packages/astro/test/astro-markdown-plugins.test.js new file mode 100644 index 000000000..44744e5d5 --- /dev/null +++ b/packages/astro/test/astro-markdown-plugins.test.js @@ -0,0 +1,29 @@ +import { suite } from 'uvu'; +import * as assert from 'uvu/assert'; +import { doc } from './test-utils.js'; +import { setup, setupBuild } from './helpers.js'; + +const MarkdownPlugin = suite('Astro Markdown plugin tests'); + +setup(MarkdownPlugin, './fixtures/astro-markdown-plugins'); +setupBuild(MarkdownPlugin, './fixtures/astro-markdown-plugins'); + +MarkdownPlugin('Can render markdown with plugins', async ({ runtime }) => { + const result = await runtime.load('/'); + if (result.error) throw new Error(result.error); + + const $ = doc(result.contents); + assert.equal($('.toc').length, 1, 'Added a TOC'); + assert.ok($('#hello-world').hasClass('title'), 'Added .title to h1'); +}); + +MarkdownPlugin('Can render Astro <Markdown> with plugins', async ({ runtime }) => { + const result = await runtime.load('/astro'); + if (result.error) throw new Error(result.error); + + const $ = doc(result.contents); + assert.equal($('.toc').length, 1, 'Added a TOC'); + assert.ok($('#hello-world').hasClass('title'), 'Added .title to h1'); +}) + +MarkdownPlugin.run(); diff --git a/packages/astro/test/fixtures/astro-markdown-plugins/astro.config.mjs b/packages/astro/test/fixtures/astro-markdown-plugins/astro.config.mjs new file mode 100644 index 000000000..c236c9215 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-plugins/astro.config.mjs @@ -0,0 +1,19 @@ +export default { + renderers: [ + '@astrojs/renderer-preact' + ], + markdownOptions: { + remarkPlugins: [ + 'remark-code-titles', + 'remark-slug', + ['remark-autolink-headings', { behavior: 'prepend' }], + ], + rehypePlugins: [ + ['rehype-toc', { headings: ["h2", "h3"] }], + ['rehype-add-classes', { 'h1,h2,h3': 'title', }], + ] + }, + buildOptions: { + sitemap: false, + }, +}; diff --git a/packages/astro/test/fixtures/astro-markdown-plugins/snowpack.config.json b/packages/astro/test/fixtures/astro-markdown-plugins/snowpack.config.json new file mode 100644 index 000000000..8f034781d --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-plugins/snowpack.config.json @@ -0,0 +1,3 @@ +{ + "workspaceRoot": "../../../../../" +} diff --git a/packages/astro/test/fixtures/astro-markdown-plugins/src/layouts/content.astro b/packages/astro/test/fixtures/astro-markdown-plugins/src/layouts/content.astro new file mode 100644 index 000000000..925a243a9 --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-plugins/src/layouts/content.astro @@ -0,0 +1,10 @@ +<html> + <head> + <!-- Head Stuff --> + </head> + <body> + <div class="container"> + <slot></slot> + </div> + </body> +</html> diff --git a/packages/astro/test/fixtures/astro-markdown-plugins/src/pages/astro.astro b/packages/astro/test/fixtures/astro-markdown-plugins/src/pages/astro.astro new file mode 100644 index 000000000..a05a7c3ff --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-plugins/src/pages/astro.astro @@ -0,0 +1,10 @@ +--- +import { Markdown } from 'astro/components'; +import Layout from '../layouts/content.astro'; +--- + +<Layout> + <Markdown> + # Hello world + </Markdown> +</Layout> diff --git a/packages/astro/test/fixtures/astro-markdown-plugins/src/pages/index.md b/packages/astro/test/fixtures/astro-markdown-plugins/src/pages/index.md new file mode 100644 index 000000000..832ccf0bc --- /dev/null +++ b/packages/astro/test/fixtures/astro-markdown-plugins/src/pages/index.md @@ -0,0 +1,5 @@ +--- +layout: ../layouts/content.astro +--- + +# Hello world diff --git a/packages/markdown-support/package.json b/packages/markdown-support/package.json index 8be2aa4a4..550fef7a7 100644 --- a/packages/markdown-support/package.json +++ b/packages/markdown-support/package.json @@ -2,7 +2,7 @@ "name": "@astrojs/markdown-support", "version": "0.1.2", "main": "./dist/index.js", - "type": "commonjs", + "type": "module", "repository": { "type": "git", "url": "https://github.com/snowpackjs/astro.git", @@ -16,6 +16,18 @@ "build": "astro-scripts build --format cjs \"src/**/*.ts\" && tsc -p tsconfig.json", "dev": "astro-scripts dev \"src/**/*.ts\"" }, + "dependencies": { + "@silvenon/remark-smartypants": "^1.0.0", + "gray-matter": "^4.0.2", + "mdast-util-mdx-expression": "^1.0.0", + "micromark-extension-mdx-expression": "^1.0.0", + "remark-footnotes": "^3.0.0", + "remark-gfm": "^1.0.0", + "remark-parse": "^9.0.0", + "remark-rehype": "^8.1.0", + "unified": "^9.2.1", + "unist-util-map": "^3.0.0" + }, "devDependencies": { "@types/github-slugger": "^1.3.0", "github-slugger": "^1.3.0", diff --git a/packages/markdown-support/src/index.ts b/packages/markdown-support/src/index.ts index 08f171c3c..f311efa7c 100644 --- a/packages/markdown-support/src/index.ts +++ b/packages/markdown-support/src/index.ts @@ -1,62 +1,67 @@ -import type { AstroMarkdownOptions } from './types'; +import type { AstroMarkdownOptions, MarkdownRenderingOptions } from './types'; import createCollectHeaders from './rehype-collect-headers.js'; import scopedStyles from './remark-scoped-styles.js'; -import { remarkCodeBlock, rehypeCodeBlock } from './codeblock.js'; +import remarkExpressions from './remark-expressions.js'; +import rehypeExpressions from './rehype-expressions.js'; +import { rehypeCodeBlock } from './codeblock.js'; +import { loadPlugins } from './load-plugins.js'; import raw from 'rehype-raw'; import unified from 'unified'; import markdown from 'remark-parse'; import markdownToHtml from 'remark-rehype'; -// import smartypants from '@silvenon/remark-smartypants'; import rehypeStringify from 'rehype-stringify'; -export interface MarkdownRenderingOptions extends Partial<AstroMarkdownOptions> { - $?: { - scopedClassName: string | null; - }; - mode: 'md' | 'astro-md'; -} +export { AstroMarkdownOptions, MarkdownRenderingOptions }; /** Internal utility for rendering a full markdown file and extracting Frontmatter data */ export async function renderMarkdownWithFrontmatter(contents: string, opts?: MarkdownRenderingOptions | null) { // Dynamic import to ensure that "gray-matter" isn't built by Snowpack const { default: matter } = await import('gray-matter'); const { data: frontmatter, content } = matter(contents); - const value = await renderMarkdown(content, { ...opts, mode: 'md' }); + const value = await renderMarkdown(content, opts); return { ...value, frontmatter }; } /** Shared utility for rendering markdown */ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOptions | null) { - const { $: { scopedClassName = null } = {}, mode = 'astro-md', footnotes: useFootnotes = true, gfm: useGfm = true } = opts ?? {}; + const { $: { scopedClassName = null } = {}, footnotes: useFootnotes = true, gfm: useGfm = true, remarkPlugins = [], rehypePlugins = [] } = opts ?? {}; const { headers, rehypeCollectHeaders } = createCollectHeaders(); + let parser = unified().use(markdown).use([remarkExpressions, { addResult: true }]); - let parser = unified().use(markdown).use(remarkCodeBlock()); + if (remarkPlugins.length === 0) { + if (useGfm) { + remarkPlugins.push('remark-gfm'); + } - if (scopedClassName) { - parser = parser.use(scopedStyles(scopedClassName)); - } + if (useFootnotes) { + remarkPlugins.push('remark-footnotes'); + } - if (useGfm) { - const { default: gfm } = await import('remark-gfm'); - parser = parser.use(gfm); + remarkPlugins.push('@silvenon/remark-smartypants'); } + const loadedRemarkPlugins = await Promise.all(loadPlugins(remarkPlugins)); + const loadedRehypePlugins = await Promise.all(loadPlugins(rehypePlugins)); - if (useFootnotes) { - const { default: footnotes } = await import('remark-footnotes'); - parser = parser.use(footnotes); + loadedRemarkPlugins.forEach(([plugin, opts]) => { + parser.use(plugin, opts); + }); + + if (scopedClassName) { + parser.use(scopedStyles(scopedClassName)); } + parser.use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw', 'mdxTextExpression'] }); + parser.use(rehypeExpressions); + + loadedRehypePlugins.forEach(([plugin, opts]) => { + parser.use(plugin, opts); + }); + let result: string; try { - const vfile = await parser - .use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw'] }) - .use(raw) - .use(rehypeCollectHeaders) - .use(rehypeCodeBlock()) - .use(rehypeStringify) - .process(content); + const vfile = await parser.use(raw).use(rehypeCollectHeaders).use(rehypeCodeBlock()).use(rehypeStringify, { entities: { useNamedReferences: true }}).process(content); result = vfile.contents.toString(); } catch (err) { throw err; diff --git a/packages/markdown-support/src/load-plugins.ts b/packages/markdown-support/src/load-plugins.ts new file mode 100644 index 000000000..52bc287f8 --- /dev/null +++ b/packages/markdown-support/src/load-plugins.ts @@ -0,0 +1,27 @@ +import unified from "unified"; +import type { Plugin, UnifiedPluginImport } from "./types"; + +async function importPlugin(p: string | UnifiedPluginImport): UnifiedPluginImport { + if (typeof p === 'string') { + return await import(p); + } + + return await p; +} + +export function loadPlugins(items: Plugin[]): Promise<[unified.Plugin] | [unified.Plugin, unified.Settings]>[] { + return items.map((p) => { + return new Promise((resolve, reject) => { + if (Array.isArray(p)) { + const [plugin, opts] = p; + return importPlugin(plugin) + .then((m) => resolve([m.default, opts])) + .catch((e) => reject(e)); + } + + return importPlugin(p) + .then((m) => resolve([m.default])) + .catch((e) => reject(e)); + }); + }); +} diff --git a/packages/markdown-support/src/rehype-collect-headers.ts b/packages/markdown-support/src/rehype-collect-headers.ts index edfcd29bc..de9b78692 100644 --- a/packages/markdown-support/src/rehype-collect-headers.ts +++ b/packages/markdown-support/src/rehype-collect-headers.ts @@ -14,11 +14,13 @@ export default function createCollectHeaders() { depth = Number.parseInt(depth); let text = ''; + visit(node, 'text', (child) => { text += child.value; }); - let slug = slugger.slug(text); + let slug = node.properties.id || slugger.slug(text); + node.properties = node.properties || {}; node.properties.id = slug; headers.push({ depth, slug, text }); diff --git a/packages/markdown-support/src/rehype-expressions.ts b/packages/markdown-support/src/rehype-expressions.ts new file mode 100644 index 000000000..2762f54fc --- /dev/null +++ b/packages/markdown-support/src/rehype-expressions.ts @@ -0,0 +1,12 @@ +import { map } from 'unist-util-map' + +export default function rehypeExpressions(): any { + return function(node: any): any { + return map(node, (child) => { + if (child.type === 'mdxTextExpression') { + return { type: 'text', value: `{${child.value}}` } + } + return child; + }) + } +} diff --git a/packages/markdown-support/src/remark-expressions.ts b/packages/markdown-support/src/remark-expressions.ts new file mode 100644 index 000000000..1cdb37894 --- /dev/null +++ b/packages/markdown-support/src/remark-expressions.ts @@ -0,0 +1,19 @@ +import {mdxExpression} from 'micromark-extension-mdx-expression' +import {mdxExpressionFromMarkdown, mdxExpressionToMarkdown} from 'mdast-util-mdx-expression' + +function remarkExpressions(this: any, options: any) { + let settings = options || {} + let data = this.data() + + add('micromarkExtensions', mdxExpression({})) + add('fromMarkdownExtensions', mdxExpressionFromMarkdown) + add('toMarkdownExtensions', mdxExpressionToMarkdown) + + function add(field: any, value: any) { + /* istanbul ignore if - other extensions. */ + if (data[field]) data[field].push(value) + else data[field] = [value] + } +} + +export default remarkExpressions; diff --git a/packages/markdown-support/src/types.ts b/packages/markdown-support/src/types.ts index b69f7fc28..6df601ae4 100644 --- a/packages/markdown-support/src/types.ts +++ b/packages/markdown-support/src/types.ts @@ -1,6 +1,20 @@ +import unified from 'unified'; + +export type UnifiedPluginImport = Promise<{ default: unified.Plugin }>; +export type Plugin = string | [string, unified.Settings] | UnifiedPluginImport | [UnifiedPluginImport, unified.Settings]; + export interface AstroMarkdownOptions { /** Enable or disable footnotes syntax extension */ footnotes: boolean; /** Enable or disable GitHub-flavored Markdown syntax extension */ gfm: boolean; + remarkPlugins: Plugin[]; + rehypePlugins: Plugin[]; +} + +export interface MarkdownRenderingOptions extends Partial<AstroMarkdownOptions> { + /** @internal */ + $?: { + scopedClassName: string | null; + }; } |