[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:
parent
838eb3e5cc
commit
471c6f784e
63 changed files with 831 additions and 490 deletions
6
.changeset/hip-dancers-move.md
Normal file
6
.changeset/hip-dancers-move.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
---
|
||||||
|
'astro': minor
|
||||||
|
'@astrojs/markdown-remark': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Speed up internal markdown builds with new vite-plugin markdown
|
|
@ -2,9 +2,8 @@
|
||||||
import MoreMenu from "../RightSidebar/MoreMenu.astro";
|
import MoreMenu from "../RightSidebar/MoreMenu.astro";
|
||||||
import TableOfContents from "../RightSidebar/TableOfContents";
|
import TableOfContents from "../RightSidebar/TableOfContents";
|
||||||
|
|
||||||
const { content, githubEditUrl } = Astro.props;
|
const { content, headings, githubEditUrl } = Astro.props;
|
||||||
const title = content.title;
|
const title = content.title;
|
||||||
const headings = content.astro.headings;
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<article id="article" class="content">
|
<article id="article" class="content">
|
||||||
|
@ -29,9 +28,11 @@ const headings = content.astro.headings;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
.content > section {
|
|
||||||
|
.content>section {
|
||||||
margin-bottom: 4rem;
|
margin-bottom: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
---
|
---
|
||||||
import TableOfContents from "./TableOfContents";
|
import TableOfContents from "./TableOfContents";
|
||||||
import MoreMenu from "./MoreMenu.astro";
|
import MoreMenu from "./MoreMenu.astro";
|
||||||
const { content, githubEditUrl } = Astro.props;
|
const { content, headings, githubEditUrl } = Astro.props;
|
||||||
const headings = content.astro.headings;
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<nav class="sidebar-nav" aria-labelledby="grid-right">
|
<nav class="sidebar-nav" aria-labelledby="grid-right">
|
||||||
|
@ -18,6 +17,7 @@ const headings = content.astro.headings;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-nav-inner {
|
.sidebar-nav-inner {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
|
@ -48,6 +48,8 @@
|
||||||
"./vite-plugin-astro-postprocess/*": "./dist/vite-plugin-astro-postprocess/*",
|
"./vite-plugin-astro-postprocess/*": "./dist/vite-plugin-astro-postprocess/*",
|
||||||
"./vite-plugin-jsx/*": "./dist/vite-plugin-jsx/*",
|
"./vite-plugin-jsx/*": "./dist/vite-plugin-jsx/*",
|
||||||
"./vite-plugin-jsx": "./dist/vite-plugin-jsx/index.js",
|
"./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/index.js",
|
||||||
"./vite-plugin-markdown/*": "./dist/vite-plugin-markdown/*",
|
"./vite-plugin-markdown/*": "./dist/vite-plugin-markdown/*",
|
||||||
"./dist/jsx/*": "./dist/jsx/*"
|
"./dist/jsx/*": "./dist/jsx/*"
|
||||||
|
|
|
@ -17,6 +17,7 @@ import { z } from 'zod';
|
||||||
import { LogOptions } from './logger/core.js';
|
import { LogOptions } from './logger/core.js';
|
||||||
import { appendForwardSlash, prependForwardSlash, trimSlashes } from './path.js';
|
import { appendForwardSlash, prependForwardSlash, trimSlashes } from './path.js';
|
||||||
import { arraify, isObject } from './util.js';
|
import { arraify, isObject } from './util.js';
|
||||||
|
import jsxRenderer from '../jsx/renderer.js';
|
||||||
|
|
||||||
load.use([loadTypeScript]);
|
load.use([loadTypeScript]);
|
||||||
|
|
||||||
|
@ -343,16 +344,11 @@ export async function validateConfig(
|
||||||
_ctx: {
|
_ctx: {
|
||||||
pageExtensions: ['.astro', '.md', '.html'],
|
pageExtensions: ['.astro', '.md', '.html'],
|
||||||
scripts: [],
|
scripts: [],
|
||||||
renderers: [],
|
renderers: [jsxRenderer],
|
||||||
injectedRoutes: [],
|
injectedRoutes: [],
|
||||||
adapter: undefined,
|
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.
|
// If successful, return the result as a verified AstroConfig object.
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -12,6 +12,7 @@ import envVitePlugin from '../vite-plugin-env/index.js';
|
||||||
import htmlVitePlugin from '../vite-plugin-html/index.js';
|
import htmlVitePlugin from '../vite-plugin-html/index.js';
|
||||||
import astroIntegrationsContainerPlugin from '../vite-plugin-integrations-container/index.js';
|
import astroIntegrationsContainerPlugin from '../vite-plugin-integrations-container/index.js';
|
||||||
import jsxVitePlugin from '../vite-plugin-jsx/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 markdownVitePlugin from '../vite-plugin-markdown/index.js';
|
||||||
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
import astroScriptsPlugin from '../vite-plugin-scripts/index.js';
|
||||||
import astroScriptsPageSSRPlugin from '../vite-plugin-scripts/page-ssr.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.
|
// the build to run very slow as the filewatcher is triggered often.
|
||||||
mode !== 'build' && astroViteServerPlugin({ config: astroConfig, logging }),
|
mode !== 'build' && astroViteServerPlugin({ config: astroConfig, logging }),
|
||||||
envVitePlugin({ config: astroConfig }),
|
envVitePlugin({ config: astroConfig }),
|
||||||
markdownVitePlugin({ config: astroConfig, logging }),
|
astroConfig.legacy.astroFlavoredMarkdown
|
||||||
|
? legacyMarkdownVitePlugin({ config: astroConfig, logging })
|
||||||
|
: markdownVitePlugin({ config: astroConfig, logging }),
|
||||||
htmlVitePlugin(),
|
htmlVitePlugin(),
|
||||||
jsxVitePlugin({ config: astroConfig, logging }),
|
jsxVitePlugin({ config: astroConfig, logging }),
|
||||||
astroPostprocessVitePlugin({ config: astroConfig }),
|
astroPostprocessVitePlugin({ config: astroConfig }),
|
||||||
|
|
|
@ -137,9 +137,31 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
|
||||||
JSX_RENDERER_CACHE.set(config, jsxRenderers);
|
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 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
|
// downlevel any non-standard syntax, but preserve JSX
|
||||||
const { code: jsxCode } = await esbuild.transform(code, {
|
const { code: jsxCode } = await esbuild.transform(code, {
|
||||||
loader: getEsbuildLoader(path.extname(id)) as esbuild.Loader,
|
loader: getEsbuildLoader(path.extname(id)) as esbuild.Loader,
|
||||||
|
@ -150,7 +172,7 @@ export default function jsx({ config, logging }: AstroPluginJSXOptions): Plugin
|
||||||
return transformJSX({
|
return transformJSX({
|
||||||
code: jsxCode,
|
code: jsxCode,
|
||||||
id,
|
id,
|
||||||
renderer: [...jsxRenderers.values()][0],
|
renderer: [...nonAstroJsxRenderers.values()][0],
|
||||||
mode,
|
mode,
|
||||||
ssr,
|
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 JSX renderer found, then use that
|
||||||
if (importSource) {
|
if (importSource) {
|
||||||
const jsxRenderer = jsxRenderers.get(importSource);
|
const jsxRenderer = jsxRenderers.get(importSource);
|
||||||
|
|
3
packages/astro/src/vite-plugin-markdown-legacy/README.md
Normal file
3
packages/astro/src/vite-plugin-markdown-legacy/README.md
Normal 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.
|
260
packages/astro/src/vite-plugin-markdown-legacy/index.ts
Normal file
260
packages/astro/src/vite-plugin-markdown-legacy/index.ts
Normal 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');
|
||||||
|
}
|
|
@ -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.
|
Adds Markdown support to Vite, both at the top level as well as within `.astro` files.
|
||||||
|
|
|
@ -1,28 +1,19 @@
|
||||||
import { renderMarkdown } from '@astrojs/markdown-remark';
|
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 matter from 'gray-matter';
|
||||||
import { fileURLToPath } from 'url';
|
import fs from 'fs';
|
||||||
import type { Plugin } from 'vite';
|
import type { Plugin } from 'vite';
|
||||||
import type { AstroConfig } from '../@types/astro';
|
import type { AstroConfig } from '../@types/astro';
|
||||||
import { pagesVirtualModuleId } from '../core/app/index.js';
|
|
||||||
import { collectErrorMetadata } from '../core/errors.js';
|
import { collectErrorMetadata } from '../core/errors.js';
|
||||||
import type { LogOptions } from '../core/logger/core.js';
|
import type { LogOptions } from '../core/logger/core.js';
|
||||||
import { warn } from '../core/logger/core.js';
|
import type { PluginMetadata } from '../vite-plugin-astro/types.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';
|
import { getFileInfo } from '../vite-plugin-utils/index.js';
|
||||||
|
import { warn } from '../core/logger/core.js';
|
||||||
|
|
||||||
interface AstroPluginOptions {
|
interface AstroPluginOptions {
|
||||||
config: AstroConfig;
|
config: AstroConfig;
|
||||||
logging: LogOptions;
|
logging: LogOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MARKDOWN_IMPORT_FLAG = '?mdImport';
|
|
||||||
const MARKDOWN_CONTENT_FLAG = '?content';
|
|
||||||
|
|
||||||
function safeMatter(source: string, id: string) {
|
function safeMatter(source: string, id: string) {
|
||||||
try {
|
try {
|
||||||
return matter(source);
|
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 {
|
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 {
|
return {
|
||||||
name: 'astro:markdown',
|
|
||||||
enforce: 'pre',
|
enforce: 'pre',
|
||||||
configResolved(_resolvedConfig) {
|
name: 'astro:markdown',
|
||||||
viteTransform = getViteTransform(_resolvedConfig);
|
// Why not the "transform" hook instead of "load" + readFile?
|
||||||
},
|
// A: Vite transforms all "import.meta.env" references to their values before
|
||||||
async resolveId(id, importer, options) {
|
// passing to the transform hook. This lets us get the truly raw value
|
||||||
// Resolve any .md files with the `?content` cache buster. This should only come from
|
// to escape "import.meta.env" ourselves.
|
||||||
// an already-resolved JS module wrapper. Needed to prevent infinite loops in Vite.
|
async load(id) {
|
||||||
// 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')) {
|
if (id.endsWith('.md')) {
|
||||||
const filename = normalizeFilename(id);
|
const { fileId, fileUrl } = getFileInfo(id, config);
|
||||||
const source = await fs.promises.readFile(filename, 'utf8');
|
const rawFile = await fs.promises.readFile(fileId, 'utf-8');
|
||||||
const renderOpts = config.markdown;
|
const raw = safeMatter(rawFile, id);
|
||||||
const isAstroFlavoredMd = config.legacy.astroFlavoredMarkdown;
|
const renderResult = await renderMarkdown(raw.content, {
|
||||||
|
...config.markdown,
|
||||||
const fileUrl = new URL(`file://${filename}`);
|
fileURL: new URL(`file://${fileId}`),
|
||||||
|
isAstroFlavoredMd: false,
|
||||||
// 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,
|
|
||||||
} as any);
|
} 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
|
const html = renderResult.code;
|
||||||
if (setup && !isAstroFlavoredMd) {
|
const { headings } = renderResult.metadata;
|
||||||
|
const frontmatter = { ...raw.data, url: fileUrl, file: fileId } as any;
|
||||||
|
const { layout } = frontmatter;
|
||||||
|
|
||||||
|
if (frontmatter.setup) {
|
||||||
warn(
|
warn(
|
||||||
logging,
|
logging,
|
||||||
'markdown',
|
'markdown',
|
||||||
|
@ -179,100 +55,59 @@ export default function markdown({ config, logging }: AstroPluginOptions): Plugi
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const prelude = `---
|
const code = escapeViteEnvReferences(`
|
||||||
import Slugger from 'github-slugger';
|
import { Fragment, jsx as h } from 'astro/jsx-runtime';
|
||||||
${layout ? `import Layout from '${layout}';` : ''}
|
${layout ? `import Layout from ${JSON.stringify(layout)};` : ''}
|
||||||
${isAstroFlavoredMd && components ? `import * from '${components}';` : ''}
|
|
||||||
${isAstroFlavoredMd ? setup : ''}
|
|
||||||
|
|
||||||
const slugger = new Slugger();
|
const html = ${JSON.stringify(html)};
|
||||||
function $$slug(value) {
|
|
||||||
return slugger.slug(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const $$content = ${JSON.stringify(
|
export const frontmatter = ${JSON.stringify(frontmatter)};
|
||||||
isAstroFlavoredMd
|
export const file = ${JSON.stringify(fileId)};
|
||||||
? content
|
export const url = ${JSON.stringify(fileUrl)};
|
||||||
: // Avoid stripping "setup" and "components"
|
export function rawContent() {
|
||||||
// in plain MD mode
|
return ${JSON.stringify(raw.content)};
|
||||||
{ ...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 function compiledContent() {
|
||||||
// If the user imported "Layout", wrap the content in a Layout
|
return html;
|
||||||
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 getHeadings() {
|
||||||
// Transform from `.astro` to valid `.ts`
|
return ${JSON.stringify(headings)};
|
||||||
const compileProps: CompileProps = {
|
}
|
||||||
config,
|
export function getHeaders() {
|
||||||
filename,
|
console.warn('getHeaders() have been deprecated. Use getHeadings() function instead.');
|
||||||
moduleId: id,
|
return getHeadings();
|
||||||
source: astroResult,
|
|
||||||
ssr: Boolean(opts?.ssr),
|
|
||||||
viteTransform,
|
|
||||||
pluginContext: this,
|
|
||||||
};
|
};
|
||||||
|
export async function Content() {
|
||||||
let transformResult = await cachedCompilation(compileProps);
|
const { layout, ...content } = frontmatter;
|
||||||
let { code: tsResult } = transformResult;
|
content.astro = {};
|
||||||
|
Object.defineProperty(content.astro, 'headings', {
|
||||||
tsResult = `\nexport const metadata = ${JSON.stringify(metadata)};
|
get() {
|
||||||
export const frontmatter = ${JSON.stringify(content)};
|
throw new Error('The "astro" property is no longer supported! To access "headings" from your layout, try using "Astro.props.headings."')
|
||||||
export function rawContent() {
|
}
|
||||||
return ${JSON.stringify(markdownContent)};
|
});
|
||||||
}
|
const contentFragment = h(Fragment, { 'set:html': html });
|
||||||
export function compiledContent() {
|
return ${
|
||||||
return ${JSON.stringify(renderResult.metadata.html)};
|
layout
|
||||||
}
|
? `h(Layout, { content, frontmatter: content, headings: getHeadings(), 'server:root': true, children: contentFragment })`
|
||||||
${tsResult}`;
|
: `contentFragment`
|
||||||
|
};
|
||||||
// Compile from `.ts` to `.js`
|
}
|
||||||
const { code } = await esbuild.transform(tsResult, {
|
export default Content;
|
||||||
loader: 'ts',
|
`);
|
||||||
sourcemap: false,
|
|
||||||
sourcefile: id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const astroMetadata: AstroPluginMetadata['astro'] = {
|
|
||||||
clientOnlyComponents: transformResult.clientOnlyComponents,
|
|
||||||
hydratedComponents: transformResult.hydratedComponents,
|
|
||||||
scripts: transformResult.scripts,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: escapeViteEnvReferences(code),
|
code,
|
||||||
map: null,
|
|
||||||
meta: {
|
meta: {
|
||||||
astro: astroMetadata,
|
astro: {
|
||||||
|
hydratedComponents: [],
|
||||||
|
clientOnlyComponents: [],
|
||||||
|
scripts: [],
|
||||||
|
} as PluginMetadata['astro'],
|
||||||
vite: {
|
vite: {
|
||||||
lang: 'ts',
|
lang: 'ts',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -2,199 +2,130 @@ import { expect } from 'chai';
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
import { loadFixture, fixLineEndings } from './test-utils.js';
|
import { loadFixture, fixLineEndings } from './test-utils.js';
|
||||||
|
|
||||||
|
const FIXTURE_ROOT = './fixtures/astro-markdown/';
|
||||||
|
|
||||||
describe('Astro Markdown', () => {
|
describe('Astro Markdown', () => {
|
||||||
let fixture;
|
let fixture;
|
||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
fixture = await loadFixture({
|
fixture = await loadFixture({
|
||||||
root: './fixtures/astro-markdown/',
|
root: FIXTURE_ROOT,
|
||||||
});
|
});
|
||||||
await fixture.build();
|
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 html = await fixture.readFile('/jsx-expressions/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
expect($('h2').html()).to.equal('Blog Post with JSX expressions');
|
expect($('h2').html()).to.equal('{frontmatter.title}');
|
||||||
|
|
||||||
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 () => {
|
it('Leaves JSX components un-transformed', async () => {
|
||||||
const html = await fixture.readFile('/slug/index.html');
|
const html = await fixture.readFile('/components/index.html');
|
||||||
const $ = cheerio.load(html);
|
|
||||||
|
|
||||||
expect($('h1').attr('id')).to.equal('my-blog-post');
|
expect(html).to.include('<counter client:load="" count="{0}">');
|
||||||
});
|
|
||||||
|
|
||||||
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 () => {
|
it('Exposes raw markdown content', async () => {
|
||||||
const { raw } = JSON.parse(await fixture.readFile('/raw-content.json'));
|
const { raw } = JSON.parse(await fixture.readFile('/raw-content.json'));
|
||||||
|
|
||||||
expect(fixLineEndings(raw)).to.equal(
|
expect(fixLineEndings(raw).trim()).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`
|
`# 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'));
|
const { compiled } = JSON.parse(await fixture.readFile('/raw-content.json'));
|
||||||
|
|
||||||
expect(fixLineEndings(compiled)).to.equal(
|
expect(fixLineEndings(compiled).trim()).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 />`
|
`<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 () => {
|
describe('syntax highlighting', async () => {
|
||||||
const html = await fixture.readFile('/vite-env-vars/index.html');
|
it('handles Shiki', async () => {
|
||||||
const $ = cheerio.load(html);
|
const html = await fixture.readFile('/code-in-md/index.html');
|
||||||
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
// test 1: referencing an existing var name
|
expect($('pre.astro-code').length).to.not.equal(0);
|
||||||
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
|
it('handles Prism', async () => {
|
||||||
expect($('code').eq(1).text()).to.equal('import.meta.env.TITLE');
|
const prismFixture = await loadFixture({
|
||||||
expect($('li').eq(1).text()).to.equal('import.meta.env.TITLE');
|
root: FIXTURE_ROOT,
|
||||||
expect($('code').eq(3).text()).to.contain('title: import.meta.env.TITLE');
|
markdown: {
|
||||||
expect($('blockquote').text()).to.contain('import.meta.env.TITLE');
|
syntaxHighlight: 'prism',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await prismFixture.build();
|
||||||
|
|
||||||
// test 3: referencing `import.meta.env` itself (without any var name)
|
const html = await prismFixture.readFile('/code-in-md/index.html');
|
||||||
expect($('code').eq(2).text()).to.equal('import.meta.env');
|
const $ = cheerio.load(html);
|
||||||
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($('pre.language-html').length).to.not.equal(0);
|
||||||
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');
|
it('Passes frontmatter to layout via "content" and "frontmatter" props', async () => {
|
||||||
|
const html = await fixture.readFile('/with-layout/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
expect($('code').eq(0).html()).to.equal('<script>');
|
const contentTitle = $('[data-content-title]');
|
||||||
expect($('blockquote').length).to.equal(1);
|
const frontmatterTitle = $('[data-frontmatter-title]');
|
||||||
expect($('code').eq(1).html()).to.equal('</script>');
|
|
||||||
expect($('pre').html()).to.contain('>This should also work without any problems.<');
|
expect(contentTitle.text()).to.equal('With layout');
|
||||||
|
expect(frontmatterTitle.text()).to.equal('With layout');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Allows defining slot contents in component children', async () => {
|
it('Passes headings to layout via "headings" prop', async () => {
|
||||||
const html = await fixture.readFile('/slots/index.html');
|
const html = await fixture.readFile('/with-layout/index.html');
|
||||||
const $ = cheerio.load(html);
|
const $ = cheerio.load(html);
|
||||||
|
|
||||||
const slots = $('article').eq(0);
|
const headingSlugs = [...$('body').find('[data-headings] > li')].map(
|
||||||
expect(slots.find('> .fragmentSlot > div').text()).to.contain('1:');
|
(el) => $(el).text()
|
||||||
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(headingSlugs.length).to.be.greaterThan(0);
|
||||||
expect(nestedSlots.find('> .fragmentSlot').html()).to.contain('1:');
|
expect(headingSlugs).to.contain('section-1');
|
||||||
expect(nestedSlots.find('> .pSlot > p').text()).to.contain('2:');
|
expect(headingSlugs).to.contain('section-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 () => {
|
it('Exposes getHeadings() on glob imports', async () => {
|
||||||
const html = await fixture.readFile('/layout-props/index.html');
|
const { headings } = JSON.parse(await fixture.readFile('/headings-glob.json'));
|
||||||
const $ = cheerio.load(html);
|
|
||||||
|
|
||||||
expect($('#title').text()).to.equal('Hello world!');
|
const headingSlugs = headings.map(heading => heading?.slug);
|
||||||
expect($('#url').text()).to.equal('/layout-props');
|
|
||||||
expect($('#file').text()).to.match(/.*\/layout-props.md$/);
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
8
packages/astro/test/fixtures/astro-head/package.json
vendored
Normal file
8
packages/astro/test/fixtures/astro-head/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "@test/astro-head",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +0,0 @@
|
||||||
# Fenced code blocks
|
|
||||||
|
|
||||||
```html
|
|
||||||
<body>
|
|
||||||
<div>This should also work without any problems.</div>
|
|
||||||
</body>
|
|
||||||
```
|
|
|
@ -1,12 +1,8 @@
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import preact from '@astrojs/preact';
|
|
||||||
import svelte from "@astrojs/svelte";
|
import svelte from "@astrojs/svelte";
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
integrations: [preact(), svelte()],
|
integrations: [svelte()],
|
||||||
site: 'https://astro.build/',
|
site: 'https://astro.build/',
|
||||||
legacy: {
|
|
||||||
astroFlavoredMarkdown: true,
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "@test/astro-markdown",
|
"name": "@test/astro-markdown-md-mode",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/preact": "workspace:*",
|
|
||||||
"@astrojs/svelte": "workspace:*",
|
"@astrojs/svelte": "workspace:*",
|
||||||
"astro": "workspace:*"
|
"astro": "workspace:*"
|
||||||
}
|
}
|
||||||
|
|
28
packages/astro/test/fixtures/astro-markdown/src/layouts/Base.astro
vendored
Normal file
28
packages/astro/test/fixtures/astro-markdown/src/layouts/Base.astro
vendored
Normal 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>
|
3
packages/astro/test/fixtures/astro-markdown/src/pages/basic.md
vendored
Normal file
3
packages/astro/test/fixtures/astro-markdown/src/pages/basic.md
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Basic page
|
||||||
|
|
||||||
|
Lets make sure raw and compiled content look right!
|
|
@ -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
|
# Fenced code blocks
|
||||||
|
|
||||||
```html
|
```html
|
||||||
|
|
9
packages/astro/test/fixtures/astro-markdown/src/pages/headings-glob.json.js
vendored
Normal file
9
packages/astro/test/fixtures/astro-markdown/src/pages/headings-glob.json.js
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { getHeadings } from './with-layout.md';
|
||||||
|
|
||||||
|
export async function get() {
|
||||||
|
return {
|
||||||
|
body: JSON.stringify({
|
||||||
|
headings: getHeadings(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { rawContent, compiledContent } from '../imported-md/with-components.md';
|
import { rawContent, compiledContent } from './basic.md';
|
||||||
|
|
||||||
export async function get() {
|
export async function get() {
|
||||||
return {
|
return {
|
||||||
|
|
7
packages/astro/test/fixtures/astro-markdown/src/pages/vite-env-vars-glob.json.js
vendored
Normal file
7
packages/astro/test/fixtures/astro-markdown/src/pages/vite-env-vars-glob.json.js
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { frontmatter } from './vite-env-vars.md';
|
||||||
|
|
||||||
|
export async function get() {
|
||||||
|
return {
|
||||||
|
body: JSON.stringify(frontmatter),
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
title: Referencing Vite Env Vars like import.meta.env.SITE, import.meta.env.TITLE and import.meta.env
|
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
|
## Referencing the full name of Vite env vars
|
||||||
|
@ -29,7 +28,3 @@ export const get = () => rss({
|
||||||
items: import.meta.glob('./**/*.md'),
|
items: import.meta.glob('./**/*.md'),
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage in frontmatter
|
|
||||||
|
|
||||||
> frontmatter.title: {frontmatter.title}
|
|
||||||
|
|
8
packages/astro/test/fixtures/astro-markdown/src/pages/with-layout.md
vendored
Normal file
8
packages/astro/test/fixtures/astro-markdown/src/pages/with-layout.md
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
title: 'With layout'
|
||||||
|
layout: '../layouts/Base.astro'
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 1
|
||||||
|
|
||||||
|
## Section 2
|
8
packages/astro/test/fixtures/client-address/package.json
vendored
Normal file
8
packages/astro/test/fixtures/client-address/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "@test/client-address",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
8
packages/astro/test/fixtures/glob-pages-css/package.json
vendored
Normal file
8
packages/astro/test/fixtures/glob-pages-css/package.json
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "@test/glob-pages-css",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"astro": "workspace:*"
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,12 @@
|
||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
|
import preact from '@astrojs/preact';
|
||||||
import svelte from "@astrojs/svelte";
|
import svelte from "@astrojs/svelte";
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
integrations: [svelte()],
|
integrations: [preact(), svelte()],
|
||||||
site: 'https://astro.build/',
|
site: 'https://astro.build/',
|
||||||
|
legacy: {
|
||||||
|
astroFlavoredMarkdown: true,
|
||||||
|
}
|
||||||
});
|
});
|
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
"name": "@test/astro-markdown-md-mode",
|
"name": "@test/astro-markdown",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrojs/preact": "workspace:*",
|
||||||
"@astrojs/svelte": "workspace:*",
|
"@astrojs/svelte": "workspace:*",
|
||||||
"astro": "workspace:*"
|
"astro": "workspace:*"
|
||||||
}
|
}
|
16
packages/astro/test/fixtures/legacy-astro-flavored-markdown/src/pages/code-in-md.md
vendored
Normal file
16
packages/astro/test/fixtures/legacy-astro-flavored-markdown/src/pages/code-in-md.md
vendored
Normal 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>
|
||||||
|
```
|
10
packages/astro/test/fixtures/legacy-astro-flavored-markdown/src/pages/raw-content.json.js
vendored
Normal file
10
packages/astro/test/fixtures/legacy-astro-flavored-markdown/src/pages/raw-content.json.js
vendored
Normal 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(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
35
packages/astro/test/fixtures/legacy-astro-flavored-markdown/src/pages/vite-env-vars.md
vendored
Normal file
35
packages/astro/test/fixtures/legacy-astro-flavored-markdown/src/pages/vite-env-vars.md
vendored
Normal 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}
|
200
packages/astro/test/legacy-astro-flavored-markdown.test.js
Normal file
200
packages/astro/test/legacy-astro-flavored-markdown.test.js
Normal 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('<script>');
|
||||||
|
expect($('blockquote').length).to.equal(1);
|
||||||
|
expect($('code').eq(1).html()).to.equal('</script>');
|
||||||
|
expect($('pre').html()).to.contain('>This should also work without any problems.<');
|
||||||
|
});
|
||||||
|
|
||||||
|
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$/);
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,7 +2,7 @@ import type { Literal } from 'unist';
|
||||||
import { visit } from 'unist-util-visit';
|
import { visit } from 'unist-util-visit';
|
||||||
|
|
||||||
// In code blocks, this removes the JS comment wrapper added to
|
// 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() {
|
export default function remarkEscape() {
|
||||||
return (tree: any) => {
|
return (tree: any) => {
|
||||||
visit(tree, 'code', removeCommentWrapper);
|
visit(tree, 'code', removeCommentWrapper);
|
||||||
|
|
|
@ -1199,6 +1199,12 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
astro: link:../../..
|
astro: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/astro-head:
|
||||||
|
specifiers:
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
astro: link:../../..
|
||||||
|
|
||||||
packages/astro/test/fixtures/astro-jsx:
|
packages/astro/test/fixtures/astro-jsx:
|
||||||
specifiers:
|
specifiers:
|
||||||
astro: workspace:*
|
astro: workspace:*
|
||||||
|
@ -1207,11 +1213,9 @@ importers:
|
||||||
|
|
||||||
packages/astro/test/fixtures/astro-markdown:
|
packages/astro/test/fixtures/astro-markdown:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/preact': workspace:*
|
|
||||||
'@astrojs/svelte': workspace:*
|
'@astrojs/svelte': workspace:*
|
||||||
astro: workspace:*
|
astro: workspace:*
|
||||||
dependencies:
|
dependencies:
|
||||||
'@astrojs/preact': link:../../../../integrations/preact
|
|
||||||
'@astrojs/svelte': link:../../../../integrations/svelte
|
'@astrojs/svelte': link:../../../../integrations/svelte
|
||||||
astro: link:../../..
|
astro: link:../../..
|
||||||
|
|
||||||
|
@ -1227,14 +1231,6 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
astro: link:../../..
|
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:
|
packages/astro/test/fixtures/astro-markdown-plugins:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/preact': workspace:*
|
'@astrojs/preact': workspace:*
|
||||||
|
@ -1349,6 +1345,12 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
astro: link:../../..
|
astro: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/client-address:
|
||||||
|
specifiers:
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
astro: link:../../..
|
||||||
|
|
||||||
packages/astro/test/fixtures/code-component:
|
packages/astro/test/fixtures/code-component:
|
||||||
specifiers:
|
specifiers:
|
||||||
astro: workspace:*
|
astro: workspace:*
|
||||||
|
@ -1479,6 +1481,12 @@ importers:
|
||||||
'@fontsource/montserrat': 4.5.11
|
'@fontsource/montserrat': 4.5.11
|
||||||
astro: link:../../..
|
astro: link:../../..
|
||||||
|
|
||||||
|
packages/astro/test/fixtures/glob-pages-css:
|
||||||
|
specifiers:
|
||||||
|
astro: workspace:*
|
||||||
|
dependencies:
|
||||||
|
astro: link:../../..
|
||||||
|
|
||||||
packages/astro/test/fixtures/hmr-css:
|
packages/astro/test/fixtures/hmr-css:
|
||||||
specifiers:
|
specifiers:
|
||||||
astro: workspace:*
|
astro: workspace:*
|
||||||
|
@ -1566,6 +1574,16 @@ importers:
|
||||||
'@astrojs/solid-js': link:../../../../integrations/solid
|
'@astrojs/solid-js': link:../../../../integrations/solid
|
||||||
astro: link:../../..
|
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:
|
packages/astro/test/fixtures/legacy-build:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/vue': workspace:*
|
'@astrojs/vue': workspace:*
|
||||||
|
|
Loading…
Reference in a new issue