summaryrefslogtreecommitdiff
path: root/packages/astro/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro/src')
-rw-r--r--packages/astro/src/@types/micromark.ts3
-rw-r--r--packages/astro/src/build/page.ts2
-rw-r--r--packages/astro/src/compiler/codegen/index.ts80
-rw-r--r--packages/astro/src/compiler/index.ts58
-rw-r--r--packages/astro/src/compiler/markdown/micromark-collect-headers.ts38
-rw-r--r--packages/astro/src/compiler/markdown/micromark-encode.ts36
-rw-r--r--packages/astro/src/compiler/markdown/micromark-mdx-astro.ts22
-rw-r--r--packages/astro/src/compiler/markdown/micromark.d.ts14
-rw-r--r--packages/astro/src/compiler/markdown/rehype-collect-headers.ts30
-rw-r--r--packages/astro/src/compiler/markdown/remark-mdx-lite.ts26
-rw-r--r--packages/astro/src/compiler/markdown/remark-scoped-styles.ts18
-rw-r--r--packages/astro/src/compiler/transform/styles.ts63
-rw-r--r--packages/astro/src/compiler/utils.ts70
-rw-r--r--packages/astro/src/frontend/markdown.ts26
-rw-r--r--packages/astro/src/frontend/render/renderer.ts10
-rw-r--r--packages/astro/src/frontend/render/utils.ts15
-rw-r--r--packages/astro/src/runtime.ts6
-rw-r--r--packages/astro/src/search.ts2
18 files changed, 315 insertions, 204 deletions
diff --git a/packages/astro/src/@types/micromark.ts b/packages/astro/src/@types/micromark.ts
index 9725aabb9..5060ab468 100644
--- a/packages/astro/src/@types/micromark.ts
+++ b/packages/astro/src/@types/micromark.ts
@@ -1,6 +1,9 @@
export interface MicromarkExtensionContext {
sliceSerialize(node: any): string;
raw(value: string): void;
+ tag(value: string): void;
+ data(value: string): void;
+ resume(): any;
}
export type MicromarkExtensionCallback = (this: MicromarkExtensionContext, node: any) => void;
diff --git a/packages/astro/src/build/page.ts b/packages/astro/src/build/page.ts
index cc28040d6..a83a945d3 100644
--- a/packages/astro/src/build/page.ts
+++ b/packages/astro/src/build/page.ts
@@ -181,7 +181,7 @@ async function gatherRuntimes({ astroConfig, buildState, filepath, logging, reso
let source = await fs.promises.readFile(filepath, 'utf8');
if (filepath.pathname.endsWith('.md')) {
- source = await convertMdToAstroSource(source);
+ source = await convertMdToAstroSource(source, { filename: fileURLToPath(filepath) });
}
const ast = parse(source, { filepath });
diff --git a/packages/astro/src/compiler/codegen/index.ts b/packages/astro/src/compiler/codegen/index.ts
index 2da1ceec5..ab66ee47d 100644
--- a/packages/astro/src/compiler/codegen/index.ts
+++ b/packages/astro/src/compiler/codegen/index.ts
@@ -305,6 +305,9 @@ interface CodegenState {
filename: string;
components: Components;
css: string[];
+ markers: {
+ insideMarkdown: boolean|string;
+ };
importExportStatements: Set<string>;
dynamicImports: DynamicImportMap;
}
@@ -318,6 +321,7 @@ function compileModule(module: Script, state: CodegenState, compileOptions: Comp
const componentExports: ExportNamedDeclaration[] = [];
const contentImports = new Map<string, { spec: string; declarator: string }>();
+ const importSpecifierTypes = new Set(['ImportDefaultSpecifier', 'ImportSpecifier']);
let script = '';
let propsStatement = '';
@@ -418,7 +422,7 @@ function compileModule(module: Script, state: CodegenState, compileOptions: Comp
const specifier = componentImport.specifiers[0];
if (!specifier) continue; // this is unused
// set componentName to default import if used (user), or use filename if no default import (mostly internal use)
- const componentName = specifier.type === 'ImportDefaultSpecifier' ? specifier.local.name : path.posix.basename(importUrl, componentType);
+ const componentName = importSpecifierTypes.has(specifier.type) ? specifier.local.name : path.posix.basename(importUrl, componentType);
const plugin = extensions[componentType] || defaultExtensions[componentType];
state.components[componentName] = {
type: componentType,
@@ -541,7 +545,7 @@ function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOption
let outSource = '';
walk(enterNode, {
- enter(node: TemplateNode) {
+ enter(node: TemplateNode, parent: TemplateNode) {
switch (node.type) {
case 'Expression': {
let children: string[] = [];
@@ -579,27 +583,42 @@ function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOption
try {
const attributes = getAttributes(node.attributes);
- outSource += outSource === '' ? '' : ',';
- if (node.type === 'Slot') {
- outSource += `(children`;
- return;
+ outSource += outSource === '' ? '' : ',';
+ if (node.type === 'Slot') {
+ outSource += `(children`;
+ return;
+ }
+ 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`
}
- const COMPONENT_NAME_SCANNER = /^[A-Z]/;
- if (!COMPONENT_NAME_SCANNER.test(name)) {
- outSource += `h("${name}", ${attributes ? generateAttributes(attributes) : 'null'}`;
+ 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}`
return;
}
- const [componentName, componentKind] = name.split(':');
- const componentImportData = components[componentName];
- if (!componentImportData) {
- throw new Error(`Unknown Component: ${componentName}`);
- }
- const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], { astroConfig, dynamicImports, filename });
- if (wrapperImport) {
- importExportStatements.add(wrapperImport);
- }
+ }
+ const { wrapper, wrapperImport } = getComponentWrapper(name, components[componentName], { astroConfig, dynamicImports, filename });
+ if (wrapperImport) {
+ importExportStatements.add(wrapperImport);
+ }
outSource += `h(${wrapper}, ${attributes ? generateAttributes(attributes) : 'null'}`;
+ if (state.markers.insideMarkdown) {
+ const attributeStr = state.markers.insideMarkdown;
+ outSource += `,h(__astroMarkdownRender, ${attributeStr}`
+ }
} catch (err) {
// handle errors in scope with filename
const rel = filename.replace(astroConfig.projectRoot.pathname, '');
@@ -617,9 +636,16 @@ function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOption
this.skip();
return;
}
+ case 'CodeSpan':
+ case 'CodeFence': {
+ outSource += ',' + JSON.stringify(node.raw);
+ return;
+ }
case 'Text': {
const text = getTextFromAttribute(node);
- if (!text.trim()) {
+ // 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()) {
return;
}
outSource += ',' + JSON.stringify(text);
@@ -632,6 +658,8 @@ function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOption
leave(node, parent, prop, index) {
switch (node.type) {
case 'Text':
+ case 'CodeSpan':
+ case 'CodeFence':
case 'Attribute':
case 'Comment':
case 'Fragment':
@@ -643,9 +671,16 @@ function compileHtml(enterNode: TemplateNode, state: CodegenState, compileOption
case 'Body':
case 'Title':
case 'Element':
- case 'InlineComponent':
+ case 'InlineComponent': {
+ if (node.type === 'InlineComponent' && node.name === 'Markdown') {
+ state.markers.insideMarkdown = false;
+ }
+ if (state.markers.insideMarkdown) {
+ outSource += ')';
+ }
outSource += ')';
return;
+ }
case 'Style': {
this.remove(); // this will be optimized in a global CSS file; remove so it‘s not accidentally inlined
return;
@@ -674,8 +709,11 @@ export async function codegen(ast: Ast, { compileOptions, filename }: CodeGenOpt
filename,
components: {},
css: [],
+ markers: {
+ insideMarkdown: false
+ },
importExportStatements: new Set(),
- dynamicImports: new Map(),
+ dynamicImports: new Map()
};
const { script, componentPlugins, createCollection } = compileModule(ast.module, state, compileOptions);
diff --git a/packages/astro/src/compiler/index.ts b/packages/astro/src/compiler/index.ts
index f4bfbb19d..afdaac986 100644
--- a/packages/astro/src/compiler/index.ts
+++ b/packages/astro/src/compiler/index.ts
@@ -3,15 +3,9 @@ import type { CompileResult, TransformResult } from '../@types/astro';
import type { CompileOptions } from '../@types/compiler.js';
import path from 'path';
-import micromark from 'micromark';
-import gfmSyntax from 'micromark-extension-gfm';
-import matter from 'gray-matter';
-import gfmHtml from 'micromark-extension-gfm/html.js';
+import { renderMarkdownWithFrontmatter } from './utils.js';
import { parse } from 'astro-parser';
-import { createMarkdownHeadersCollector } from './markdown/micromark-collect-headers.js';
-import { encodeMarkdown } from './markdown/micromark-encode.js';
-import { encodeAstroMdx } from './markdown/micromark-mdx-astro.js';
import { transform } from './transform/index.js';
import { codegen } from './codegen/index.js';
@@ -53,38 +47,24 @@ async function convertAstroToJsx(template: string, opts: ConvertAstroOptions): P
/**
* .md -> .astro source
*/
-export async function convertMdToAstroSource(contents: string): Promise<string> {
- const { data: frontmatterData, content } = matter(contents);
- const { headers, headersExtension } = createMarkdownHeadersCollector();
- const { htmlAstro, mdAstro } = encodeAstroMdx();
- const mdHtml = micromark(content, {
- allowDangerousHtml: true,
- extensions: [gfmSyntax(), ...htmlAstro],
- htmlExtensions: [gfmHtml, encodeMarkdown, headersExtension, mdAstro],
- });
-
- // TODO: Warn if reserved word is used in "frontmatterData"
+export async function convertMdToAstroSource(contents: string, { filename }: { filename: string }): Promise<string> {
+ const { 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}`);
+ }
const contentData: any = {
- ...frontmatterData,
- headers,
- source: content,
+ ...frontmatter,
+ ...data
};
-
- let imports = '';
- for (let [ComponentName, specifier] of Object.entries(frontmatterData.import || {})) {
- imports += `import ${ComponentName} from '${specifier}';\n`;
- }
-
// </script> can't be anywhere inside of a JS string, otherwise the HTML parser fails.
// Break it up here so that the HTML parser won't detect it.
const stringifiedSetupContext = JSON.stringify(contentData).replace(/\<\/script\>/g, `</scrip" + "t>`);
return `---
- ${imports}
- ${frontmatterData.layout ? `import {__renderPage as __layout} from '${frontmatterData.layout}';` : 'const __layout = undefined;'}
- export const __content = ${stringifiedSetupContext};
+${layout ? `import {__renderPage as __layout} from '${layout}';` : 'const __layout = undefined;'}
+export const __content = ${stringifiedSetupContext};
---
-<section>${mdHtml}</section>`;
+${content}`;
}
/**
@@ -95,24 +75,24 @@ async function convertMdToJsx(
contents: string,
{ compileOptions, filename, fileID }: { compileOptions: CompileOptions; filename: string; fileID: string }
): Promise<TransformResult> {
- const raw = await convertMdToAstroSource(contents);
+const raw = await convertMdToAstroSource(contents, { filename });
const convertOptions = { compileOptions, filename, fileID };
return await convertAstroToJsx(raw, convertOptions);
}
-type SupportedExtensions = '.astro' | '.md';
-
-/** Given a file, process it either as .astro or .md. */
+/** Given a file, process it either as .astro, .md */
async function transformFromSource(
contents: string,
{ compileOptions, filename, projectRoot }: { compileOptions: CompileOptions; filename: string; projectRoot: string }
): Promise<TransformResult> {
const fileID = path.relative(projectRoot, filename);
- switch (path.extname(filename) as SupportedExtensions) {
- case '.astro':
+ switch (true) {
+ case filename.slice(-6) === '.astro':
return await convertAstroToJsx(contents, { compileOptions, filename, fileID });
- case '.md':
+
+ case filename.slice(-3) === '.md':
return await convertMdToJsx(contents, { compileOptions, filename, fileID });
+
default:
throw new Error('Not Supported!');
}
@@ -125,6 +105,7 @@ 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 = `
@@ -135,6 +116,7 @@ ${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/micromark-collect-headers.ts b/packages/astro/src/compiler/markdown/micromark-collect-headers.ts
deleted file mode 100644
index 69781231a..000000000
--- a/packages/astro/src/compiler/markdown/micromark-collect-headers.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import slugger from 'github-slugger';
-
-/**
- * Create Markdown Headers Collector
- * NOTE: micromark has terrible TS types. Instead of fighting with the
- * limited/broken TS types that they ship, we just reach for our good friend, "any".
- */
-export function createMarkdownHeadersCollector() {
- const headers: any[] = [];
- let currentHeader: any;
- return {
- headers,
- headersExtension: {
- enter: {
- atxHeading(node: any) {
- currentHeader = {};
- headers.push(currentHeader);
- this.buffer();
- },
- atxHeadingSequence(node: any) {
- currentHeader.depth = this.sliceSerialize(node).length;
- },
- atxHeadingText(node: any) {
- currentHeader.text = this.sliceSerialize(node);
- },
- } as any,
- exit: {
- atxHeading(node: any) {
- currentHeader.slug = slugger.slug(currentHeader.text);
- this.resume();
- this.tag(`<h${currentHeader.depth} id="${currentHeader.slug}">`);
- this.raw(currentHeader.text);
- this.tag(`</h${currentHeader.depth}>`);
- },
- } as any,
- } as any,
- };
-}
diff --git a/packages/astro/src/compiler/markdown/micromark-encode.ts b/packages/astro/src/compiler/markdown/micromark-encode.ts
deleted file mode 100644
index 635ab3b54..000000000
--- a/packages/astro/src/compiler/markdown/micromark-encode.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import type { Token } from 'micromark/dist/shared-types';
-import type { MicromarkExtension, MicromarkExtensionContext } from '../../@types/micromark';
-
-const characterReferences = {
- '"': 'quot',
- '&': 'amp',
- '<': 'lt',
- '>': 'gt',
- '{': 'lbrace',
- '}': 'rbrace',
-};
-
-type EncodedChars = '"' | '&' | '<' | '>' | '{' | '}';
-
-/** Encode HTML entity */
-function encode(value: string): string {
- return value.replace(/["&<>{}]/g, (raw: string) => {
- return '&' + characterReferences[raw as EncodedChars] + ';';
- });
-}
-
-/** Encode Markdown node */
-function encodeToken(this: MicromarkExtensionContext) {
- const token: Token = arguments[0];
- const value = this.sliceSerialize(token);
- this.raw(encode(value));
-}
-
-const plugin: MicromarkExtension = {
- exit: {
- codeTextData: encodeToken,
- codeFlowValue: encodeToken,
- },
-};
-
-export { plugin as encodeMarkdown };
diff --git a/packages/astro/src/compiler/markdown/micromark-mdx-astro.ts b/packages/astro/src/compiler/markdown/micromark-mdx-astro.ts
deleted file mode 100644
index b978ad407..000000000
--- a/packages/astro/src/compiler/markdown/micromark-mdx-astro.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { MicromarkExtension } from '../../@types/micromark';
-import mdxExpression from 'micromark-extension-mdx-expression';
-import mdxJsx from 'micromark-extension-mdx-jsx';
-
-/**
- * Keep MDX.
- */
-export function encodeAstroMdx() {
- const extension: MicromarkExtension = {
- enter: {
- mdxJsxFlowTag(node: any) {
- const mdx = this.sliceSerialize(node);
- this.raw(mdx);
- },
- },
- };
-
- return {
- htmlAstro: [mdxExpression(), mdxJsx()],
- mdAstro: extension,
- };
-}
diff --git a/packages/astro/src/compiler/markdown/micromark.d.ts b/packages/astro/src/compiler/markdown/micromark.d.ts
index fd094306e..9c084f437 100644
--- a/packages/astro/src/compiler/markdown/micromark.d.ts
+++ b/packages/astro/src/compiler/markdown/micromark.d.ts
@@ -1,11 +1,11 @@
-declare module 'micromark-extension-mdx-expression' {
- import type { HtmlExtension } from 'micromark/dist/shared-types';
-
- export default function (): HtmlExtension;
+declare module '@silvenon/remark-smartypants' {
+ export default function (): any;
}
-declare module 'micromark-extension-mdx-jsx' {
- import type { HtmlExtension } from 'micromark/dist/shared-types';
+declare module 'mdast-util-mdx/from-markdown.js' {
+ export default function (): any;
+}
- export default function (): HtmlExtension;
+declare module 'mdast-util-mdx/to-markdown.js' {
+ export default function (): any;
}
diff --git a/packages/astro/src/compiler/markdown/rehype-collect-headers.ts b/packages/astro/src/compiler/markdown/rehype-collect-headers.ts
new file mode 100644
index 000000000..3ebf3257d
--- /dev/null
+++ b/packages/astro/src/compiler/markdown/rehype-collect-headers.ts
@@ -0,0 +1,30 @@
+import { visit } from 'unist-util-visit';
+import slugger from 'github-slugger';
+
+/** */
+export default function createCollectHeaders() {
+ const headers: any[] = [];
+
+ const visitor = (node: any) => {
+ if (node.type !== 'element') return;
+ const { tagName, children } = node
+ if (tagName[0] !== 'h') return;
+ let [_, depth] = tagName.match(/h([0-6])/) ?? [];
+ if (!depth) return;
+ depth = Number.parseInt(depth);
+
+ let text = '';
+ visit(node, 'text', (child) => {
+ text += child.value;
+ })
+
+ let slug = slugger.slug(text);
+ node.properties = node.properties || {};
+ node.properties.id = slug;
+ headers.push({ depth, slug, text });
+
+ return node;
+ }
+
+ return { headers, rehypeCollectHeaders: () => (tree: any) => visit(tree, visitor) }
+}
diff --git a/packages/astro/src/compiler/markdown/remark-mdx-lite.ts b/packages/astro/src/compiler/markdown/remark-mdx-lite.ts
new file mode 100644
index 000000000..27eed917e
--- /dev/null
+++ b/packages/astro/src/compiler/markdown/remark-mdx-lite.ts
@@ -0,0 +1,26 @@
+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']} };
+
+/**
+ * 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.
+ */
+function mdxLite (this: any) {
+ let data = this.data()
+
+ add('micromarkExtensions', syntax);
+ add('fromMarkdownExtensions', fromMarkdown)
+ add('toMarkdownExtensions', toMarkdown)
+
+ /** Adds remark plugin */
+ function add(field: string, value: any) {
+ 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
new file mode 100644
index 000000000..9e2c8c290
--- /dev/null
+++ b/packages/astro/src/compiler/markdown/remark-scoped-styles.ts
@@ -0,0 +1,18 @@
+import { visit } from 'unist-util-visit';
+const noVisit = new Set(['root', 'html', 'text']);
+
+/** */
+export default function scopedStyles(className: string) {
+ const visitor = (node: any) => {
+ if (noVisit.has(node.type)) return;
+
+ const {data} = node
+ const currentClassName = data?.hProperties?.class ?? '';
+ node.data = node.data || {};
+ node.data.hProperties = node.data.hProperties || {};
+ node.data.hProperties.className = `${className} ${currentClassName}`.trim();
+
+ return node;
+ }
+ return () => (tree: any) => visit(tree, visitor);
+}
diff --git a/packages/astro/src/compiler/transform/styles.ts b/packages/astro/src/compiler/transform/styles.ts
index 89a8c9c7f..10d9158a0 100644
--- a/packages/astro/src/compiler/transform/styles.ts
+++ b/packages/astro/src/compiler/transform/styles.ts
@@ -156,6 +156,36 @@ async function transformStyle(code: string, { logging, type, filename, scopedCla
return { css, type: styleType };
}
+/** For a given node, inject or append a `scopedClass` to its `class` attribute */
+function injectScopedClassAttribute(node: TemplateNode, scopedClass: string, attribute = 'class') {
+ if (!node.attributes) node.attributes = [];
+ const classIndex = node.attributes.findIndex(({ name }: any) => name === attribute);
+ if (classIndex === -1) {
+ // 3a. element has no class="" attribute; add one and append scopedClass
+ node.attributes.push({ start: -1, end: -1, type: 'Attribute', name: attribute, value: [{ type: 'Text', raw: scopedClass, data: scopedClass }] });
+ } else {
+ // 3b. element has class=""; append scopedClass
+ const attr = node.attributes[classIndex];
+ for (let k = 0; k < attr.value.length; k++) {
+ if (attr.value[k].type === 'Text') {
+ // don‘t add same scopedClass twice
+ if (!hasClass(attr.value[k].data, scopedClass)) {
+ // string literal
+ attr.value[k].raw += ' ' + scopedClass;
+ attr.value[k].data += ' ' + scopedClass;
+ }
+ } else if (attr.value[k].type === 'MustacheTag' && attr.value[k]) {
+ // don‘t add same scopedClass twice (this check is a little more basic, but should suffice)
+ if (!attr.value[k].expression.codeChunks[0].includes(`' ${scopedClass}'`)) {
+ // MustacheTag
+ // FIXME: this won't work when JSX element can appear in attributes (rare but possible).
+ attr.value[k].expression.codeChunks[0] = `(${attr.value[k].expression.codeChunks[0]}) + ' ${scopedClass}'`;
+ }
+ }
+ }
+ }
+}
+
/** Transform <style> tags */
export default function transformStyles({ compileOptions, filename, fileID }: TransformOptions): Transformer {
const styleNodes: TemplateNode[] = []; // <style> tags to be updated
@@ -180,6 +210,12 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr
return {
visitors: {
html: {
+ InlineComponent: {
+ enter(node) {
+ if (node.name !== 'Markdown') return;
+ injectScopedClassAttribute(node, scopedClass, '$scope');
+ }
+ },
Element: {
enter(node) {
// 1. if <style> tag, transform it and continue to next node
@@ -204,32 +240,7 @@ export default function transformStyles({ compileOptions, filename, fileID }: Tr
if (NEVER_SCOPED_TAGS.has(node.name)) return; // only continue if this is NOT a <script> tag, etc.
// Note: currently we _do_ scope web components/custom elements. This seems correct?
- if (!node.attributes) node.attributes = [];
- const classIndex = node.attributes.findIndex(({ name }: any) => name === 'class');
- if (classIndex === -1) {
- // 3a. element has no class="" attribute; add one and append scopedClass
- node.attributes.push({ start: -1, end: -1, type: 'Attribute', name: 'class', value: [{ type: 'Text', raw: scopedClass, data: scopedClass }] });
- } else {
- // 3b. element has class=""; append scopedClass
- const attr = node.attributes[classIndex];
- for (let k = 0; k < attr.value.length; k++) {
- if (attr.value[k].type === 'Text') {
- // don‘t add same scopedClass twice
- if (!hasClass(attr.value[k].data, scopedClass)) {
- // string literal
- attr.value[k].raw += ' ' + scopedClass;
- attr.value[k].data += ' ' + scopedClass;
- }
- } else if (attr.value[k].type === 'MustacheTag' && attr.value[k]) {
- // don‘t add same scopedClass twice (this check is a little more basic, but should suffice)
- if (!attr.value[k].expression.codeChunks[0].includes(`' ${scopedClass}'`)) {
- // MustacheTag
- // FIXME: this won't work when JSX element can appear in attributes (rare but possible).
- attr.value[k].expression.codeChunks[0] = `(${attr.value[k].expression.codeChunks[0]}) + ' ${scopedClass}'`;
- }
- }
- }
- }
+ injectScopedClassAttribute(node, scopedClass);
},
},
},
diff --git a/packages/astro/src/compiler/utils.ts b/packages/astro/src/compiler/utils.ts
new file mode 100644
index 000000000..701dc2adf
--- /dev/null
+++ b/packages/astro/src/compiler/utils.ts
@@ -0,0 +1,70 @@
+import mdxLite from './markdown/remark-mdx-lite.js';
+import createCollectHeaders from './markdown/rehype-collect-headers.js';
+import scopedStyles from './markdown/remark-scoped-styles.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';
+
+export interface MarkdownRenderingOptions {
+ $?: {
+ scopedClassName: string | null;
+ };
+ footnotes?: boolean;
+ gfm?: boolean;
+ plugins?: any[];
+}
+
+/** 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);
+ 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 { headers, rehypeCollectHeaders } = createCollectHeaders();
+
+ let parser = unified().use(markdown).use(mdxLite).use(smartypants);
+
+ if (scopedClassName) {
+ parser = parser.use(scopedStyles(scopedClassName));
+ }
+
+ if (useGfm) {
+ const {default:gfm} = await import('remark-gfm');
+ parser = parser.use(gfm);
+ }
+
+ if (useFootnotes) {
+ const {default:footnotes} = await import('remark-footnotes');
+ parser = parser.use(footnotes);
+ }
+
+ let result: string;
+ try {
+ const vfile = await parser
+ .use(markdownToHtml, { allowDangerousHtml: true, passThrough: ['raw'] })
+ .use(raw)
+ .use(rehypeCollectHeaders)
+ .use(stringify)
+ .process(content);
+ result = vfile.contents.toString();
+ } catch (err) {
+ throw err;
+ }
+
+ return {
+ astro: { headers, source: content },
+ content: result.toString(),
+ };
+}
diff --git a/packages/astro/src/frontend/markdown.ts b/packages/astro/src/frontend/markdown.ts
new file mode 100644
index 000000000..8fb013d76
--- /dev/null
+++ b/packages/astro/src/frontend/markdown.ts
@@ -0,0 +1,26 @@
+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/frontend/render/renderer.ts b/packages/astro/src/frontend/render/renderer.ts
index 7bdf7d8a8..86d74fa84 100644
--- a/packages/astro/src/frontend/render/renderer.ts
+++ b/packages/astro/src/frontend/render/renderer.ts
@@ -36,17 +36,15 @@ export function createRenderer(renderer: SupportedComponentRenderer) {
}
value = `<div data-astro-id="${innerContext['data-astro-id']}" style="display:contents">${value}</div>`;
- const script = `
- ${typeof wrapperStart === 'function' ? wrapperStart(innerContext) : wrapperStart}
- ${_imports(renderContext)}
- ${renderer.render({
+ const script = `${typeof wrapperStart === 'function' ? wrapperStart(innerContext) : wrapperStart}
+${_imports(renderContext)}
+${renderer.render({
...innerContext,
props: serializeProps(props),
children: `[${childrenToH(renderer, children) ?? ''}]`,
childrenAsString: `\`${children}\``,
})}
- ${typeof wrapperEnd === 'function' ? wrapperEnd(innerContext) : wrapperEnd}
- `;
+${typeof wrapperEnd === 'function' ? wrapperEnd(innerContext) : wrapperEnd}`;
return [value, `<script type="module">${script.trim()}</script>`].join('\n');
};
diff --git a/packages/astro/src/frontend/render/utils.ts b/packages/astro/src/frontend/render/utils.ts
index 2dddf083e..29eaf64b5 100644
--- a/packages/astro/src/frontend/render/utils.ts
+++ b/packages/astro/src/frontend/render/utils.ts
@@ -3,12 +3,10 @@ import parse from 'rehype-parse';
import toH from 'hast-to-hyperscript';
import { ComponentRenderer } from '../../@types/renderer';
import moize from 'moize';
-// This prevents tree-shaking of render.
-Function.prototype(toH);
/** @internal */
-function childrenToTree(children: string[]) {
- return children.map((child) => (unified().use(parse, { fragment: true }).parse(child) as any).children.pop());
+function childrenToTree(children: string[]): any[] {
+ return [].concat(...children.map((child) => (unified().use(parse, { fragment: true }).parse(child) as any).children));
}
/**
@@ -32,17 +30,20 @@ export const childrenToVnodes = moize.deep(function childrenToVnodes(h: any, chi
*/
export const childrenToH = moize.deep(function childrenToH(renderer: ComponentRenderer<any>, children: string[]): any {
if (!renderer.jsxPragma) return;
+
const tree = childrenToTree(children);
const innerH = (name: any, attrs: Record<string, any> | null = null, _children: string[] | null = null) => {
const vnode = renderer.jsxPragma?.(name, attrs, _children);
const childStr = _children ? `, [${_children.map((child) => serializeChild(child)).join(',')}]` : '';
- /* fix(react): avoid hard-coding keys into the serialized tree */
- if (attrs && attrs.key) attrs.key = undefined;
+ if (attrs && attrs.key) attrs.key = Math.random();
const __SERIALIZED = `${renderer.jsxPragmaName}("${name}", ${attrs ? JSON.stringify(attrs) : 'null'}${childStr})` as string;
return { ...vnode, __SERIALIZED };
};
+
+ const simpleTypes = new Set(['number', 'boolean']);
const serializeChild = (child: unknown) => {
- if (['string', 'number', 'boolean'].includes(typeof child)) return JSON.stringify(child);
+ if (typeof child === 'string') return JSON.stringify(child).replace(/<\/script>/gmi, '</script" + ">');
+ if (simpleTypes.has(typeof child)) return JSON.stringify(child);
if (child === null) return `null`;
if ((child as any).__SERIALIZED) return (child as any).__SERIALIZED;
return innerH(child).__SERIALIZED;
diff --git a/packages/astro/src/runtime.ts b/packages/astro/src/runtime.ts
index 965ea641a..1eabbd364 100644
--- a/packages/astro/src/runtime.ts
+++ b/packages/astro/src/runtime.ts
@@ -314,7 +314,11 @@ 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'
+ ],
},
});
diff --git a/packages/astro/src/search.ts b/packages/astro/src/search.ts
index 20f600d31..84a2ee634 100644
--- a/packages/astro/src/search.ts
+++ b/packages/astro/src/search.ts
@@ -45,7 +45,7 @@ export function searchForPage(url: URL, astroRoot: URL): SearchResult {
// Try to find index.astro/md paths
if (reqPath.endsWith('/')) {
- const candidates = [`${base}index.astro`, `${base}index.md`];
+ const candidates = [`${base}index.astro`, `${base}index.md`,];
const location = findAnyPage(candidates, astroRoot);
if (location) {
return {