summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Nate Moore <natemoo-re@users.noreply.github.com> 2021-05-21 15:52:20 -0500
committerGravatar GitHub <noreply@github.com> 2021-05-21 15:52:20 -0500
commit9cdada0bcc5fe3fad6f645ffda5f7e5934c738b5 (patch)
tree03dba70159fe324e9038460b708e515f3f6e3261
parent19e20f2c54eaa8f0c2ebec54b071ffc4abd524bd (diff)
downloadastro-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.md6
-rw-r--r--packages/astro-parser/src/parse/state/text.ts10
-rw-r--r--packages/astro/package.json1
-rw-r--r--packages/astro/src/@types/astro.ts8
-rw-r--r--packages/astro/src/compiler/codegen/index.ts330
-rw-r--r--packages/astro/src/compiler/index.ts7
-rw-r--r--packages/astro/src/compiler/markdown/codeblock.ts41
-rw-r--r--packages/astro/src/compiler/markdown/micromark.d.ts3
-rw-r--r--packages/astro/src/compiler/markdown/remark-mdx-lite.ts39
-rw-r--r--packages/astro/src/compiler/markdown/remark-scoped-styles.ts4
-rw-r--r--packages/astro/src/compiler/transform/prism.ts19
-rw-r--r--packages/astro/src/compiler/utils.ts23
-rw-r--r--packages/astro/src/config.ts1
-rw-r--r--packages/astro/src/frontend/markdown.ts26
-rw-r--r--packages/astro/src/runtime.ts7
-rw-r--r--yarn.lock57
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(/&#x26;#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, '&#123;') };
+ }
+ 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(/&#123;/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(/&#x26;#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',
+ ],
},
});
diff --git a/yarn.lock b/yarn.lock
index b386fb2d5..8ff5db51e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"