diff options
author | 2021-05-21 15:52:20 -0500 | |
---|---|---|
committer | 2021-05-21 15:52:20 -0500 | |
commit | 9cdada0bcc5fe3fad6f645ffda5f7e5934c738b5 (patch) | |
tree | 03dba70159fe324e9038460b708e515f3f6e3261 | |
parent | 19e20f2c54eaa8f0c2ebec54b071ffc4abd524bd (diff) | |
download | astro-9cdada0bcc5fe3fad6f645ffda5f7e5934c738b5.tar.gz astro-9cdada0bcc5fe3fad6f645ffda5f7e5934c738b5.tar.zst astro-9cdada0bcc5fe3fad6f645ffda5f7e5934c738b5.zip |
Markdown issue cleanup (#224)
* fix: markdown issues
* chore: add changeset
* chore: add missing dep
* perf: parallelize compileHtml for children
-rw-r--r-- | .changeset/spotty-ways-leave.md | 6 | ||||
-rw-r--r-- | packages/astro-parser/src/parse/state/text.ts | 10 | ||||
-rw-r--r-- | packages/astro/package.json | 1 | ||||
-rw-r--r-- | packages/astro/src/@types/astro.ts | 8 | ||||
-rw-r--r-- | packages/astro/src/compiler/codegen/index.ts | 330 | ||||
-rw-r--r-- | packages/astro/src/compiler/index.ts | 7 | ||||
-rw-r--r-- | packages/astro/src/compiler/markdown/codeblock.ts | 41 | ||||
-rw-r--r-- | packages/astro/src/compiler/markdown/micromark.d.ts | 3 | ||||
-rw-r--r-- | packages/astro/src/compiler/markdown/remark-mdx-lite.ts | 39 | ||||
-rw-r--r-- | packages/astro/src/compiler/markdown/remark-scoped-styles.ts | 4 | ||||
-rw-r--r-- | packages/astro/src/compiler/transform/prism.ts | 19 | ||||
-rw-r--r-- | packages/astro/src/compiler/utils.ts | 23 | ||||
-rw-r--r-- | packages/astro/src/config.ts | 1 | ||||
-rw-r--r-- | packages/astro/src/frontend/markdown.ts | 26 | ||||
-rw-r--r-- | packages/astro/src/runtime.ts | 7 | ||||
-rw-r--r-- | yarn.lock | 57 |
16 files changed, 378 insertions, 204 deletions
diff --git a/.changeset/spotty-ways-leave.md b/.changeset/spotty-ways-leave.md new file mode 100644 index 000000000..17e16b696 --- /dev/null +++ b/.changeset/spotty-ways-leave.md @@ -0,0 +1,6 @@ +--- +'astro': patch +'astro-parser': patch +--- + +Fixes a few edge case bugs with Astro's handling of Markdown content diff --git a/packages/astro-parser/src/parse/state/text.ts b/packages/astro-parser/src/parse/state/text.ts index eac810a0a..bde2ec5e4 100644 --- a/packages/astro-parser/src/parse/state/text.ts +++ b/packages/astro-parser/src/parse/state/text.ts @@ -8,7 +8,15 @@ export default function text(parser: Parser) { let data = ''; - while (parser.index < parser.template.length && !parser.match('---') && !parser.match('<') && !parser.match('{') && !parser.match('`')) { + const shouldContinue = () => { + // Special case 'code' content to avoid tripping up on user code + if (parser.current().name === 'code') { + return !parser.match('<') && !parser.match('{'); + } + return !parser.match('---') && !parser.match('<') && !parser.match('{') && !parser.match('`'); + } + + while (parser.index < parser.template.length && shouldContinue()) { data += parser.template[parser.index++]; } diff --git a/packages/astro/package.json b/packages/astro/package.json index 29a4fff4d..6b2dfb1d8 100644 --- a/packages/astro/package.json +++ b/packages/astro/package.json @@ -61,6 +61,7 @@ "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", diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 5df93635f..2f9983f53 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -8,12 +8,20 @@ export interface AstroConfigRaw { export type ValidExtensionPlugins = 'astro' | 'react' | 'preact' | 'svelte' | 'vue'; +export interface AstroMarkdownOptions { + /** Enable or disable footnotes syntax extension */ + footnotes: boolean; + /** Enable or disable GitHub-flavored Markdown syntax extension */ + gfm: boolean; +} export interface AstroConfig { dist: string; projectRoot: URL; astroRoot: URL; public: URL; extensions?: Record<string, ValidExtensionPlugins>; + /** Options for rendering markdown content */ + markdownOptions?: Partial<AstroMarkdownOptions>; /** Options specific to `astro build` */ buildOptions: { /** Your public domain, e.g.: https://my-site.dev/. Used to generate sitemaps and canonical URLs. */ diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts index 64d7c1822..fb6bac9be 100644 --- a/packages/astro/src/compiler/codegen/index.ts +++ b/packages/astro/src/compiler/codegen/index.ts @@ -1,12 +1,13 @@ import type { Ast, Script, Style, TemplateNode } from 'astro-parser'; import type { CompileOptions } from '../../@types/compiler'; -import type { AstroConfig, TransformResult, ValidExtensionPlugins } from '../../@types/astro'; +import type { AstroConfig, AstroMarkdownOptions, TransformResult, ValidExtensionPlugins } from '../../@types/astro'; import 'source-map-support/register.js'; import eslexer from 'es-module-lexer'; import esbuild from 'esbuild'; import path from 'path'; -import { walk } from 'estree-walker'; +import { parse } from 'astro-parser'; +import { walk, asyncWalk } from 'estree-walker'; import _babelGenerator from '@babel/generator'; import babelParser from '@babel/parser'; import { codeFrameColumns } from '@babel/code-frame'; @@ -16,6 +17,7 @@ import { error, warn } from '../../logger.js'; import { fetchContent } from './content.js'; import { isFetchContent } from './utils.js'; import { yellow } from 'kleur/colors'; +import { MarkdownRenderingOptions, renderMarkdown } from '../utils'; const traverse: typeof babelTraverse.default = (babelTraverse.default as any).default; @@ -306,7 +308,7 @@ interface CodegenState { components: Components; css: string[]; markers: { - insideMarkdown: boolean | string; + insideMarkdown: boolean | Record<string, any>; }; importExportStatements: Set<string>; dynamicImports: DynamicImportMap; @@ -538,160 +540,210 @@ function compileCss(style: Style, state: CodegenState) { }); } +/** dedent markdown */ +function dedent(str: string) { + let arr = str.match(/^[ \t]*(?=\S)/gm); + let first = !!arr && arr.find((x) => x.length > 0)?.length; + return !arr || !first ? str : str.replace(new RegExp(`^[ \\t]{0,${first}}`, 'gm'), ''); +} + + /** Compile page markup */ -function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions) { - const { components, css, importExportStatements, dynamicImports, filename } = state; - const { astroConfig } = compileOptions; +async function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOptions: CompileOptions): Promise<string> { + return new Promise((resolve) => { + const { components, css, importExportStatements, dynamicImports, filename } = state; + const { astroConfig } = compileOptions; + + let paren = -1; + let buffers = { + out: '', + markdown: '', + }; + let curr: keyof typeof buffers = 'out'; + + /** renders markdown stored in `buffers.markdown` to JSX and pushes that to `buffers.out` */ + async function pushMarkdownToBuffer() { + const md = buffers.markdown; + const { markdownOptions = {} } = astroConfig; + 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.slice(1, -1) } }); + const ast = parse(rendered); + const result = await compileHtml(ast.html, {...state, markers: {...state.markers, insideMarkdown: false }}, compileOptions); + + buffers.out += ',' + result; + buffers.markdown = ''; + curr = 'out'; + } - let outSource = ''; - walk(enterNode, { - enter(node: TemplateNode, parent: TemplateNode) { - switch (node.type) { - case 'Expression': { - let children: string[] = []; - for (const child of node.children || []) { - children.push(compileHtml(child, state, compileOptions)); - } - let raw = ''; - let nextChildIndex = 0; - for (const chunk of node.codeChunks) { - raw += chunk; - if (nextChildIndex < children.length) { - raw += children[nextChildIndex++]; + asyncWalk(enterNode, { + async enter(node: TemplateNode, parent: TemplateNode) { + switch (node.type) { + case 'Expression': { + const children: string[] = await Promise.all((node.children ?? []).map(child => compileHtml(child, state, compileOptions))); + let raw = ''; + let nextChildIndex = 0; + for (const chunk of node.codeChunks) { + raw += chunk; + if (nextChildIndex < children.length) { + raw += children[nextChildIndex++]; + } } + // TODO Do we need to compile this now, or should we compile the entire module at the end? + let code = compileExpressionSafe(raw).trim().replace(/\;$/, ''); + if (state.markers.insideMarkdown) { + buffers[curr] += `{${code}}`; + } else { + buffers[curr] += `,(${code})`; + } + this.skip(); + break; } - // TODO Do we need to compile this now, or should we compile the entire module at the end? - let code = compileExpressionSafe(raw).trim().replace(/\;$/, ''); - outSource += `,(${code})`; - this.skip(); - break; - } - case 'MustacheTag': - case 'Comment': - return; - case 'Fragment': - break; - case 'Slot': - case 'Head': - case 'InlineComponent': - case 'Title': - case 'Element': { - const name: string = node.name; - if (!name) { - throw new Error('AHHHH'); - } - try { - const attributes = getAttributes(node.attributes); - - outSource += outSource === '' ? '' : ','; - if (node.type === 'Slot') { - outSource += `(children`; - return; + case 'MustacheTag': + case 'Comment': + return; + case 'Fragment': + break; + case 'Slot': + case 'Head': + case 'InlineComponent': + case 'Title': + case 'Element': { + const name: string = node.name; + if (!name) { + throw new Error('AHHHH'); } - const COMPONENT_NAME_SCANNER = /^[A-Z]/; - if (!COMPONENT_NAME_SCANNER.test(name)) { - outSource += `h("${name}", ${attributes ? generateAttributes(attributes) : 'null'}`; - if (state.markers.insideMarkdown) { - outSource += `,h(__astroMarkdownRender, null`; + try { + const attributes = getAttributes(node.attributes); + + buffers.out += buffers.out === '' ? '' : ','; + + if (node.type === 'Slot') { + buffers[curr] += `(children`; + paren++; + return; } - return; - } - const [componentName, componentKind] = name.split(':'); - const componentImportData = components[componentName]; - if (!componentImportData) { - throw new Error(`Unknown Component: ${componentName}`); - } - if (componentImportData.type === '.astro') { - if (componentName === 'Markdown') { - const attributeStr = attributes ? generateAttributes(attributes) : 'null'; - state.markers.insideMarkdown = attributeStr; - outSource += `h(__astroMarkdownRender, ${attributeStr}`; + const COMPONENT_NAME_SCANNER = /^[A-Z]/; + if (!COMPONENT_NAME_SCANNER.test(name)) { + if (curr === 'markdown') { + await pushMarkdownToBuffer(); + } + buffers[curr] += `h("${name}", ${attributes ? generateAttributes(attributes) : 'null'}`; + paren++; return; } - } - const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], { astroConfig, dynamicImports, filename }); - if (wrapperImport) { - importExportStatements.add(wrapperImport); - } + const [componentName, componentKind] = name.split(':'); + const componentImportData = components[componentName]; + if (!componentImportData) { + throw new Error(`Unknown Component: ${componentName}`); + } + if (componentImportData.type === '.astro') { + if (componentName === 'Markdown') { + const { $scope } = attributes ?? {}; + state.markers.insideMarkdown = { $scope }; + curr = 'markdown'; + return; + } + } + const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], { astroConfig, dynamicImports, filename }); + if (wrapperImport) { + importExportStatements.add(wrapperImport); + } + if (curr === 'markdown') { + await pushMarkdownToBuffer(); + } - outSource += `h(${wrapper}, ${attributes ? generateAttributes(attributes) : 'null'}`; + paren++; + buffers[curr] += `h(${wrapper}, ${attributes ? generateAttributes(attributes) : 'null'}`; + } catch (err) { + // handle errors in scope with filename + const rel = filename.replace(astroConfig.projectRoot.pathname, ''); + // TODO: return actual codeframe here + error(compileOptions.logging, rel, err.toString()); + } + return; + } + case 'Attribute': { + this.skip(); + return; + } + case 'Style': { + css.push(node.content.styles); // if multiple <style> tags, combine together + this.skip(); + return; + } + case 'CodeSpan': + case 'CodeFence': { if (state.markers.insideMarkdown) { - const attributeStr = state.markers.insideMarkdown; - outSource += `,h(__astroMarkdownRender, ${attributeStr}`; + if (curr === 'out') curr = 'markdown'; + buffers[curr] += node.raw; + return; } - } catch (err) { - // handle errors in scope with filename - const rel = filename.replace(astroConfig.projectRoot.pathname, ''); - // TODO: return actual codeframe here - error(compileOptions.logging, rel, err.toString()); + buffers[curr] += ',' + JSON.stringify(node.data); + return; } - return; - } - case 'Attribute': { - this.skip(); - return; - } - case 'Style': { - css.push(node.content.styles); // if multiple <style> tags, combine together - this.skip(); - return; - } - case 'CodeSpan': - case 'CodeFence': { - outSource += ',' + JSON.stringify(node.raw); - return; - } - case 'Text': { - const text = getTextFromAttribute(node); - // Whitespace is significant if we are immediately inside of <Markdown>, - // but not if we're inside of another component in <Markdown> - if (parent.name !== 'Markdown' && !text.trim()) { + case 'Text': { + let text = getTextFromAttribute(node); + if (state.markers.insideMarkdown) { + if (curr === 'out') curr = 'markdown'; + buffers[curr] += text; + return; + } + if (parent.name !== 'Markdown' && !text.trim()) { + return; + } + if (parent.name === 'code') { + // Special case, escaped { characters from markdown content + text = node.raw.replace(/&#123;/g, '{'); + } + buffers[curr] += ',' + JSON.stringify(text); return; } - outSource += ',' + JSON.stringify(text); - return; + default: + throw new Error('Unexpected (enter) node type: ' + node.type); } - default: - throw new Error('Unexpected (enter) node type: ' + node.type); - } - }, - leave(node, parent, prop, index) { - switch (node.type) { - case 'Text': - case 'CodeSpan': - case 'CodeFence': - case 'Attribute': - case 'Comment': - case 'Fragment': - case 'Expression': - case 'MustacheTag': - return; - case 'Slot': - case 'Head': - case 'Body': - case 'Title': - case 'Element': - case 'InlineComponent': { - if (node.type === 'InlineComponent' && node.name === 'Markdown') { - state.markers.insideMarkdown = false; + }, + async leave(node, parent, prop, index) { + switch (node.type) { + case 'Text': + case 'Attribute': + case 'Comment': + case 'Fragment': + case 'Expression': + case 'MustacheTag': + return; + case 'CodeSpan': + case 'CodeFence': + return; + case 'Slot': + case 'Head': + case 'Body': + case 'Title': + case 'Element': + case 'InlineComponent': { + if (node.type === 'InlineComponent' && curr === 'markdown' && buffers.markdown !== '') { + await pushMarkdownToBuffer(); + } + if (paren !== -1) { + buffers.out += ')'; + paren--; + } + return; } - if (state.markers.insideMarkdown) { - outSource += ')'; + case 'Style': { + this.remove(); // this will be optimized in a global CSS file; remove so it‘s not accidentally inlined + return; } - outSource += ')'; - return; + default: + throw new Error('Unexpected (leave) node type: ' + node.type); } - case 'Style': { - this.remove(); // this will be optimized in a global CSS file; remove so it‘s not accidentally inlined - return; - } - default: - throw new Error('Unexpected (leave) node type: ' + node.type); - } - }, + }, + }).then(() => { + const content = buffers.out.replace(/^\,/, '').replace(/\,\)/g, ')').replace(/\,+/g, ',').replace(/\)h/g, '),h'); + buffers.out = ''; + buffers.markdown = ''; + return resolve(content); + }); }); - - return outSource; } /** @@ -721,7 +773,7 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt compileCss(ast.css, state); - const html = compileHtml(ast.html, state, compileOptions); + const html = await compileHtml(ast.html, state, compileOptions); return { script: script, diff --git a/packages/astro/src/compiler/index.ts b/packages/astro/src/compiler/index.ts index 0eef6b5cd..fb1ca71af 100644 --- a/packages/astro/src/compiler/index.ts +++ b/packages/astro/src/compiler/index.ts @@ -29,7 +29,7 @@ interface ConvertAstroOptions { * 2. Transform * 3. Codegen */ -async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): Promise<TransformResult> { +export async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): Promise<TransformResult> { const { filename } = opts; // 1. Parse @@ -48,11 +48,12 @@ async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): P * .md -> .astro source */ export async function convertMdToAstroSource(contents: string, { filename }: { filename: string }): Promise<string> { - const { + let { content, frontmatter: { layout, ...frontmatter }, ...data } = await renderMarkdownWithFrontmatter(contents); + if (frontmatter['astro'] !== undefined) { throw new Error(`"astro" is a reserved word but was used as a frontmatter value!\n\tat ${filename}`); } @@ -109,7 +110,6 @@ export async function compileComponent( ): Promise<CompileResult> { const result = await transformFromSource(source, { compileOptions, filename, projectRoot }); const site = compileOptions.astroConfig.buildOptions.site || `http://localhost:${compileOptions.astroConfig.devOptions.port}`; - const usesMarkdown = !!result.imports.find((spec) => spec.indexOf('Markdown') > -1); // return template let modJsx = ` @@ -120,7 +120,6 @@ ${result.imports.join('\n')} // \`__render()\`: Render the contents of the Astro module. import { h, Fragment } from '${internalImport('h.js')}'; -${usesMarkdown ? `import __astroMarkdownRender from '${internalImport('markdown.js')}';` : ''}; const __astroRequestSymbol = Symbol('astro.request'); async function __render(props, ...children) { const Astro = { diff --git a/packages/astro/src/compiler/markdown/codeblock.ts b/packages/astro/src/compiler/markdown/codeblock.ts new file mode 100644 index 000000000..3f7e86951 --- /dev/null +++ b/packages/astro/src/compiler/markdown/codeblock.ts @@ -0,0 +1,41 @@ +import { visit } from 'unist-util-visit'; + +/** */ +export function remarkCodeBlock() { + const visitor = (node: any) => { + const { data, lang, meta } = node; + let currentClassName = data?.hProperties?.class ?? ''; + node.data = node.data || {}; + node.data.hProperties = node.data.hProperties || {}; + node.data.hProperties = { ...node.data.hProperties, class: `language-${lang} ${currentClassName}`.trim(), lang, meta } + + return node; + }; + return () => (tree: any) => visit(tree, 'code', visitor); +} + +/** */ +export function rehypeCodeBlock() { + const escapeCode = (code: any) => { + code.children = code.children.map((child: any) => { + if (child.type === 'text') { + return { ...child, value: child.value.replace(/\{/g, '{') }; + } + return child; + }) + } + const visitor = (node: any) => { + if (node.tagName === 'code') { + escapeCode(node); + return; + } + + if (node.tagName !== 'pre') return; + const code = node.children[0]; + if (code.tagName !== 'code') return; + node.properties = { ...code.properties }; + + return node; + }; + return () => (tree: any) => visit(tree, 'element', visitor); +} diff --git a/packages/astro/src/compiler/markdown/micromark.d.ts b/packages/astro/src/compiler/markdown/micromark.d.ts index 245b91fc1..e0832e31c 100644 --- a/packages/astro/src/compiler/markdown/micromark.d.ts +++ b/packages/astro/src/compiler/markdown/micromark.d.ts @@ -2,6 +2,9 @@ declare module '@silvenon/remark-smartypants' { export default function (): any; } +declare module 'mdast-util-mdx'; +declare module 'micromark-extension-mdxjs'; + declare module 'mdast-util-mdx/from-markdown.js' { export default function (): any; } diff --git a/packages/astro/src/compiler/markdown/remark-mdx-lite.ts b/packages/astro/src/compiler/markdown/remark-mdx-lite.ts index 9ab8d764f..6ec492211 100644 --- a/packages/astro/src/compiler/markdown/remark-mdx-lite.ts +++ b/packages/astro/src/compiler/markdown/remark-mdx-lite.ts @@ -1,26 +1,31 @@ -import fromMarkdown from 'mdast-util-mdx/from-markdown.js'; -import toMarkdown from 'mdast-util-mdx/to-markdown.js'; -/** See https://github.com/micromark/micromark-extension-mdx-md */ -const syntax = { disable: { null: ['autolink', 'codeIndented'] } }; +import syntaxMdxjs from 'micromark-extension-mdxjs' +import {fromMarkdown, toMarkdown} from 'mdast-util-mdx' /** - * Lite version of https://github.com/mdx-js/mdx/tree/main/packages/remark-mdx - * We don't need all the features MDX does because all components are precompiled - * to HTML. We just want to disable a few MD features that cause issues. + * Add the micromark and mdast extensions for MDX.js (JS aware MDX). + * + * @this {Processor} + * @param {MdxOptions} [options] + * @return {void} */ -function mdxLite(this: any) { - let data = this.data(); +export function remarkMdx(this: any, options: any) { + let data = this.data() - add('micromarkExtensions', syntax); - add('fromMarkdownExtensions', fromMarkdown); - add('toMarkdownExtensions', toMarkdown); + add('micromarkExtensions', syntaxMdxjs(options)) + add('fromMarkdownExtensions', fromMarkdown) + add('toMarkdownExtensions', toMarkdown) - /** Adds remark plugin */ + /** + * @param {string} field + * @param {unknown} value + */ function add(field: string, value: any) { - if (data[field]) data[field].push(value); - else data[field] = [value]; + // Other extensions defined before this. + // Useful when externalizing. + /* c8 ignore next 2 */ + // @ts-ignore Assume it’s an array. + if (data[field]) data[field].push(value) + else data[field] = [value] } } - -export default mdxLite; diff --git a/packages/astro/src/compiler/markdown/remark-scoped-styles.ts b/packages/astro/src/compiler/markdown/remark-scoped-styles.ts index 7d19ae0ee..9ca70c029 100644 --- a/packages/astro/src/compiler/markdown/remark-scoped-styles.ts +++ b/packages/astro/src/compiler/markdown/remark-scoped-styles.ts @@ -7,10 +7,10 @@ export default function scopedStyles(className: string) { if (noVisit.has(node.type)) return; const { data } = node; - const currentClassName = data?.hProperties?.class ?? ''; + let currentClassName = data?.hProperties?.class ?? ''; node.data = node.data || {}; node.data.hProperties = node.data.hProperties || {}; - node.data.hProperties.className = `${className} ${currentClassName}`.trim(); + node.data.hProperties.class = `${className} ${currentClassName}`.trim(); return node; }; diff --git a/packages/astro/src/compiler/transform/prism.ts b/packages/astro/src/compiler/transform/prism.ts index 3b2674618..5e89e06b4 100644 --- a/packages/astro/src/compiler/transform/prism.ts +++ b/packages/astro/src/compiler/transform/prism.ts @@ -1,5 +1,5 @@ import type { Transformer } from '../../@types/transformer'; -import type { Script } from 'astro-parser'; +import type { Script, TemplateNode } from 'astro-parser'; import { getAttrValue } from '../../ast.js'; const PRISM_IMPORT = `import Prism from 'astro/components/Prism.astro';\n`; @@ -8,7 +8,17 @@ const prismImportExp = /import Prism from ['"]astro\/components\/Prism.astro['"] function escape(code: string) { return code.replace(/[`$]/g, (match) => { return '\\' + match; - }); + }).replace(/{/g, '{'); +} + +/** Unescape { characters transformed by Markdown generation */ +function unescapeCode(code: TemplateNode) { + code.children = code.children?.map(child => { + if (child.type === 'Text') { + return { ...child, raw: child.raw.replace(/&#123;/g, '{') } + } + return child; + }) } /** default export - Transform prism */ export default function (module: Script): Transformer { @@ -19,6 +29,11 @@ export default function (module: Script): Transformer { html: { Element: { enter(node) { + if (node.name === 'code') { + unescapeCode(node); + return; + } + if (node.name !== 'pre') return; const codeEl = node.children && node.children[0]; if (!codeEl || codeEl.name !== 'code') return; diff --git a/packages/astro/src/compiler/utils.ts b/packages/astro/src/compiler/utils.ts index 0fbc070f1..078b0e7ab 100644 --- a/packages/astro/src/compiler/utils.ts +++ b/packages/astro/src/compiler/utils.ts @@ -1,20 +1,20 @@ -import mdxLite from './markdown/remark-mdx-lite.js'; +import type { AstroMarkdownOptions } from '../@types/astro'; import createCollectHeaders from './markdown/rehype-collect-headers.js'; import scopedStyles from './markdown/remark-scoped-styles.js'; +import { remarkCodeBlock, rehypeCodeBlock } from './markdown/codeblock.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 stringify from 'rehype-stringify'; +// import smartypants from '@silvenon/remark-smartypants'; +import rehypeStringify from 'rehype-stringify'; -export interface MarkdownRenderingOptions { +export interface MarkdownRenderingOptions extends Partial<AstroMarkdownOptions> { $?: { scopedClassName: string | null; }; - footnotes?: boolean; - gfm?: boolean; - plugins?: any[]; + mode: 'md'|'astro-md'; } /** Internal utility for rendering a full markdown file and extracting Frontmatter data */ @@ -22,16 +22,16 @@ export async function renderMarkdownWithFrontmatter(contents: string, opts?: Mar // 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); + const value = await renderMarkdown(content, { ...opts, mode: 'md' }); return { ...value, frontmatter }; } /** Shared utility for rendering markdown */ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOptions | null) { - const { $: { scopedClassName = null } = {}, footnotes: useFootnotes = true, gfm: useGfm = true, plugins = [] } = opts ?? {}; + const { $: { scopedClassName = null } = {}, mode = 'astro-md', footnotes: useFootnotes = true, gfm: useGfm = true } = opts ?? {}; const { headers, rehypeCollectHeaders } = createCollectHeaders(); - let parser = unified().use(markdown).use(mdxLite).use(smartypants); + let parser = unified().use(markdown).use(remarkCodeBlock()); if (scopedClassName) { parser = parser.use(scopedStyles(scopedClassName)); @@ -53,7 +53,8 @@ export async function renderMarkdown(content: string, opts?: MarkdownRenderingOp .use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw'] }) .use(raw) .use(rehypeCollectHeaders) - .use(stringify) + .use(rehypeCodeBlock()) + .use(rehypeStringify) .process(content); result = vfile.contents.toString(); } catch (err) { diff --git a/packages/astro/src/config.ts b/packages/astro/src/config.ts index 62a28fc8f..ace1b931f 100644 --- a/packages/astro/src/config.ts +++ b/packages/astro/src/config.ts @@ -60,6 +60,7 @@ function configDefaults(userConfig?: any): any { if (!config.devOptions) config.devOptions = {}; if (!config.devOptions.port) config.devOptions.port = 3000; if (!config.buildOptions) config.buildOptions = {}; + if (!config.markdownOptions) config.markdownOptions = {}; if (typeof config.buildOptions.sitemap === 'undefined') config.buildOptions.sitemap = true; return config; diff --git a/packages/astro/src/frontend/markdown.ts b/packages/astro/src/frontend/markdown.ts deleted file mode 100644 index 2cae2a65b..000000000 --- a/packages/astro/src/frontend/markdown.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { renderMarkdown } from '../compiler/utils.js'; - -/** - * Functional component which uses Astro's built-in Markdown rendering - * to render out its children. - * - * Note: the children have already been properly escaped/rendered - * by the parser and Astro, so at this point we're just rendering - * out plain markdown, no need for JSX support - */ -export default async function Markdown(props: { $scope: string | null }, ...children: string[]): Promise<string> { - const { $scope = null } = props ?? {}; - const text = dedent(children.join('').trimEnd()); - let { content } = await renderMarkdown(text, { $: { scopedClassName: $scope } }); - if (content.split('<p>').length === 2) { - content = content.replace(/^\<p\>/i, '').replace(/\<\/p\>$/i, ''); - } - return content; -} - -/** Remove leading indentation based on first line */ -function dedent(str: string) { - let arr = str.match(/^[ \t]*(?=\S)/gm); - let first = !!arr && arr.find((x) => x.length > 0)?.length; - return !arr || !first ? str : str.replace(new RegExp(`^[ \\t]{0,${first}}`, 'gm'), ''); -} diff --git a/packages/astro/src/runtime.ts b/packages/astro/src/runtime.ts index fd5366f8b..b59642f50 100644 --- a/packages/astro/src/runtime.ts +++ b/packages/astro/src/runtime.ts @@ -339,7 +339,12 @@ async function createSnowpack(astroConfig: AstroConfig, options: CreateSnowpackO }, packageOptions: { knownEntrypoints: ['preact-render-to-string'], - external: ['@vue/server-renderer', 'node-fetch', 'prismjs/components/index.js'], + external: [ + '@vue/server-renderer', + 'node-fetch', + 'prismjs/components/index.js', + 'gray-matter', + ], }, }); @@ -1710,7 +1710,7 @@ acorn-globals@^3.0.0: dependencies: acorn "^4.0.4" -acorn-jsx@^5.3.1: +acorn-jsx@^5.0.0, acorn-jsx@^5.3.1: version "5.3.1" resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz" integrity sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng== @@ -1744,6 +1744,11 @@ acorn@^7.0.0, acorn@^7.4.0: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +acorn@^8.0.0: + version "8.2.4" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.2.4.tgz#caba24b08185c3b56e3168e97d15ed17f4d31fd0" + integrity sha512-Ibt84YwBDDA890eDiDCEqcbwvHlBvzzDkU2cGBBDDI1QWT12jTiXIOn2CIw5KK4i6N5Z2HUxwYjzriDyqaqqZg== + add-stream@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz" @@ -4043,6 +4048,11 @@ estraverse@^5.1.0, estraverse@^5.2.0: resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz" integrity sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ== +estree-util-is-identifier-name@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/estree-util-is-identifier-name/-/estree-util-is-identifier-name-1.1.0.tgz#2e3488ea06d9ea2face116058864f6370b37456d" + integrity sha512-OVJZ3fGGt9By77Ix9NhaRbzfbDV/2rx9EP7YIDJTmsZSEc5kYn2vWcNccYyahJL2uAQZK2a5Or2i0wtIKTPoRQ== + estree-walker@^0.6.1: version "0.6.1" resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz" @@ -6627,6 +6637,51 @@ micromark-extension-gfm@^0.3.0: micromark-extension-gfm-tagfilter "~0.3.0" micromark-extension-gfm-task-list-item "~0.3.0" +micromark-extension-mdx-expression@^0.3.0, micromark-extension-mdx-expression@^0.3.2, micromark-extension-mdx-expression@~0.3.0: + version "0.3.2" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-0.3.2.tgz#827592af50116110dc9ee27201a73c037e61aa27" + integrity sha512-Sh8YHLSAlbm/7TZkVKEC4wDcJE8XhVpZ9hUXBue1TcAicrrzs/oXu7PHH3NcyMemjGyMkiVS34Y0AHC5KG3y4A== + dependencies: + micromark "~2.11.0" + vfile-message "^2.0.0" + +micromark-extension-mdx-jsx@~0.3.0: + version "0.3.3" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-0.3.3.tgz#68e8e700f2860e32e96ff48e44afb7465d462e21" + integrity sha512-kG3VwaJlzAPdtIVDznfDfBfNGMTIzsHqKpTmMlew/iPnUCDRNkX+48ElpaOzXAtK5axtpFKE3Hu3VBriZDnRTQ== + dependencies: + estree-util-is-identifier-name "^1.0.0" + micromark "~2.11.0" + micromark-extension-mdx-expression "^0.3.2" + vfile-message "^2.0.0" + +micromark-extension-mdx-md@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/micromark-extension-mdx-md/-/micromark-extension-mdx-md-0.1.1.tgz#277b4e82ada37bfdf222f6c3530e20563d73e064" + integrity sha512-emlFQEyfx/2aPhwyEqeNDfKE6jPH1cvLTb5ANRo4qZBjaUObnzjLRdzK8RJ4Xc8+/dOmKN8TTRxFnOYF5/EAwQ== + +micromark-extension-mdxjs-esm@~0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-0.3.1.tgz#40a710fe145b381e39a2930db2813f3efaa014ac" + integrity sha512-tuLgcELrgY1a5tPxjk+MrI3BdYtwW67UaHZdzKiDYD8loNbxwIscfdagI6A2BKuAkrfeyHF6FW3B8KuDK3ZMXw== + dependencies: + micromark "~2.11.0" + micromark-extension-mdx-expression "^0.3.0" + vfile-message "^2.0.0" + +micromark-extension-mdxjs@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/micromark-extension-mdxjs/-/micromark-extension-mdxjs-0.3.0.tgz#35ecebaf14b8377b6046b659780fd3111196eccd" + integrity sha512-NQuiYA0lw+eFDtSG4+c7ao3RG9dM4P0Kx/sn8OLyPhxtIc6k+9n14k5VfLxRKfAxYRTo8c5PLZPaRNmslGWxJw== + dependencies: + acorn "^8.0.0" + acorn-jsx "^5.0.0" + micromark "~2.11.0" + micromark-extension-mdx-expression "~0.3.0" + micromark-extension-mdx-jsx "~0.3.0" + micromark-extension-mdx-md "~0.1.0" + micromark-extension-mdxjs-esm "~0.3.0" + micromark@^2.11.3, micromark@~2.11.0, micromark@~2.11.3: version "2.11.4" resolved "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz" |