[markdown] Harder, better, faster, stronger vite-plugin-markdown (#4137)

* refactor: vite-plugin-md -> vite-plugin-md-legacy

* wip: add vite-plugin-md

* feat: always apply jsx renderer

* fix: markHTMLString on VNode result

* feat: apply new vite-plugin-markdown!

* fix: add meta export to md

* fix: remove needless $$metadata export

* fix: toggle to legacy plugin on flag

* fix: pass fileId to renderMarkdown

* test: raw and compiled content on plain md

* fix: escape vite env refs

* refactor: astro-md -> legacy-astro-flavored-md, astro-md-mode -> astro-markdown

* fix: import.meta.env refs with tests

* fix: add pkg.json to clientAddress

* fix: prefer JSX integration over Astro runtime

* Revert "fix: prefer JSX integration over Astro runtime"

This reverts commit 3e5fa49344be9c857393da9af095faab152e92e1.

* fix: remove .mdx check on importSource

* chore: changeset

* chore: remove TODO

* fix: add back getHeadings

* fix: add pkg.json to astro-head fixture

* fix: default to Astro renderer for MDX and MD

* feat: add "headings" and "frontmatter" to md layouts

* refactor: remove legacy flag conditionals from legacy plugin

* fix: add back MDX warning when legacy is off

* test: getHeadings() glob

* fix: add error on "astro.headings" access

* feat: update docs example astro.headings => headings

* refactor: readFile as string w/ utf-8

* chore: remove astro metadata TODO

* refactor: stringify HTML once

* fix: add pkg.json to glob-pages-css
This commit is contained in:
Ben Holmes 2022-08-05 14:43:50 -05:00 committed by GitHub
parent 838eb3e5cc
commit 471c6f784e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 831 additions and 490 deletions

View file

@ -0,0 +1,6 @@
---
'astro': minor
'@astrojs/markdown-remark': patch
---
Speed up internal markdown builds with new vite-plugin markdown

View file

@ -2,9 +2,8 @@
import MoreMenu from "../RightSidebar/MoreMenu.astro";
import TableOfContents from "../RightSidebar/TableOfContents";
const { content, githubEditUrl } = Astro.props;
const { content, headings, githubEditUrl } = Astro.props;
const title = content.title;
const headings = content.astro.headings;
---
<article id="article" class="content">
@ -29,9 +28,11 @@ const headings = content.astro.headings;
display: flex;
flex-direction: column;
}
.content > section {
.content>section {
margin-bottom: 4rem;
}
.block {
display: block;
}

View file

@ -1,8 +1,7 @@
---
import TableOfContents from "./TableOfContents";
import MoreMenu from "./MoreMenu.astro";
const { content, githubEditUrl } = Astro.props;
const headings = content.astro.headings;
const { content, headings, githubEditUrl } = Astro.props;
---
<nav class="sidebar-nav" aria-labelledby="grid-right">
@ -18,6 +17,7 @@ const headings = content.astro.headings;
position: sticky;
top: 0;
}
.sidebar-nav-inner {
height: 100%;
padding: 0;

View file

@ -48,6 +48,8 @@
"./vite-plugin-astro-postprocess/*": "./dist/vite-plugin-astro-postprocess/*",
"./vite-plugin-jsx/*": "./dist/vite-plugin-jsx/*",
"./vite-plugin-jsx": "./dist/vite-plugin-jsx/index.js",
"./vite-plugin-markdown-legacy": "./dist/vite-plugin-markdown-legacy/index.js",
"./vite-plugin-markdown-legacy/*": "./dist/vite-plugin-markdown-legacy/*",
"./vite-plugin-markdown": "./dist/vite-plugin-markdown/index.js",
"./vite-plugin-markdown/*": "./dist/vite-plugin-markdown/*",
"./dist/jsx/*": "./dist/jsx/*"

View file

@ -17,6 +17,7 @@ import { z } from 'zod';
import { LogOptions } from './logger/core.js';
import { appendForwardSlash, prependForwardSlash, trimSlashes } from './path.js';
import { arraify, isObject } from './util.js';
import jsxRenderer from '../jsx/renderer.js';
load.use([loadTypeScript]);
@ -343,16 +344,11 @@ export async function validateConfig(
_ctx: {
pageExtensions: ['.astro', '.md', '.html'],
scripts: [],
renderers: [],
renderers: [jsxRenderer],
injectedRoutes: [],
adapter: undefined,
},
};
if (result.integrations.find((integration) => integration.name === '@astrojs/mdx')) {
// Enable default JSX integration. It needs to come first, so unshift rather than push!
const { default: jsxRenderer } = await import('../jsx/renderer.js');
(result._ctx.renderers as any[]).unshift(jsxRenderer);
}
// If successful, return the result as a verified AstroConfig object.
return result;

View file

@ -12,6 +12,7 @@ import envVitePlugin from '../vite-plugin-env/index.js';
import htmlVitePlugin from '../vite-plugin-html/index.js';
import astroIntegrationsContainerPlugin from '../vite-plugin-integrations-container/index.js';
import jsxVitePlugin from '../vite-plugin-jsx/index.js';
import legacyMarkdownVitePlugin from '../vite-plugin-markdown-legacy/index.js';
import markdownVitePlugin from '../vite-plugin-markdown/index.js';
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.js';
@ -76,7 +77,9 @@ export async function createVite(
// the build to run very slow as the filewatcher is triggered often.
mode !== 'build' && astroViteServerPlugin({ config: astroConfig, logging }),
envVitePlugin({ config: astroConfig }),
markdownVitePlugin({ config: astroConfig, logging }),
astroConfig.legacy.astroFlavoredMarkdown
? legacyMarkdownVitePlugin({ config: astroConfig, logging })
: markdownVitePlugin({ config: astroConfig, logging }),
htmlVitePlugin(),
jsxVitePlugin({ config: astroConfig, logging }),
astroPostprocessVitePlugin({ config: astroConfig }),

View file

@ -137,9 +137,31 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
JSX_RENDERER_CACHE.set(config, jsxRenderers);
}
// Attempt: Single JSX renderer
const astroRenderer = jsxRenderers.get('astro');
// Shortcut: only use Astro renderer for MD and MDX files
if ((id.includes('.mdx') || id.includes('.md')) && astroRenderer) {
const { code: jsxCode } = await esbuild.transform(code, {
loader: getEsbuildLoader(path.extname(id)) as esbuild.Loader,
jsx: 'preserve',
sourcefile: id,
sourcemap: 'inline',
});
return transformJSX({
code: jsxCode,
id,
renderer: astroRenderer,
mode,
ssr,
});
}
// Attempt: Single JSX integration
// If we only have one renderer, we can skip a bunch of work!
if (jsxRenderers.size === 1) {
const nonAstroJsxRenderers = new Map(
[...jsxRenderers.entries()].filter(([key]) => key !== 'astro')
);
if (nonAstroJsxRenderers.size === 1) {
// downlevel any non-standard syntax, but preserve JSX
const { code: jsxCode } = await esbuild.transform(code, {
loader: getEsbuildLoader(path.extname(id)) as esbuild.Loader,
@ -150,7 +172,7 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
return transformJSX({
code: jsxCode,
id,
renderer: [...jsxRenderers.values()][0],
renderer: [...nonAstroJsxRenderers.values()][0],
mode,
ssr,
});
@ -196,10 +218,6 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
}
}
if (!importSource && jsxRenderers.has('astro') && id.includes('.mdx')) {
importSource = 'astro';
}
// if JSX renderer found, then use that
if (importSource) {
const jsxRenderer = jsxRenderers.get(importSource);

View file

@ -0,0 +1,3 @@
# vite-plugin-markdown-legacy
Adds Markdown support to Vite, both at the top level as well as within `.astro` files.

View file

@ -0,0 +1,260 @@
import { renderMarkdown } from '@astrojs/markdown-remark';
import ancestor from 'common-ancestor-path';
import esbuild from 'esbuild';
import fs from 'fs';
import matter from 'gray-matter';
import { fileURLToPath } from 'url';
import type { Plugin } from 'vite';
import type { AstroConfig } from '../@types/astro';
import { pagesVirtualModuleId } from '../core/app/index.js';
import { collectErrorMetadata } from '../core/errors.js';
import type { LogOptions } from '../core/logger/core.js';
import { cachedCompilation, CompileProps } from '../vite-plugin-astro/compile.js';
import { getViteTransform, TransformHook } from '../vite-plugin-astro/styles.js';
import type { PluginMetadata as AstroPluginMetadata } from '../vite-plugin-astro/types';
import { getFileInfo } from '../vite-plugin-utils/index.js';
interface AstroPluginOptions {
config: AstroConfig;
logging: LogOptions;
}
const MARKDOWN_IMPORT_FLAG = '?mdImport';
const MARKDOWN_CONTENT_FLAG = '?content';
function safeMatter(source: string, id: string) {
try {
return matter(source);
} catch (e) {
(e as any).id = id;
throw collectErrorMetadata(e);
}
}
// TODO: Clean up some of the shared logic between this Markdown plugin and the Astro plugin.
// Both end up connecting a `load()` hook to the Astro compiler, and share some copy-paste
// logic in how that is done.
export default function markdown({ config, logging }: AstroPluginOptions): Plugin {
function normalizeFilename(filename: string) {
if (filename.startsWith('/@fs')) {
filename = filename.slice('/@fs'.length);
} else if (filename.startsWith('/') && !ancestor(filename, config.root.pathname)) {
filename = new URL('.' + filename, config.root).pathname;
}
return filename;
}
// Weird Vite behavior: Vite seems to use a fake "index.html" importer when you
// have `enforce: pre`. This can probably be removed once the vite issue is fixed.
// see: https://github.com/vitejs/vite/issues/5981
const fakeRootImporter = fileURLToPath(new URL('index.html', config.root));
function isRootImport(importer: string | undefined) {
if (!importer) {
return true;
}
if (importer === fakeRootImporter) {
return true;
}
if (importer === '\0' + pagesVirtualModuleId) {
return true;
}
return false;
}
let viteTransform: TransformHook;
return {
name: 'astro:markdown',
enforce: 'pre',
configResolved(_resolvedConfig) {
viteTransform = getViteTransform(_resolvedConfig);
},
async resolveId(id, importer, options) {
// Resolve any .md files with the `?content` cache buster. This should only come from
// an already-resolved JS module wrapper. Needed to prevent infinite loops in Vite.
// Unclear if this is expected or if cache busting is just working around a Vite bug.
if (id.endsWith(`.md${MARKDOWN_CONTENT_FLAG}`)) {
const resolvedId = await this.resolve(id, importer, { skipSelf: true, ...options });
return resolvedId?.id.replace(MARKDOWN_CONTENT_FLAG, '');
}
// If the markdown file is imported from another file via ESM, resolve a JS representation
// that defers the markdown -> HTML rendering until it is needed. This is especially useful
// when fetching and then filtering many markdown files, like with import.meta.glob() or Astro.glob().
// Otherwise, resolve directly to the actual component.
if (id.endsWith('.md') && !isRootImport(importer)) {
const resolvedId = await this.resolve(id, importer, { skipSelf: true, ...options });
if (resolvedId) {
return resolvedId.id + MARKDOWN_IMPORT_FLAG;
}
}
// In all other cases, we do nothing and rely on normal Vite resolution.
return undefined;
},
async load(id, opts) {
// A markdown file has been imported via ESM!
// Return the file's JS representation, including all Markdown
// frontmatter and a deferred `import() of the compiled markdown content.
if (id.endsWith(`.md${MARKDOWN_IMPORT_FLAG}`)) {
const { fileId, fileUrl } = getFileInfo(id, config);
const source = await fs.promises.readFile(fileId, 'utf8');
const { data: frontmatter, content: rawContent } = safeMatter(source, fileId);
return {
code: `
// Static
export const frontmatter = ${escapeViteEnvReferences(JSON.stringify(frontmatter))};
export const file = ${JSON.stringify(fileId)};
export const url = ${JSON.stringify(fileUrl)};
export function rawContent() {
return ${escapeViteEnvReferences(JSON.stringify(rawContent))};
}
export async function compiledContent() {
return load().then((m) => m.compiledContent());
}
export function $$loadMetadata() {
return load().then((m) => m.$$metadata);
}
// Deferred
export default async function load() {
return (await import(${JSON.stringify(fileId + MARKDOWN_CONTENT_FLAG)}));
}
export function Content(...args) {
return load().then((m) => m.default(...args));
}
Content.isAstroComponentFactory = true;
export function getHeadings() {
return load().then((m) => m.metadata.headings);
}
export function getHeaders() {
console.warn('getHeaders() have been deprecated. Use getHeadings() function instead.');
return load().then((m) => m.metadata.headings);
};`,
map: null,
};
}
// A markdown file is being rendered! This markdown file was either imported
// directly as a page in Vite, or it was a deferred render from a JS module.
// This returns the compiled markdown -> astro component that renders to HTML.
if (id.endsWith('.md')) {
const filename = normalizeFilename(id);
const source = await fs.promises.readFile(filename, 'utf8');
const renderOpts = config.markdown;
const fileUrl = new URL(`file://${filename}`);
// Extract special frontmatter keys
let { data: frontmatter, content: markdownContent } = safeMatter(source, filename);
// Turn HTML comments into JS comments while preventing nested `*/` sequences
// from ending the JS comment by injecting a zero-width space
// Inside code blocks, this is removed during renderMarkdown by the remark-escape plugin.
markdownContent = markdownContent.replace(
/<\s*!--([^-->]*)(.*?)-->/gs,
(whole) => `{/*${whole.replace(/\*\//g, '*\u200b/')}*/}`
);
let renderResult = await renderMarkdown(markdownContent, {
...renderOpts,
fileURL: fileUrl,
isAstroFlavoredMd: true,
} as any);
let { code: astroResult, metadata } = renderResult;
const { layout = '', components = '', setup = '', ...content } = frontmatter;
content.astro = metadata;
content.url = getFileInfo(id, config).fileUrl;
content.file = filename;
const prelude = `---
import Slugger from 'github-slugger';
${layout ? `import Layout from '${layout}';` : ''}
${components ? `import * from '${components}';` : ''}
${setup}
const slugger = new Slugger();
function $$slug(value) {
return slugger.slug(value);
}
const $$content = ${JSON.stringify(content)};
Object.defineProperty($$content.astro, 'headers', {
get() {
console.warn('[${JSON.stringify(id)}] content.astro.headers is now content.astro.headings.');
return this.headings;
}
});
---`;
const imports = `${layout ? `import Layout from '${layout}';` : ''}
${setup}`.trim();
// If the user imported "Layout", wrap the content in a Layout
if (/\bLayout\b/.test(imports)) {
astroResult = `${prelude}\n<Layout content={$$content}>\n\n${astroResult}\n\n</Layout>`;
} else {
// Note: without a Layout, we need to inject `head` manually so `maybeRenderHead` runs
astroResult = `${prelude}\n<head></head>${astroResult}`;
}
// Transform from `.astro` to valid `.ts`
const compileProps: CompileProps = {
config,
filename,
moduleId: id,
source: astroResult,
ssr: Boolean(opts?.ssr),
viteTransform,
pluginContext: this,
};
let transformResult = await cachedCompilation(compileProps);
let { code: tsResult } = transformResult;
tsResult = `\nexport const metadata = ${JSON.stringify(metadata)};
export const frontmatter = ${JSON.stringify(content)};
export function rawContent() {
return ${JSON.stringify(markdownContent)};
}
export function compiledContent() {
return ${JSON.stringify(renderResult.metadata.html)};
}
${tsResult}`;
// Compile from `.ts` to `.js`
const { code } = await esbuild.transform(tsResult, {
loader: 'ts',
sourcemap: false,
sourcefile: id,
});
const astroMetadata: AstroPluginMetadata['astro'] = {
clientOnlyComponents: transformResult.clientOnlyComponents,
hydratedComponents: transformResult.hydratedComponents,
scripts: transformResult.scripts,
};
return {
code: escapeViteEnvReferences(code),
map: null,
meta: {
astro: astroMetadata,
vite: {
lang: 'ts',
},
},
};
}
return null;
},
};
}
// Converts the first dot in `import.meta.env` to its Unicode escape sequence,
// which prevents Vite from replacing strings like `import.meta.env.SITE`
// in our JS representation of loaded Markdown files
function escapeViteEnvReferences(code: string) {
return code.replace(/import\.meta\.env/g, 'import\\u002Emeta.env');
}

View file

@ -1,3 +1,3 @@
# vite-plugin-markdown
# vite-plugin-markdown-legacy
Adds Markdown support to Vite, both at the top level as well as within `.astro` files.

View file

@ -1,28 +1,19 @@
import { renderMarkdown } from '@astrojs/markdown-remark';
import ancestor from 'common-ancestor-path';
import esbuild from 'esbuild';
import fs from 'fs';
import matter from 'gray-matter';
import { fileURLToPath } from 'url';
import fs from 'fs';
import type { Plugin } from 'vite';
import type { AstroConfig } from '../@types/astro';
import { pagesVirtualModuleId } from '../core/app/index.js';
import { collectErrorMetadata } from '../core/errors.js';
import type { LogOptions } from '../core/logger/core.js';
import { warn } from '../core/logger/core.js';
import { cachedCompilation, CompileProps } from '../vite-plugin-astro/compile.js';
import { getViteTransform, TransformHook } from '../vite-plugin-astro/styles.js';
import type { PluginMetadata as AstroPluginMetadata } from '../vite-plugin-astro/types';
import type { PluginMetadata } from '../vite-plugin-astro/types.js';
import { getFileInfo } from '../vite-plugin-utils/index.js';
import { warn } from '../core/logger/core.js';
interface AstroPluginOptions {
config: AstroConfig;
logging: LogOptions;
}
const MARKDOWN_IMPORT_FLAG = '?mdImport';
const MARKDOWN_CONTENT_FLAG = '?content';
function safeMatter(source: string, id: string) {
try {
return matter(source);
@ -32,146 +23,31 @@ function safeMatter(source: string, id: string) {
}
}
// TODO: Clean up some of the shared logic between this Markdown plugin and the Astro plugin.
// Both end up connecting a `load()` hook to the Astro compiler, and share some copy-paste
// logic in how that is done.
export default function markdown({ config, logging }: AstroPluginOptions): Plugin {
function normalizeFilename(filename: string) {
if (filename.startsWith('/@fs')) {
filename = filename.slice('/@fs'.length);
} else if (filename.startsWith('/') && !ancestor(filename, config.root.pathname)) {
filename = new URL('.' + filename, config.root).pathname;
}
return filename;
}
// Weird Vite behavior: Vite seems to use a fake "index.html" importer when you
// have `enforce: pre`. This can probably be removed once the vite issue is fixed.
// see: https://github.com/vitejs/vite/issues/5981
const fakeRootImporter = fileURLToPath(new URL('index.html', config.root));
function isRootImport(importer: string | undefined) {
if (!importer) {
return true;
}
if (importer === fakeRootImporter) {
return true;
}
if (importer === '\0' + pagesVirtualModuleId) {
return true;
}
return false;
}
let viteTransform: TransformHook;
return {
name: 'astro:markdown',
enforce: 'pre',
configResolved(_resolvedConfig) {
viteTransform = getViteTransform(_resolvedConfig);
},
async resolveId(id, importer, options) {
// Resolve any .md files with the `?content` cache buster. This should only come from
// an already-resolved JS module wrapper. Needed to prevent infinite loops in Vite.
// Unclear if this is expected or if cache busting is just working around a Vite bug.
if (id.endsWith(`.md${MARKDOWN_CONTENT_FLAG}`)) {
const resolvedId = await this.resolve(id, importer, { skipSelf: true, ...options });
return resolvedId?.id.replace(MARKDOWN_CONTENT_FLAG, '');
}
// If the markdown file is imported from another file via ESM, resolve a JS representation
// that defers the markdown -> HTML rendering until it is needed. This is especially useful
// when fetching and then filtering many markdown files, like with import.meta.glob() or Astro.glob().
// Otherwise, resolve directly to the actual component.
if (id.endsWith('.md') && !isRootImport(importer)) {
const resolvedId = await this.resolve(id, importer, { skipSelf: true, ...options });
if (resolvedId) {
return resolvedId.id + MARKDOWN_IMPORT_FLAG;
}
}
// In all other cases, we do nothing and rely on normal Vite resolution.
return undefined;
},
async load(id, opts) {
// A markdown file has been imported via ESM!
// Return the file's JS representation, including all Markdown
// frontmatter and a deferred `import() of the compiled markdown content.
if (id.endsWith(`.md${MARKDOWN_IMPORT_FLAG}`)) {
const { fileId, fileUrl } = getFileInfo(id, config);
const source = await fs.promises.readFile(fileId, 'utf8');
const { data: frontmatter, content: rawContent } = safeMatter(source, fileId);
return {
code: `
// Static
export const frontmatter = ${escapeViteEnvReferences(JSON.stringify(frontmatter))};
export const file = ${JSON.stringify(fileId)};
export const url = ${JSON.stringify(fileUrl)};
export function rawContent() {
return ${escapeViteEnvReferences(JSON.stringify(rawContent))};
}
export async function compiledContent() {
return load().then((m) => m.compiledContent());
}
export function $$loadMetadata() {
return load().then((m) => m.$$metadata);
}
// Deferred
export default async function load() {
return (await import(${JSON.stringify(fileId + MARKDOWN_CONTENT_FLAG)}));
}
export function Content(...args) {
return load().then((m) => m.default(...args));
}
Content.isAstroComponentFactory = true;
export function getHeadings() {
return load().then((m) => m.metadata.headings);
}
export function getHeaders() {
console.warn('getHeaders() have been deprecated. Use getHeadings() function instead.');
return load().then((m) => m.metadata.headings);
};`,
map: null,
};
}
// A markdown file is being rendered! This markdown file was either imported
// directly as a page in Vite, or it was a deferred render from a JS module.
// This returns the compiled markdown -> astro component that renders to HTML.
name: 'astro:markdown',
// Why not the "transform" hook instead of "load" + readFile?
// A: Vite transforms all "import.meta.env" references to their values before
// passing to the transform hook. This lets us get the truly raw value
// to escape "import.meta.env" ourselves.
async load(id) {
if (id.endsWith('.md')) {
const filename = normalizeFilename(id);
const source = await fs.promises.readFile(filename, 'utf8');
const renderOpts = config.markdown;
const isAstroFlavoredMd = config.legacy.astroFlavoredMarkdown;
const fileUrl = new URL(`file://${filename}`);
// Extract special frontmatter keys
let { data: frontmatter, content: markdownContent } = safeMatter(source, filename);
// Turn HTML comments into JS comments while preventing nested `*/` sequences
// from ending the JS comment by injecting a zero-width space
// Inside code blocks, this is removed during renderMarkdown by the remark-escape plugin.
if (isAstroFlavoredMd) {
markdownContent = markdownContent.replace(
/<\s*!--([^-->]*)(.*?)-->/gs,
(whole) => `{/*${whole.replace(/\*\//g, '*\u200b/')}*/}`
);
}
let renderResult = await renderMarkdown(markdownContent, {
...renderOpts,
fileURL: fileUrl,
isAstroFlavoredMd,
const { fileId, fileUrl } = getFileInfo(id, config);
const rawFile = await fs.promises.readFile(fileId, 'utf-8');
const raw = safeMatter(rawFile, id);
const renderResult = await renderMarkdown(raw.content, {
...config.markdown,
fileURL: new URL(`file://${fileId}`),
isAstroFlavoredMd: false,
} as any);
let { code: astroResult, metadata } = renderResult;
const { layout = '', components = '', setup = '', ...content } = frontmatter;
content.astro = metadata;
content.url = getFileInfo(id, config).fileUrl;
content.file = filename;
// Warn when attempting to use setup without the legacy flag
if (setup && !isAstroFlavoredMd) {
const html = renderResult.code;
const { headings } = renderResult.metadata;
const frontmatter = { ...raw.data, url: fileUrl, file: fileId } as any;
const { layout } = frontmatter;
if (frontmatter.setup) {
warn(
logging,
'markdown',
@ -179,100 +55,59 @@ export default function markdown({ config, logging }: AstroPluginOptions): Plugi
);
}
const prelude = `---
import Slugger from 'github-slugger';
${layout ? `import Layout from '${layout}';` : ''}
${isAstroFlavoredMd && components ? `import * from '${components}';` : ''}
${isAstroFlavoredMd ? setup : ''}
const code = escapeViteEnvReferences(`
import { Fragment, jsx as h } from 'astro/jsx-runtime';
${layout ? `import Layout from ${JSON.stringify(layout)};` : ''}
const slugger = new Slugger();
function $$slug(value) {
return slugger.slug(value);
}
const html = ${JSON.stringify(html)};
const $$content = ${JSON.stringify(
isAstroFlavoredMd
? content
: // Avoid stripping "setup" and "components"
// in plain MD mode
{ ...content, setup, components }
)};
Object.defineProperty($$content.astro, 'headers', {
get() {
console.warn('[${JSON.stringify(id)}] content.astro.headers is now content.astro.headings.');
return this.headings;
}
});
---`;
const imports = `${layout ? `import Layout from '${layout}';` : ''}
${isAstroFlavoredMd ? setup : ''}`.trim();
// Wrap with set:html fragment to skip
// JSX expressions and components in "plain" md mode
if (!isAstroFlavoredMd) {
astroResult = `<Fragment set:html={${JSON.stringify(astroResult)}} />`;
export const frontmatter = ${JSON.stringify(frontmatter)};
export const file = ${JSON.stringify(fileId)};
export const url = ${JSON.stringify(fileUrl)};
export function rawContent() {
return ${JSON.stringify(raw.content)};
}
// If the user imported "Layout", wrap the content in a Layout
if (/\bLayout\b/.test(imports)) {
astroResult = `${prelude}\n<Layout content={$$content}>\n\n${astroResult}\n\n</Layout>`;
} else {
// Note: without a Layout, we need to inject `head` manually so `maybeRenderHead` runs
astroResult = `${prelude}\n<head></head>${astroResult}`;
export function compiledContent() {
return html;
}
// Transform from `.astro` to valid `.ts`
const compileProps: CompileProps = {
config,
filename,
moduleId: id,
source: astroResult,
ssr: Boolean(opts?.ssr),
viteTransform,
pluginContext: this,
export function getHeadings() {
return ${JSON.stringify(headings)};
}
export function getHeaders() {
console.warn('getHeaders() have been deprecated. Use getHeadings() function instead.');
return getHeadings();
};
let transformResult = await cachedCompilation(compileProps);
let { code: tsResult } = transformResult;
tsResult = `\nexport const metadata = ${JSON.stringify(metadata)};
export const frontmatter = ${JSON.stringify(content)};
export function rawContent() {
return ${JSON.stringify(markdownContent)};
}
export function compiledContent() {
return ${JSON.stringify(renderResult.metadata.html)};
}
${tsResult}`;
// Compile from `.ts` to `.js`
const { code } = await esbuild.transform(tsResult, {
loader: 'ts',
sourcemap: false,
sourcefile: id,
});
const astroMetadata: AstroPluginMetadata['astro'] = {
clientOnlyComponents: transformResult.clientOnlyComponents,
hydratedComponents: transformResult.hydratedComponents,
scripts: transformResult.scripts,
};
export async function Content() {
const { layout, ...content } = frontmatter;
content.astro = {};
Object.defineProperty(content.astro, 'headings', {
get() {
throw new Error('The "astro" property is no longer supported! To access "headings" from your layout, try using "Astro.props.headings."')
}
});
const contentFragment = h(Fragment, { 'set:html': html });
return ${
layout
? `h(Layout, { content, frontmatter: content, headings: getHeadings(), 'server:root': true, children: contentFragment })`
: `contentFragment`
};
}
export default Content;
`);
return {
code: escapeViteEnvReferences(code),
map: null,
code,
meta: {
astro: astroMetadata,
astro: {
hydratedComponents: [],
clientOnlyComponents: [],
scripts: [],
} as PluginMetadata['astro'],
vite: {
lang: 'ts',
},
},
};
}
return null;
},
};
}

View file

@ -1,51 +0,0 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';
describe('Astro Markdown - plain MD mode', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/astro-markdown-md-mode/',
});
await fixture.build();
});
it('Leaves JSX expressions unprocessed', async () => {
const html = await fixture.readFile('/jsx-expressions/index.html');
const $ = cheerio.load(html);
expect($('h2').html()).to.equal('{frontmatter.title}');
});
it('Leaves JSX components un-transformed', async () => {
const html = await fixture.readFile('/components/index.html');
expect(html).to.include('<counter client:load="" count="{0}">');
});
describe('syntax highlighting', async () => {
it('handles Shiki', async () => {
const html = await fixture.readFile('/code-in-md/index.html');
const $ = cheerio.load(html);
expect($('pre.astro-code').length).to.not.equal(0);
});
it('handles Prism', async () => {
fixture = await loadFixture({
root: './fixtures/astro-markdown-md-mode/',
markdown: {
syntaxHighlight: 'prism',
},
});
await fixture.build();
const html = await fixture.readFile('/code-in-md/index.html');
const $ = cheerio.load(html);
expect($('pre.language-html').length).to.not.equal(0);
});
});
});

View file

@ -2,199 +2,130 @@ import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture, fixLineEndings } from './test-utils.js';
const FIXTURE_ROOT = './fixtures/astro-markdown/';
describe('Astro Markdown', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/astro-markdown/',
root: FIXTURE_ROOT,
});
await fixture.build();
});
it('Can parse JSX expressions in markdown pages', async () => {
it('Leaves JSX expressions unprocessed', async () => {
const html = await fixture.readFile('/jsx-expressions/index.html');
const $ = cheerio.load(html);
expect($('h2').html()).to.equal('Blog Post with JSX expressions');
expect(html).to.contain('JSX at the start of the line!');
for (let listItem of ['test-1', 'test-2', 'test-3']) {
expect($(`#${listItem}`).html()).to.equal(`${listItem}`);
}
expect($('h2').html()).to.equal('{frontmatter.title}');
});
it('Can handle slugs with JSX expressions in markdown pages', async () => {
const html = await fixture.readFile('/slug/index.html');
const $ = cheerio.load(html);
it('Leaves JSX components un-transformed', async () => {
const html = await fixture.readFile('/components/index.html');
expect($('h1').attr('id')).to.equal('my-blog-post');
});
it('Can handle code elements without extra spacing', async () => {
const html = await fixture.readFile('/code-element/index.html');
const $ = cheerio.load(html);
$('code').each((_, el) => {
expect($(el).html()).to.equal($(el).html().trim());
});
});
it('Can handle namespaced components in markdown', async () => {
const html = await fixture.readFile('/namespace/index.html');
const $ = cheerio.load(html);
expect($('h1').text()).to.equal('Hello Namespace!');
expect($('button').length).to.equal(1);
});
it('Correctly handles component children in markdown pages (#3319)', async () => {
const html = await fixture.readFile('/children/index.html');
expect(html).not.to.contain('<p></p>');
});
it('Can handle HTML comments in markdown pages', async () => {
const html = await fixture.readFile('/comment/index.html');
const $ = cheerio.load(html);
expect($('h1').text()).to.equal('It works!');
});
it('Prevents `*/` sequences from breaking HTML comments (#3476)', async () => {
const html = await fixture.readFile('/comment-with-js/index.html');
const $ = cheerio.load(html);
expect($('h1').text()).to.equal('It still works!');
});
it('Can handle HTML comments in inline code', async () => {
const html = await fixture.readFile('/comment-with-js/index.html');
const $ = cheerio.load(html);
expect($('p code').text()).to.equal('<!-- HTML comments in code -->');
});
it('Can handle HTML comments in code fences', async () => {
const html = await fixture.readFile('/comment-with-js/index.html');
const $ = cheerio.load(html);
expect($('pre > code').text()).to.equal('<!-- HTML comments in code fence -->');
});
// https://github.com/withastro/astro/issues/3254
it('Can handle scripts in markdown pages', async () => {
const html = await fixture.readFile('/script/index.html');
expect(html).not.to.match(new RegExp('/src/scripts/test.js'));
});
it('Empty code blocks do not fail', async () => {
const html = await fixture.readFile('/empty-code/index.html');
const $ = cheerio.load(html);
// test 1: There is not a `<code>` in the codeblock
expect($('pre')[0].children).to.have.lengthOf(1);
// test 2: The empty `<pre>` failed to render
expect($('pre')[1].children).to.have.lengthOf(0);
});
it('Can render markdown with --- for horizontal rule', async () => {
const html = await fixture.readFile('/dash/index.html');
expect(!!html).to.equal(true);
expect(html).to.include('<counter client:load="" count="{0}">');
});
it('Exposes raw markdown content', async () => {
const { raw } = JSON.parse(await fixture.readFile('/raw-content.json'));
expect(fixLineEndings(raw)).to.equal(
`\n## With components\n\n### Non-hydrated\n\n<Hello name="Astro Naut" />\n\n### Hydrated\n\n<Counter client:load />\n<SvelteButton client:load />\n`
expect(fixLineEndings(raw).trim()).to.equal(
`# Basic page\n\nLets make sure raw and compiled content look right!`
);
});
it('Exposes HTML parser for raw markdown content', async () => {
it('Exposes compiled HTML content', async () => {
const { compiled } = JSON.parse(await fixture.readFile('/raw-content.json'));
expect(fixLineEndings(compiled)).to.equal(
`<h2 id="with-components">With components</h2>\n<h3 id="non-hydrated">Non-hydrated</h3>\n<Hello name="Astro Naut" />\n<h3 id="hydrated">Hydrated</h3>\n<Counter client:load />\n<SvelteButton client:load />`
expect(fixLineEndings(compiled).trim()).to.equal(
`<h1 id="basic-page">Basic page</h1>\n<p>Lets make sure raw and compiled content look right!</p>`
);
});
it('Allows referencing Vite env var names in markdown (#3412)', async () => {
const html = await fixture.readFile('/vite-env-vars/index.html');
const $ = cheerio.load(html);
describe('syntax highlighting', async () => {
it('handles Shiki', async () => {
const html = await fixture.readFile('/code-in-md/index.html');
const $ = cheerio.load(html);
// test 1: referencing an existing var name
expect($('code').eq(0).text()).to.equal('import.meta.env.SITE');
expect($('li').eq(0).text()).to.equal('import.meta.env.SITE');
expect($('code').eq(3).text()).to.contain('site: import.meta.env.SITE');
expect($('blockquote').text()).to.contain('import.meta.env.SITE');
expect($('pre.astro-code').length).to.not.equal(0);
});
// test 2: referencing a non-existing var name
expect($('code').eq(1).text()).to.equal('import.meta.env.TITLE');
expect($('li').eq(1).text()).to.equal('import.meta.env.TITLE');
expect($('code').eq(3).text()).to.contain('title: import.meta.env.TITLE');
expect($('blockquote').text()).to.contain('import.meta.env.TITLE');
it('handles Prism', async () => {
const prismFixture = await loadFixture({
root: FIXTURE_ROOT,
markdown: {
syntaxHighlight: 'prism',
},
});
await prismFixture.build();
// test 3: referencing `import.meta.env` itself (without any var name)
expect($('code').eq(2).text()).to.equal('import.meta.env');
expect($('li').eq(2).text()).to.equal('import.meta.env');
expect($('code').eq(3).text()).to.contain('// Use Vite env vars with import.meta.env');
expect($('blockquote').text()).to.match(/import\.meta\.env\s*$/);
const html = await prismFixture.readFile('/code-in-md/index.html');
const $ = cheerio.load(html);
expect($('pre.language-html').length).to.not.equal(0);
});
});
it('Escapes HTML tags in code blocks', async () => {
const html = await fixture.readFile('/code-in-md/index.html');
it('Passes frontmatter to layout via "content" and "frontmatter" props', async () => {
const html = await fixture.readFile('/with-layout/index.html');
const $ = cheerio.load(html);
expect($('code').eq(0).html()).to.equal('&lt;script&gt;');
expect($('blockquote').length).to.equal(1);
expect($('code').eq(1).html()).to.equal('&lt;/script&gt;');
expect($('pre').html()).to.contain('&gt;This should also work without any problems.&lt;');
const contentTitle = $('[data-content-title]');
const frontmatterTitle = $('[data-frontmatter-title]');
expect(contentTitle.text()).to.equal('With layout');
expect(frontmatterTitle.text()).to.equal('With layout');
});
it('Allows defining slot contents in component children', async () => {
const html = await fixture.readFile('/slots/index.html');
it('Passes headings to layout via "headings" prop', async () => {
const html = await fixture.readFile('/with-layout/index.html');
const $ = cheerio.load(html);
const slots = $('article').eq(0);
expect(slots.find('> .fragmentSlot > div').text()).to.contain('1:');
expect(slots.find('> .fragmentSlot > div + p').text()).to.contain('2:');
expect(slots.find('> .pSlot > p[title="hello"]').text()).to.contain('3:');
expect(slots.find('> .defaultSlot').html()).to.match(
new RegExp(
`<div>4: Div in default slot</div>` +
// Optional extra paragraph due to the line breaks between components
`(<p></p>)?` +
`<p>5: Paragraph in fragment in default slot</p>` +
// Optional whitespace due to the line breaks between components
`[\s\n]*` +
`6: Regular text in default slot`
)
const headingSlugs = [...$('body').find('[data-headings] > li')].map(
(el) => $(el).text()
);
const nestedSlots = $('article').eq(1);
expect(nestedSlots.find('> .fragmentSlot').html()).to.contain('1:');
expect(nestedSlots.find('> .pSlot > p').text()).to.contain('2:');
expect(nestedSlots.find('> .defaultSlot > article').text().replace(/\s+/g, ' ')).to.equal(
`
3: nested fragmentSlot
4: nested pSlot
5: nested text in default slot
`.replace(/\s+/g, ' ')
);
expect($('article').eq(3).text().replace(/[^❌]/g, '')).to.equal('❌❌❌');
expect($('article').eq(4).text().replace(/[^❌]/g, '')).to.equal('❌❌❌');
expect(headingSlugs.length).to.be.greaterThan(0);
expect(headingSlugs).to.contain('section-1');
expect(headingSlugs).to.contain('section-2');
});
it('Generate the right props for the layout', async () => {
const html = await fixture.readFile('/layout-props/index.html');
const $ = cheerio.load(html);
it('Exposes getHeadings() on glob imports', async () => {
const { headings } = JSON.parse(await fixture.readFile('/headings-glob.json'));
expect($('#title').text()).to.equal('Hello world!');
expect($('#url').text()).to.equal('/layout-props');
expect($('#file').text()).to.match(/.*\/layout-props.md$/);
const headingSlugs = headings.map(heading => heading?.slug);
expect(headingSlugs).to.contain('section-1');
expect(headingSlugs).to.contain('section-2');
});
describe('Vite env vars (#3412)', () => {
it('Allows referencing import.meta.env in content', async () => {
const html = await fixture.readFile('/vite-env-vars/index.html');
const $ = cheerio.load(html);
// test 1: referencing an existing var name
expect($('code').eq(0).text()).to.equal('import.meta.env.SITE');
expect($('li').eq(0).text()).to.equal('import.meta.env.SITE');
expect($('code').eq(3).text()).to.contain('site: import.meta.env.SITE');
// // test 2: referencing a non-existing var name
expect($('code').eq(1).text()).to.equal('import.meta.env.TITLE');
expect($('li').eq(1).text()).to.equal('import.meta.env.TITLE');
expect($('code').eq(3).text()).to.contain('title: import.meta.env.TITLE');
// // test 3: referencing `import.meta.env` itself (without any var name)
expect($('code').eq(2).text()).to.equal('import.meta.env');
expect($('li').eq(2).text()).to.equal('import.meta.env');
expect($('code').eq(3).text()).to.contain('// Use Vite env vars with import.meta.env');
});
it('Allows referencing import.meta.env in frontmatter', async () => {
const { title = '' } = JSON.parse(await fixture.readFile('/vite-env-vars-glob.json'));
expect(title).to.contain('import.meta.env.SITE');
expect(title).to.contain('import.meta.env.TITLE');
});
});
});

View file

@ -0,0 +1,8 @@
{
"name": "@test/astro-head",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -1,7 +0,0 @@
# Fenced code blocks
```html
<body>
<div>This should also work without any problems.</div>
</body>
```

View file

@ -1,12 +1,8 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
import svelte from "@astrojs/svelte";
// https://astro.build/config
export default defineConfig({
integrations: [preact(), svelte()],
integrations: [svelte()],
site: 'https://astro.build/',
legacy: {
astroFlavoredMarkdown: true,
}
});

View file

@ -1,9 +1,8 @@
{
"name": "@test/astro-markdown",
"name": "@test/astro-markdown-md-mode",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/preact": "workspace:*",
"@astrojs/svelte": "workspace:*",
"astro": "workspace:*"
}

View file

@ -0,0 +1,28 @@
---
const {
content = { title: "content didn't work" },
frontmatter = { title: "frontmatter didn't work" },
headings = [],
} = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<p data-content-title>{content.title}</p>
<p data-frontmatter-title>{frontmatter.title}</p>
<p data-layout-rendered>Layout rendered!</p>
<ul data-headings>
{headings.map(heading => <li>{heading.slug}</li>)}
</ul>
<slot />
</body>
</html>

View file

@ -0,0 +1,3 @@
# Basic page
Lets make sure raw and compiled content look right!

View file

@ -1,12 +1,3 @@
# Inline code blocks
`<script>` tags in **Astro** components are now built,
bundled and optimized by default.
> Markdown formatting still works between tags in inline code blocks.
We can also use closing `</script>` tags without any problems.
# Fenced code blocks
```html

View file

@ -0,0 +1,9 @@
import { getHeadings } from './with-layout.md';
export async function get() {
return {
body: JSON.stringify({
headings: getHeadings(),
}),
}
}

View file

@ -1,4 +1,4 @@
import { rawContent, compiledContent } from '../imported-md/with-components.md';
import { rawContent, compiledContent } from './basic.md';
export async function get() {
return {

View file

@ -0,0 +1,7 @@
import { frontmatter } from './vite-env-vars.md';
export async function get() {
return {
body: JSON.stringify(frontmatter),
}
}

View file

@ -1,6 +1,5 @@
---
title: Referencing Vite Env Vars like import.meta.env.SITE, import.meta.env.TITLE and import.meta.env
layout: ../layouts/content.astro
---
## Referencing the full name of Vite env vars
@ -29,7 +28,3 @@ export const get = () => rss({
items: import.meta.glob('./**/*.md'),
});
```
## Usage in frontmatter
> frontmatter.title: {frontmatter.title}

View file

@ -0,0 +1,8 @@
---
title: 'With layout'
layout: '../layouts/Base.astro'
---
## Section 1
## Section 2

View file

@ -0,0 +1,8 @@
{
"name": "@test/client-address",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -0,0 +1,8 @@
{
"name": "@test/glob-pages-css",
"version": "0.0.0",
"private": true,
"dependencies": {
"astro": "workspace:*"
}
}

View file

@ -1,8 +1,12 @@
import { defineConfig } from 'astro/config';
import preact from '@astrojs/preact';
import svelte from "@astrojs/svelte";
// https://astro.build/config
export default defineConfig({
integrations: [svelte()],
integrations: [preact(), svelte()],
site: 'https://astro.build/',
legacy: {
astroFlavoredMarkdown: true,
}
});

View file

@ -1,8 +1,9 @@
{
"name": "@test/astro-markdown-md-mode",
"name": "@test/astro-markdown",
"version": "0.0.0",
"private": true,
"dependencies": {
"@astrojs/preact": "workspace:*",
"@astrojs/svelte": "workspace:*",
"astro": "workspace:*"
}

View file

@ -0,0 +1,16 @@
# Inline code blocks
`<script>` tags in **Astro** components are now built,
bundled and optimized by default.
> Markdown formatting still works between tags in inline code blocks.
We can also use closing `</script>` tags without any problems.
# Fenced code blocks
```html
<body>
<div>This should also work without any problems.</div>
</body>
```

View file

@ -0,0 +1,10 @@
import { rawContent, compiledContent } from '../imported-md/with-components.md';
export async function get() {
return {
body: JSON.stringify({
raw: rawContent(),
compiled: await compiledContent(),
}),
}
}

View file

@ -0,0 +1,35 @@
---
title: Referencing Vite Env Vars like import.meta.env.SITE, import.meta.env.TITLE and import.meta.env
layout: ../layouts/content.astro
---
## Referencing the full name of Vite env vars
You can get the configured site URL with `import.meta.env.SITE`.
The variable `import.meta.env.TITLE` is not configured.
You can reference all env vars through `import.meta.env`.
This should also work outside of code blocks:
- import.meta.env.SITE
- import.meta.env.TITLE
- import.meta.env
## Usage in fenced code blocks with syntax highlighting
```js
// src/pages/rss.xml.js
import rss from '@astrojs/rss';
export const get = () => rss({
// Use Vite env vars with import.meta.env
site: import.meta.env.SITE,
title: import.meta.env.TITLE,
items: import.meta.glob('./**/*.md'),
});
```
## Usage in frontmatter
> frontmatter.title: {frontmatter.title}

View file

@ -0,0 +1,200 @@
import { expect } from 'chai';
import * as cheerio from 'cheerio';
import { loadFixture, fixLineEndings } from './test-utils.js';
describe('Legacy Astro-flavored Markdown', () => {
let fixture;
before(async () => {
fixture = await loadFixture({
root: './fixtures/legacy-astro-flavored-markdown/',
});
await fixture.build();
});
it('Can parse JSX expressions in markdown pages', async () => {
const html = await fixture.readFile('/jsx-expressions/index.html');
const $ = cheerio.load(html);
expect($('h2').html()).to.equal('Blog Post with JSX expressions');
expect(html).to.contain('JSX at the start of the line!');
for (let listItem of ['test-1', 'test-2', 'test-3']) {
expect($(`#${listItem}`).html()).to.equal(`${listItem}`);
}
});
it('Can handle slugs with JSX expressions in markdown pages', async () => {
const html = await fixture.readFile('/slug/index.html');
const $ = cheerio.load(html);
expect($('h1').attr('id')).to.equal('my-blog-post');
});
it('Can handle code elements without extra spacing', async () => {
const html = await fixture.readFile('/code-element/index.html');
const $ = cheerio.load(html);
$('code').each((_, el) => {
expect($(el).html()).to.equal($(el).html().trim());
});
});
it('Can handle namespaced components in markdown', async () => {
const html = await fixture.readFile('/namespace/index.html');
const $ = cheerio.load(html);
expect($('h1').text()).to.equal('Hello Namespace!');
expect($('button').length).to.equal(1);
});
it('Correctly handles component children in markdown pages (#3319)', async () => {
const html = await fixture.readFile('/children/index.html');
expect(html).not.to.contain('<p></p>');
});
it('Can handle HTML comments in markdown pages', async () => {
const html = await fixture.readFile('/comment/index.html');
const $ = cheerio.load(html);
expect($('h1').text()).to.equal('It works!');
});
it('Prevents `*/` sequences from breaking HTML comments (#3476)', async () => {
const html = await fixture.readFile('/comment-with-js/index.html');
const $ = cheerio.load(html);
expect($('h1').text()).to.equal('It still works!');
});
it('Can handle HTML comments in inline code', async () => {
const html = await fixture.readFile('/comment-with-js/index.html');
const $ = cheerio.load(html);
expect($('p code').text()).to.equal('<!-- HTML comments in code -->');
});
it('Can handle HTML comments in code fences', async () => {
const html = await fixture.readFile('/comment-with-js/index.html');
const $ = cheerio.load(html);
expect($('pre > code').text()).to.equal('<!-- HTML comments in code fence -->');
});
// https://github.com/withastro/astro/issues/3254
it('Can handle scripts in markdown pages', async () => {
const html = await fixture.readFile('/script/index.html');
expect(html).not.to.match(new RegExp('/src/scripts/test.js'));
});
it('Empty code blocks do not fail', async () => {
const html = await fixture.readFile('/empty-code/index.html');
const $ = cheerio.load(html);
// test 1: There is not a `<code>` in the codeblock
expect($('pre')[0].children).to.have.lengthOf(1);
// test 2: The empty `<pre>` failed to render
expect($('pre')[1].children).to.have.lengthOf(0);
});
it('Can render markdown with --- for horizontal rule', async () => {
const html = await fixture.readFile('/dash/index.html');
expect(!!html).to.equal(true);
});
it('Exposes raw markdown content', async () => {
const { raw } = JSON.parse(await fixture.readFile('/raw-content.json'));
expect(fixLineEndings(raw)).to.equal(
`\n## With components\n\n### Non-hydrated\n\n<Hello name="Astro Naut" />\n\n### Hydrated\n\n<Counter client:load />\n<SvelteButton client:load />\n`
);
});
it('Exposes compiled HTML content', async () => {
const { compiled } = JSON.parse(await fixture.readFile('/raw-content.json'));
expect(fixLineEndings(compiled)).to.equal(
`<h2 id="with-components">With components</h2>\n<h3 id="non-hydrated">Non-hydrated</h3>\n<Hello name="Astro Naut" />\n<h3 id="hydrated">Hydrated</h3>\n<Counter client:load />\n<SvelteButton client:load />`
);
});
it('Allows referencing Vite env var names in markdown (#3412)', async () => {
const html = await fixture.readFile('/vite-env-vars/index.html');
const $ = cheerio.load(html);
// test 1: referencing an existing var name
expect($('code').eq(0).text()).to.equal('import.meta.env.SITE');
expect($('li').eq(0).text()).to.equal('import.meta.env.SITE');
expect($('code').eq(3).text()).to.contain('site: import.meta.env.SITE');
expect($('blockquote').text()).to.contain('import.meta.env.SITE');
// test 2: referencing a non-existing var name
expect($('code').eq(1).text()).to.equal('import.meta.env.TITLE');
expect($('li').eq(1).text()).to.equal('import.meta.env.TITLE');
expect($('code').eq(3).text()).to.contain('title: import.meta.env.TITLE');
expect($('blockquote').text()).to.contain('import.meta.env.TITLE');
// test 3: referencing `import.meta.env` itself (without any var name)
expect($('code').eq(2).text()).to.equal('import.meta.env');
expect($('li').eq(2).text()).to.equal('import.meta.env');
expect($('code').eq(3).text()).to.contain('// Use Vite env vars with import.meta.env');
expect($('blockquote').text()).to.match(/import\.meta\.env\s*$/);
});
it('Escapes HTML tags in code blocks', async () => {
const html = await fixture.readFile('/code-in-md/index.html');
const $ = cheerio.load(html);
expect($('code').eq(0).html()).to.equal('&lt;script&gt;');
expect($('blockquote').length).to.equal(1);
expect($('code').eq(1).html()).to.equal('&lt;/script&gt;');
expect($('pre').html()).to.contain('&gt;This should also work without any problems.&lt;');
});
it('Allows defining slot contents in component children', async () => {
const html = await fixture.readFile('/slots/index.html');
const $ = cheerio.load(html);
const slots = $('article').eq(0);
expect(slots.find('> .fragmentSlot > div').text()).to.contain('1:');
expect(slots.find('> .fragmentSlot > div + p').text()).to.contain('2:');
expect(slots.find('> .pSlot > p[title="hello"]').text()).to.contain('3:');
expect(slots.find('> .defaultSlot').html()).to.match(
new RegExp(
`<div>4: Div in default slot</div>` +
// Optional extra paragraph due to the line breaks between components
`(<p></p>)?` +
`<p>5: Paragraph in fragment in default slot</p>` +
// Optional whitespace due to the line breaks between components
`[\s\n]*` +
`6: Regular text in default slot`
)
);
const nestedSlots = $('article').eq(1);
expect(nestedSlots.find('> .fragmentSlot').html()).to.contain('1:');
expect(nestedSlots.find('> .pSlot > p').text()).to.contain('2:');
expect(nestedSlots.find('> .defaultSlot > article').text().replace(/\s+/g, ' ')).to.equal(
`
3: nested fragmentSlot
4: nested pSlot
5: nested text in default slot
`.replace(/\s+/g, ' ')
);
expect($('article').eq(3).text().replace(/[^❌]/g, '')).to.equal('❌❌❌');
expect($('article').eq(4).text().replace(/[^❌]/g, '')).to.equal('❌❌❌');
});
it('Generate the right props for the layout', async () => {
const html = await fixture.readFile('/layout-props/index.html');
const $ = cheerio.load(html);
expect($('#title').text()).to.equal('Hello world!');
expect($('#url').text()).to.equal('/layout-props');
expect($('#file').text()).to.match(/.*\/layout-props.md$/);
});
});

View file

@ -2,7 +2,7 @@ import type { Literal } from 'unist';
import { visit } from 'unist-util-visit';
// In code blocks, this removes the JS comment wrapper added to
// HTML comments by vite-plugin-markdown.
// HTML comments by vite-plugin-markdown-legacy.
export default function remarkEscape() {
return (tree: any) => {
visit(tree, 'code', removeCommentWrapper);

View file

@ -1199,6 +1199,12 @@ importers:
dependencies:
astro: link:../../..
packages/astro/test/fixtures/astro-head:
specifiers:
astro: workspace:*
dependencies:
astro: link:../../..
packages/astro/test/fixtures/astro-jsx:
specifiers:
astro: workspace:*
@ -1207,11 +1213,9 @@ importers:
packages/astro/test/fixtures/astro-markdown:
specifiers:
'@astrojs/preact': workspace:*
'@astrojs/svelte': workspace:*
astro: workspace:*
dependencies:
'@astrojs/preact': link:../../../../integrations/preact
'@astrojs/svelte': link:../../../../integrations/svelte
astro: link:../../..
@ -1227,14 +1231,6 @@ importers:
dependencies:
astro: link:../../..
packages/astro/test/fixtures/astro-markdown-md-mode:
specifiers:
'@astrojs/svelte': workspace:*
astro: workspace:*
dependencies:
'@astrojs/svelte': link:../../../../integrations/svelte
astro: link:../../..
packages/astro/test/fixtures/astro-markdown-plugins:
specifiers:
'@astrojs/preact': workspace:*
@ -1349,6 +1345,12 @@ importers:
dependencies:
astro: link:../../..
packages/astro/test/fixtures/client-address:
specifiers:
astro: workspace:*
dependencies:
astro: link:../../..
packages/astro/test/fixtures/code-component:
specifiers:
astro: workspace:*
@ -1479,6 +1481,12 @@ importers:
'@fontsource/montserrat': 4.5.11
astro: link:../../..
packages/astro/test/fixtures/glob-pages-css:
specifiers:
astro: workspace:*
dependencies:
astro: link:../../..
packages/astro/test/fixtures/hmr-css:
specifiers:
astro: workspace:*
@ -1566,6 +1574,16 @@ importers:
'@astrojs/solid-js': link:../../../../integrations/solid
astro: link:../../..
packages/astro/test/fixtures/legacy-astro-flavored-markdown:
specifiers:
'@astrojs/preact': workspace:*
'@astrojs/svelte': workspace:*
astro: workspace:*
dependencies:
'@astrojs/preact': link:../../../../integrations/preact
'@astrojs/svelte': link:../../../../integrations/svelte
astro: link:../../..
packages/astro/test/fixtures/legacy-build:
specifiers:
'@astrojs/vue': workspace:*