Remove Astro-flavored Markdown from @astrojs/markdown-remark
(#5785)
This commit is contained in:
parent
54076a41eb
commit
16107b6a10
22 changed files with 12 additions and 842 deletions
5
.changeset/poor-chicken-film.md
Normal file
5
.changeset/poor-chicken-film.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'@astrojs/markdown-remark': major
|
||||
---
|
||||
|
||||
Drop support for legacy Astro-flavored Markdown
|
|
@ -71,7 +71,6 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
|
|||
const renderResult = await renderMarkdown(raw.content, {
|
||||
...settings.config.markdown,
|
||||
fileURL: new URL(`file://${fileId}`),
|
||||
isAstroFlavoredMd: false,
|
||||
isExperimentalContentCollections: settings.config.experimental.contentCollections,
|
||||
contentDir: getContentPaths(settings.config).contentDir,
|
||||
frontmatter: raw.data,
|
||||
|
|
|
@ -25,19 +25,11 @@
|
|||
"test": "mocha --exit --timeout 20000"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/micromark-extension-mdx-jsx": "^1.0.3",
|
||||
"@astrojs/prism": "^1.0.0",
|
||||
"acorn": "^8.7.1",
|
||||
"acorn-jsx": "^5.3.2",
|
||||
"github-slugger": "^1.4.0",
|
||||
"hast-util-to-html": "^8.0.3",
|
||||
"import-meta-resolve": "^2.1.0",
|
||||
"mdast-util-from-markdown": "^1.2.0",
|
||||
"mdast-util-mdx-expression": "^1.2.1",
|
||||
"mdast-util-mdx-jsx": "^1.2.0",
|
||||
"micromark-extension-mdx-expression": "^1.0.3",
|
||||
"micromark-extension-mdx-md": "^1.0.0",
|
||||
"micromark-util-combine-extensions": "^1.0.0",
|
||||
"rehype-raw": "^6.1.1",
|
||||
"rehype-stringify": "^9.0.3",
|
||||
"remark-gfm": "^3.0.1",
|
||||
|
@ -46,7 +38,6 @@
|
|||
"remark-smartypants": "^2.0.0",
|
||||
"shiki": "^0.11.1",
|
||||
"unified": "^10.1.2",
|
||||
"unist-util-map": "^3.1.1",
|
||||
"unist-util-visit": "^4.1.0",
|
||||
"vfile": "^5.3.2"
|
||||
},
|
||||
|
@ -59,7 +50,6 @@
|
|||
"@types/unist": "^2.0.6",
|
||||
"astro-scripts": "workspace:*",
|
||||
"chai": "^4.3.6",
|
||||
"micromark-util-types": "^1.0.2",
|
||||
"mocha": "^9.2.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,18 +8,10 @@ import type {
|
|||
import { toRemarkInitializeAstroData } from './frontmatter-injection.js';
|
||||
import { loadPlugins } from './load-plugins.js';
|
||||
import { rehypeHeadingIds } from './rehype-collect-headings.js';
|
||||
import rehypeEscape from './rehype-escape.js';
|
||||
import rehypeExpressions from './rehype-expressions.js';
|
||||
import rehypeIslands from './rehype-islands.js';
|
||||
import rehypeJsx from './rehype-jsx.js';
|
||||
import toRemarkContentRelImageError from './remark-content-rel-image-error.js';
|
||||
import remarkEscape from './remark-escape.js';
|
||||
import remarkMarkAndUnravel from './remark-mark-and-unravel.js';
|
||||
import remarkMdxish from './remark-mdxish.js';
|
||||
import remarkPrism from './remark-prism.js';
|
||||
import scopedStyles from './remark-scoped-styles.js';
|
||||
import remarkShiki from './remark-shiki.js';
|
||||
import remarkUnwrap from './remark-unwrap.js';
|
||||
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import rehypeStringify from 'rehype-stringify';
|
||||
|
@ -61,7 +53,6 @@ export async function renderMarkdown(
|
|||
remarkRehype = markdownConfigDefaults.remarkRehype,
|
||||
gfm = markdownConfigDefaults.gfm,
|
||||
smartypants = markdownConfigDefaults.smartypants,
|
||||
isAstroFlavoredMd = false,
|
||||
isExperimentalContentCollections = false,
|
||||
contentDir,
|
||||
frontmatter: userFrontmatter = {},
|
||||
|
@ -72,7 +63,7 @@ export async function renderMarkdown(
|
|||
let parser = unified()
|
||||
.use(markdown)
|
||||
.use(toRemarkInitializeAstroData({ userFrontmatter }))
|
||||
.use(isAstroFlavoredMd ? [remarkMdxish, remarkMarkAndUnravel, remarkUnwrap, remarkEscape] : []);
|
||||
.use([]);
|
||||
|
||||
if (gfm) {
|
||||
parser.use(remarkGfm);
|
||||
|
@ -109,15 +100,7 @@ export async function renderMarkdown(
|
|||
markdownToHtml as any,
|
||||
{
|
||||
allowDangerousHtml: true,
|
||||
passThrough: isAstroFlavoredMd
|
||||
? [
|
||||
'raw',
|
||||
'mdxFlowExpression',
|
||||
'mdxJsxFlowElement',
|
||||
'mdxJsxTextElement',
|
||||
'mdxTextExpression',
|
||||
]
|
||||
: [],
|
||||
passThrough: [],
|
||||
...remarkRehype,
|
||||
},
|
||||
],
|
||||
|
@ -127,13 +110,7 @@ export async function renderMarkdown(
|
|||
parser.use([[plugin, pluginOpts]]);
|
||||
});
|
||||
|
||||
parser
|
||||
.use(
|
||||
isAstroFlavoredMd
|
||||
? [rehypeJsx, rehypeExpressions, rehypeEscape, rehypeIslands, rehypeHeadingIds]
|
||||
: [rehypeHeadingIds, rehypeRaw]
|
||||
)
|
||||
.use(rehypeStringify, { allowDangerousHtml: true });
|
||||
parser.use([rehypeHeadingIds, rehypeRaw]).use(rehypeStringify, { allowDangerousHtml: true });
|
||||
|
||||
let vfile: MarkdownVFile;
|
||||
try {
|
||||
|
|
|
@ -1,12 +0,0 @@
|
|||
import { mdxExpressionFromMarkdown, mdxExpressionToMarkdown } from 'mdast-util-mdx-expression';
|
||||
import { mdxJsxFromMarkdown, mdxJsxToMarkdown } from 'mdast-util-mdx-jsx';
|
||||
|
||||
export function mdxFromMarkdown(): any {
|
||||
return [mdxExpressionFromMarkdown, mdxJsxFromMarkdown];
|
||||
}
|
||||
|
||||
export function mdxToMarkdown(): any {
|
||||
return {
|
||||
extensions: [mdxExpressionToMarkdown, mdxJsxToMarkdown],
|
||||
};
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
// Note: The code in this file is based on `micromark-extension-mdxjs`
|
||||
// and was adapted to use our fork `@astrojs/micromark-extension-mdx-jsx`
|
||||
// instead of `micromark-extension-mdx-jsx` to allow some extended syntax.
|
||||
// See `@astrojs/micromark-extension-mdx-jsx` on NPM for more details.
|
||||
// Also, support for ESM imports & exports in Markdown content was removed.
|
||||
|
||||
import { mdxJsx } from '@astrojs/micromark-extension-mdx-jsx';
|
||||
import { Parser } from 'acorn';
|
||||
import acornJsx from 'acorn-jsx';
|
||||
import type { Options } from 'micromark-extension-mdx-expression';
|
||||
import { mdxExpression } from 'micromark-extension-mdx-expression';
|
||||
import { mdxMd } from 'micromark-extension-mdx-md';
|
||||
import { combineExtensions } from 'micromark-util-combine-extensions';
|
||||
import type { Extension } from 'micromark-util-types';
|
||||
|
||||
export function mdxjs(options: Options): Extension {
|
||||
const settings: any = Object.assign(
|
||||
{
|
||||
acorn: Parser.extend(acornJsx()),
|
||||
acornOptions: { ecmaVersion: 2020, sourceType: 'module' },
|
||||
addResult: true,
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
return combineExtensions([mdxExpression(settings), mdxJsx(settings), mdxMd]);
|
||||
}
|
|
@ -21,7 +21,6 @@ export function rehypeHeadingIds(): ReturnType<RehypePlugin> {
|
|||
const depth = Number.parseInt(level);
|
||||
|
||||
let text = '';
|
||||
let isJSX = false;
|
||||
visit(node, (child, __, parent) => {
|
||||
if (child.type === 'element' || parent == null) {
|
||||
return;
|
||||
|
@ -36,31 +35,17 @@ export function rehypeHeadingIds(): ReturnType<RehypePlugin> {
|
|||
text += child.value;
|
||||
} else {
|
||||
text += child.value.replace(/\{/g, '${');
|
||||
isJSX = isJSX || child.value.includes('{');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
node.properties = node.properties || {};
|
||||
if (typeof node.properties.id !== 'string') {
|
||||
if (isJSX) {
|
||||
// HACK: serialized JSX from internal plugins, ignore these for slug
|
||||
const raw = toHtml(node.children, { allowDangerousHtml: true })
|
||||
.replace(/\n(<)/g, '<')
|
||||
.replace(/(>)\n/g, '>');
|
||||
// HACK: for ids that have JSX content, use $$slug helper to generate slug at runtime
|
||||
node.properties.id = `$$slug(\`${text}\`)`;
|
||||
(node as any).type = 'raw';
|
||||
(
|
||||
node as any
|
||||
).value = `<${node.tagName} id={${node.properties.id}}>${raw}</${node.tagName}>`;
|
||||
} else {
|
||||
let slug = slugger.slug(text);
|
||||
let slug = slugger.slug(text);
|
||||
|
||||
if (slug.endsWith('-')) slug = slug.slice(0, -1);
|
||||
if (slug.endsWith('-')) slug = slug.slice(0, -1);
|
||||
|
||||
node.properties.id = slug;
|
||||
}
|
||||
node.properties.id = slug;
|
||||
}
|
||||
|
||||
headings.push({ depth, slug: node.properties.id, text });
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import { SKIP, visit } from 'unist-util-visit';
|
||||
|
||||
export function escapeEntities(value: string): string {
|
||||
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
export default function rehypeEscape(): any {
|
||||
return function (node: any): any {
|
||||
return visit(node, 'element', (el) => {
|
||||
if (el.tagName === 'code' || el.tagName === 'pre') {
|
||||
el.properties['is:raw'] = true;
|
||||
// Visit all raw children and escape HTML tags to prevent Markdown code
|
||||
// like "This is a `<script>` tag" from actually opening a script tag
|
||||
visit(el, 'raw', (raw) => {
|
||||
raw.value = escapeEntities(raw.value);
|
||||
});
|
||||
// Do not visit children to prevent double escaping
|
||||
return SKIP;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { map } from 'unist-util-map';
|
||||
|
||||
export default function rehypeExpressions(): any {
|
||||
return function (node: any): any {
|
||||
return map(node, (child) => {
|
||||
if (child.type === 'text') {
|
||||
return { ...child, type: 'raw' };
|
||||
}
|
||||
if (child.type === 'mdxTextExpression') {
|
||||
return { type: 'raw', value: `{${(child as any).value}}` };
|
||||
}
|
||||
if (child.type === 'mdxFlowExpression') {
|
||||
return { type: 'raw', value: `{${(child as any).value}}` };
|
||||
}
|
||||
return child;
|
||||
});
|
||||
};
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
import { SKIP, visit as _visit } from 'unist-util-visit';
|
||||
|
||||
// This is a workaround.
|
||||
// It fixes a compatibility issue between different, incompatible ASTs given by plugins to Unist
|
||||
const visit = _visit as (
|
||||
node: any,
|
||||
type: string,
|
||||
callback?: (node: any, index: number, parent: any) => any
|
||||
) => any;
|
||||
|
||||
// This fixes some confusing bugs coming from somewhere inside of our Markdown pipeline.
|
||||
// `unist`/`remark`/`rehype` (not sure) often generate malformed HTML inside of <astro-island>
|
||||
// For hydration to work properly, frameworks need the DOM to be the exact same on server/client.
|
||||
// This reverts some "helpful corrections" that are applied to our perfectly valid HTML!
|
||||
export default function rehypeIslands(): any {
|
||||
return function (node: any): any {
|
||||
return visit(node, 'element', (el) => {
|
||||
// Bugs only happen inside of <astro-island> islands
|
||||
if (el.tagName == 'astro-island') {
|
||||
visit(el, 'text', (child, index, parent) => {
|
||||
if (child.type === 'text') {
|
||||
// Sometimes comments can be trapped as text, which causes them to be escaped
|
||||
// This casts them back to real HTML comments
|
||||
if (parent && child.value.indexOf('<!--') > -1 && index != null) {
|
||||
parent.children.splice(index, 1, {
|
||||
...child,
|
||||
type: 'comment',
|
||||
value: child.value.replace('<!--', '').replace('-->', '').trim(),
|
||||
});
|
||||
return [SKIP, index];
|
||||
}
|
||||
// For some reason `rehype` likes to inject extra linebreaks,
|
||||
// but React and Vue throw hydration errors when they see these!
|
||||
// This removes any extra linebreaks, which is fine because
|
||||
// framework compilers don't preserve them anyway
|
||||
child.value = child.value.replace(/\n+/g, '');
|
||||
return child;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
import { visit } from 'unist-util-visit';
|
||||
import type { RehypePlugin } from './types.js';
|
||||
|
||||
const MDX_ELEMENTS = ['mdxJsxFlowElement', 'mdxJsxTextElement'];
|
||||
|
||||
export default function rehypeJsx(): ReturnType<RehypePlugin> {
|
||||
return function (tree) {
|
||||
visit(tree, MDX_ELEMENTS, (node: any, index: number | null, parent: any) => {
|
||||
if (index === null || !Boolean(parent)) return;
|
||||
|
||||
const attrs = node.attributes.reduce((acc: any[], entry: any) => {
|
||||
let attr = entry.value;
|
||||
if (attr && typeof attr === 'object') {
|
||||
attr = `{${attr.value}}`;
|
||||
} else if (attr && entry.type === 'mdxJsxExpressionAttribute') {
|
||||
attr = `{${attr}}`;
|
||||
} else if (attr === null) {
|
||||
attr = '';
|
||||
} else if (typeof attr === 'string') {
|
||||
attr = `"${attr}"`;
|
||||
}
|
||||
if (!entry.name) {
|
||||
return acc + ` ${attr}`;
|
||||
}
|
||||
return acc + ` ${entry.name}${attr ? '=' : ''}${attr}`;
|
||||
}, '');
|
||||
|
||||
if (node.children.length === 0) {
|
||||
node.type = 'raw';
|
||||
node.value = `<${node.name}${attrs} />`;
|
||||
return;
|
||||
}
|
||||
|
||||
// If the current node is a JSX <a> element, remove autolinks from its children
|
||||
// to prevent Markdown code like `<a href="/">**Go to www.example.com now!**</a>`
|
||||
// from creating a nested link to `www.example.com`
|
||||
if (node.name === 'a') {
|
||||
visit(node, 'element', (el, elIndex, elParent) => {
|
||||
const isAutolink =
|
||||
el.tagName === 'a' &&
|
||||
el.children.length === 1 &&
|
||||
el.children[0].type === 'text' &&
|
||||
el.children[0].value.match(/^(https?:\/\/|www\.)/i);
|
||||
|
||||
// If we found an autolink, remove it by replacing it with its text-only child
|
||||
if (isAutolink) {
|
||||
elParent.children.splice(elIndex, 1, el.children[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Replace the current node with its children
|
||||
// wrapped by raw opening and closing tags
|
||||
const openingTag = {
|
||||
type: 'raw',
|
||||
value: `<${node.name}${attrs}>`,
|
||||
};
|
||||
const closingTag = {
|
||||
type: 'raw',
|
||||
value: `</${node.name}>`,
|
||||
};
|
||||
parent.children.splice(index, 1, openingTag, ...node.children, closingTag);
|
||||
});
|
||||
};
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
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-legacy.
|
||||
export default function remarkEscape() {
|
||||
return (tree: any) => {
|
||||
visit(tree, 'code', removeCommentWrapper);
|
||||
visit(tree, 'inlineCode', removeCommentWrapper);
|
||||
};
|
||||
|
||||
function removeCommentWrapper(node: Literal<string>) {
|
||||
node.value = node.value.replace(/{\/\*<!--/gs, '<!--').replace(/-->\*\/}/gs, '-->');
|
||||
}
|
||||
}
|
|
@ -1,72 +0,0 @@
|
|||
// https://github.com/mdx-js/mdx/blob/main/packages/mdx/lib/plugin/remark-mark-and-unravel.js
|
||||
/**
|
||||
* @typedef {import('mdast').Root} Root
|
||||
* @typedef {import('mdast').Content} Content
|
||||
* @typedef {Root|Content} Node
|
||||
* @typedef {Extract<Node, import('unist').Parent>} Parent
|
||||
*
|
||||
* @typedef {import('remark-mdx')} DoNotTouchAsThisImportItIncludesMdxInTree
|
||||
*/
|
||||
|
||||
import { visit } from 'unist-util-visit';
|
||||
|
||||
/**
|
||||
* A tiny plugin that unravels `<p><h1>x</h1></p>` but also
|
||||
* `<p><Component /></p>` (so it has no knowledge of “HTML”).
|
||||
* It also marks JSX as being explicitly JSX, so when a user passes a `h1`
|
||||
* component, it is used for `# heading` but not for `<h1>heading</h1>`.
|
||||
*
|
||||
* @type {import('unified').Plugin<Array<void>, Root>}
|
||||
*/
|
||||
export default function remarkMarkAndUnravel() {
|
||||
return (tree: any) => {
|
||||
visit(tree, (node, index, parent_) => {
|
||||
const parent = /** @type {Parent} */ parent_;
|
||||
let offset = -1;
|
||||
let all = true;
|
||||
/** @type {boolean|undefined} */
|
||||
let oneOrMore;
|
||||
|
||||
if (parent && typeof index === 'number' && node.type === 'paragraph') {
|
||||
const children = node.children;
|
||||
|
||||
while (++offset < children.length) {
|
||||
const child = children[offset];
|
||||
|
||||
if (child.type === 'mdxJsxTextElement' || child.type === 'mdxTextExpression') {
|
||||
oneOrMore = true;
|
||||
} else if (child.type === 'text' && /^[\t\r\n ]+$/.test(String(child.value))) {
|
||||
// Empty.
|
||||
} else {
|
||||
all = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (all && oneOrMore) {
|
||||
offset = -1;
|
||||
|
||||
while (++offset < children.length) {
|
||||
const child = children[offset];
|
||||
|
||||
if (child.type === 'mdxJsxTextElement') {
|
||||
child.type = 'mdxJsxFlowElement';
|
||||
}
|
||||
|
||||
if (child.type === 'mdxTextExpression') {
|
||||
child.type = 'mdxFlowExpression';
|
||||
}
|
||||
}
|
||||
|
||||
parent.children.splice(index, 1, ...children);
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') {
|
||||
const data = node.data || (node.data = {});
|
||||
data._mdxExplicitJsx = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
import type * as fromMarkdown from 'mdast-util-from-markdown';
|
||||
import type { Tag } from 'mdast-util-mdx-jsx';
|
||||
import { mdxFromMarkdown, mdxToMarkdown } from './mdast-util-mdxish.js';
|
||||
import { mdxjs } from './mdxjs.js';
|
||||
|
||||
// Prepare markdown extensions once to prevent performance issues
|
||||
const extMdxJs = mdxjs({});
|
||||
const extMdxFromMarkdown = makeFromMarkdownLessStrict(mdxFromMarkdown());
|
||||
const extMdxToMarkdown = mdxToMarkdown();
|
||||
|
||||
export default function remarkMdxish(this: any) {
|
||||
const data = this.data();
|
||||
|
||||
add('micromarkExtensions', extMdxJs);
|
||||
add('fromMarkdownExtensions', extMdxFromMarkdown);
|
||||
add('toMarkdownExtensions', extMdxToMarkdown);
|
||||
|
||||
function add(field: string, value: unknown) {
|
||||
const list = data[field] ? data[field] : (data[field] = []);
|
||||
list.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
function makeFromMarkdownLessStrict(extensions: fromMarkdown.Extension[]) {
|
||||
extensions.forEach((extension) => {
|
||||
// Fix exit handlers that are too strict
|
||||
['mdxJsxFlowTag', 'mdxJsxTextTag'].forEach((exitHandler) => {
|
||||
if (!extension.exit || !extension.exit[exitHandler]) return;
|
||||
extension.exit[exitHandler] = chainHandlers(fixSelfClosing, extension.exit[exitHandler]);
|
||||
});
|
||||
});
|
||||
|
||||
return extensions;
|
||||
}
|
||||
|
||||
const selfClosingTags = new Set([
|
||||
'area',
|
||||
'base',
|
||||
'br',
|
||||
'col',
|
||||
'embed',
|
||||
'hr',
|
||||
'img',
|
||||
'input',
|
||||
'link',
|
||||
'meta',
|
||||
'source',
|
||||
'track',
|
||||
'wbr',
|
||||
]);
|
||||
|
||||
function fixSelfClosing(this: fromMarkdown.CompileContext) {
|
||||
const tag = this.getData('mdxJsxTag') as Tag;
|
||||
if (tag.name && selfClosingTags.has(tag.name)) tag.selfClosing = true;
|
||||
}
|
||||
|
||||
function chainHandlers(...handlers: fromMarkdown.Handle[]) {
|
||||
return function handlerChain(this: fromMarkdown.CompileContext, token: fromMarkdown.Token) {
|
||||
handlers.forEach((handler) => handler.call(this, token));
|
||||
};
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import { SKIP, visit as _visit } from 'unist-util-visit';
|
||||
|
||||
// This is a workaround.
|
||||
// It fixes a compatibility issue between different, incompatible ASTs given by plugins to Unist
|
||||
const visit = _visit as (
|
||||
node: any,
|
||||
type: string,
|
||||
callback?: (node: any, index: number, parent: any) => any
|
||||
) => any;
|
||||
|
||||
// Remove the wrapping paragraph for <astro-island> islands
|
||||
export default function remarkUnwrap() {
|
||||
const astroRootNodes = new Set();
|
||||
let insideAstroRoot = false;
|
||||
|
||||
return (tree: any) => {
|
||||
// reset state
|
||||
insideAstroRoot = false;
|
||||
astroRootNodes.clear();
|
||||
|
||||
visit(tree, 'html', (node) => {
|
||||
if (node.value.indexOf('<astro-island') > -1 && !insideAstroRoot) {
|
||||
insideAstroRoot = true;
|
||||
}
|
||||
if (node.value.indexOf('</astro-island') > -1 && insideAstroRoot) {
|
||||
insideAstroRoot = false;
|
||||
}
|
||||
astroRootNodes.add(node);
|
||||
});
|
||||
|
||||
visit(tree, 'paragraph', (node, index, parent) => {
|
||||
if (parent && typeof index === 'number' && containsAstroRootNode(node)) {
|
||||
parent.children.splice(index, 1, ...node.children);
|
||||
return [SKIP, index];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function containsAstroRootNode(node: any) {
|
||||
return node.children
|
||||
.map((child: any) => astroRootNodes.has(child))
|
||||
.reduce((all: boolean, v: boolean) => (all ? all : v), false);
|
||||
}
|
||||
}
|
|
@ -58,7 +58,6 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
|
|||
$?: {
|
||||
scopedClassName: string | null;
|
||||
};
|
||||
isAstroFlavoredMd?: boolean;
|
||||
/** Used to prevent relative image imports from `src/content/` */
|
||||
isExperimentalContentCollections?: boolean;
|
||||
/** Used to prevent relative image imports from `src/content/` */
|
||||
|
|
|
@ -33,76 +33,4 @@ describe('autolinking', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('astro-flavored md', () => {
|
||||
const renderAstroMd = (text) => renderMarkdown(text, { isAstroFlavoredMd: true });
|
||||
|
||||
it('does not autolink URLs in code blocks', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
'See `https://example.com` or `www.example.com` for more.',
|
||||
{}
|
||||
);
|
||||
|
||||
chai
|
||||
.expect(code.trim())
|
||||
.to.equal(
|
||||
`<p>See <code is:raw>https://example.com</code> or ` +
|
||||
`<code is:raw>www.example.com</code> for more.</p>`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not autolink URLs in fenced code blocks', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
'Example:\n```\nGo to https://example.com or www.example.com now.\n```'
|
||||
);
|
||||
|
||||
chai
|
||||
.expect(code)
|
||||
.to.contain(`<pre is:raw`)
|
||||
.to.contain(`Go to https://example.com or www.example.com now.`);
|
||||
});
|
||||
|
||||
it('does not autolink URLs starting with a protocol when nested inside links', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
`See [http://example.com](http://example.com) or ` +
|
||||
`<a test href="https://example.com">https://example.com</a>`
|
||||
);
|
||||
|
||||
chai
|
||||
.expect(code.replace(/\n/g, ''))
|
||||
.to.equal(
|
||||
`<p>See <a href="http://example.com">http://example.com</a> or ` +
|
||||
`<a test href="https://example.com">https://example.com</a></p>`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not autolink URLs starting with "www." when nested inside links', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
`See [www.example.com](https://www.example.com) or ` +
|
||||
`<a test href="https://www.example.com">www.example.com</a>`
|
||||
);
|
||||
|
||||
chai
|
||||
.expect(code.replace(/\n/g, ''))
|
||||
.to.equal(
|
||||
`<p>See <a href="https://www.example.com">www.example.com</a> or ` +
|
||||
`<a test href="https://www.example.com">www.example.com</a></p>`
|
||||
);
|
||||
});
|
||||
|
||||
it('does not autolink URLs when nested several layers deep inside links', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
`<a href="https://www.example.com">**Visit _our www.example.com or ` +
|
||||
`http://localhost pages_ for more!**</a>`
|
||||
);
|
||||
|
||||
chai
|
||||
.expect(code.replace(/\n/g, ''))
|
||||
.to.equal(
|
||||
`<a href="https://www.example.com"><strong>` +
|
||||
`Visit <em>our www.example.com or http://localhost pages</em> for more!` +
|
||||
`</strong></a>`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
import { renderMarkdown } from '../dist/index.js';
|
||||
import chai from 'chai';
|
||||
|
||||
describe('components', () => {
|
||||
const renderAstroMd = (text) => renderMarkdown(text, { isAstroFlavoredMd: true });
|
||||
|
||||
it('should be able to serialize string', async () => {
|
||||
const { code } = await renderAstroMd(`<Component str="cool!" />`);
|
||||
|
||||
chai.expect(code).to.equal(`<Component str="cool!" />`);
|
||||
});
|
||||
|
||||
it('should be able to serialize boolean attribute', async () => {
|
||||
const { code } = await renderAstroMd(`<Component bool={true} />`);
|
||||
|
||||
chai.expect(code).to.equal(`<Component bool={true} />`);
|
||||
});
|
||||
|
||||
it('should be able to serialize array', async () => {
|
||||
const { code } = await renderAstroMd(`<Component prop={["a", "b", "c"]} />`);
|
||||
|
||||
chai.expect(code).to.equal(`<Component prop={["a", "b", "c"]} />`);
|
||||
});
|
||||
|
||||
it('should be able to serialize object', async () => {
|
||||
const { code } = await renderAstroMd(`<Component prop={{ a: 0, b: 1, c: 2 }} />`);
|
||||
|
||||
chai.expect(code).to.equal(`<Component prop={{ a: 0, b: 1, c: 2 }} />`);
|
||||
});
|
||||
|
||||
it('should be able to serialize empty attribute', async () => {
|
||||
const { code } = await renderAstroMd(`<Component empty />`);
|
||||
|
||||
chai.expect(code).to.equal(`<Component empty />`);
|
||||
});
|
||||
|
||||
// Notable omission: shorthand attribute
|
||||
|
||||
it('should be able to serialize spread attribute', async () => {
|
||||
const { code } = await renderAstroMd(`<Component {...spread} />`);
|
||||
|
||||
chai.expect(code).to.equal(`<Component {...spread} />`);
|
||||
});
|
||||
|
||||
it('should allow client:* directives', async () => {
|
||||
const { code } = await renderAstroMd(`<Component client:load />`);
|
||||
|
||||
chai.expect(code).to.equal(`<Component client:load />`);
|
||||
});
|
||||
|
||||
it('should normalize children', async () => {
|
||||
const { code } = await renderAstroMd(`<Component bool={true}>Hello world!</Component>`);
|
||||
|
||||
chai.expect(code).to.equal(`<Component bool={true}>Hello world!</Component>`);
|
||||
});
|
||||
|
||||
it('should be able to nest components', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
`<Component bool={true}><Component>Hello world!</Component></Component>`,
|
||||
{}
|
||||
);
|
||||
|
||||
chai
|
||||
.expect(code)
|
||||
.to.equal(`<Component bool={true}><Component>Hello world!</Component></Component>`);
|
||||
});
|
||||
|
||||
it('should allow markdown without many spaces', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
`<Component>
|
||||
# Hello world!
|
||||
</Component>`,
|
||||
{}
|
||||
);
|
||||
|
||||
chai.expect(code).to.equal(`<Component><h1 id="hello-world">Hello world!</h1></Component>`);
|
||||
});
|
||||
});
|
|
@ -3,21 +3,8 @@ import { expect } from 'chai';
|
|||
|
||||
describe('entities', () => {
|
||||
it('should not unescape entities in regular Markdown', async () => {
|
||||
const { code } = await renderMarkdown(`<i>This should NOT be italic</i>`, {
|
||||
isAstroFlavoredMd: false,
|
||||
});
|
||||
const { code } = await renderMarkdown(`<i>This should NOT be italic</i>`, {});
|
||||
|
||||
expect(code).to.equal(`<p><i>This should NOT be italic</i></p>`);
|
||||
});
|
||||
|
||||
it('should not escape entities in code blocks twice in Astro-flavored markdown', async () => {
|
||||
const { code } = await renderMarkdown(`\`\`\`astro\n<h1>{x && x.name || ''}!</h1>\n\`\`\``, {
|
||||
isAstroFlavoredMd: true,
|
||||
syntaxHighlight: false,
|
||||
});
|
||||
|
||||
expect(code).to.equal(
|
||||
`<pre is:raw><code class="language-astro"><h1>{x && x.name || ''}!</h1>\n</code></pre>`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
import { renderMarkdown } from '../dist/index.js';
|
||||
import chai from 'chai';
|
||||
|
||||
describe('expressions', () => {
|
||||
const renderAstroMd = (text, opts) => renderMarkdown(text, { isAstroFlavoredMd: true, ...opts });
|
||||
|
||||
it('should be able to serialize bare expression', async () => {
|
||||
const { code } = await renderAstroMd(`{a}`, {});
|
||||
|
||||
chai.expect(code).to.equal(`{a}`);
|
||||
});
|
||||
|
||||
it('should be able to serialize expression inside component', async () => {
|
||||
const { code } = await renderAstroMd(`<Component>{a}</Component>`, {});
|
||||
|
||||
chai.expect(code).to.equal(`<Component>{a}</Component>`);
|
||||
});
|
||||
|
||||
it('should be able to serialize expression inside markdown', async () => {
|
||||
const { code } = await renderAstroMd(`# {frontmatter.title}`, {});
|
||||
|
||||
chai
|
||||
.expect(code)
|
||||
.to.equal(`<h1 id={$$slug(\`\${frontmatter.title}\`)}>{frontmatter.title}</h1>`);
|
||||
});
|
||||
|
||||
it('should be able to serialize complex expression inside markdown', async () => {
|
||||
const { code } = await renderAstroMd(`# Hello {frontmatter.name}`, {});
|
||||
|
||||
chai
|
||||
.expect(code)
|
||||
.to.equal(`<h1 id={$$slug(\`Hello \${frontmatter.name}\`)}>Hello {frontmatter.name}</h1>`);
|
||||
});
|
||||
|
||||
it('should be able to serialize complex expression with markup inside markdown', async () => {
|
||||
const { code } = await renderAstroMd(`# Hello <span>{frontmatter.name}</span>`, {});
|
||||
|
||||
chai
|
||||
.expect(code)
|
||||
.to.equal(
|
||||
`<h1 id={$$slug(\`Hello \${frontmatter.name}\`)}>Hello <span>{frontmatter.name}</span></h1>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to avoid evaluating JSX-like expressions in an inline code & generate a slug for id', async () => {
|
||||
const { code } = await renderAstroMd(`# \`{frontmatter.title}\``, {});
|
||||
|
||||
chai
|
||||
.expect(code)
|
||||
.to.equal('<h1 id="frontmattertitle"><code is:raw>{frontmatter.title}</code></h1>');
|
||||
});
|
||||
|
||||
it('should be able to avoid evaluating JSX-like expressions in inline codes', async () => {
|
||||
const { code } = await renderAstroMd(`# \`{ foo }\` is a shorthand for \`{ foo: foo }\``, {});
|
||||
|
||||
chai
|
||||
.expect(code)
|
||||
.to.equal(
|
||||
'<h1 id="-foo--is-a-shorthand-for--foo-foo"><code is:raw>{ foo }</code> is a shorthand for <code is:raw>{ foo: foo }</code></h1>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to avoid evaluating JSX-like expressions & escape HTML tag characters in inline codes', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
`###### \`{}\` is equivalent to \`Record<never, never>\` <small>(at TypeScript v{frontmatter.version})</small>`,
|
||||
{}
|
||||
);
|
||||
|
||||
chai
|
||||
.expect(code)
|
||||
.to.equal(
|
||||
`<h6 id={$$slug(\`{} is equivalent to Record<never, never> (at TypeScript v\${frontmatter.version})\`)}><code is:raw>{}</code> is equivalent to <code is:raw>Record<never, never></code> <small>(at TypeScript v{frontmatter.version})</small></h6>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to encode ampersand characters in code blocks', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
'The ampersand in ` ` must be encoded in code blocks.',
|
||||
{}
|
||||
);
|
||||
|
||||
chai
|
||||
.expect(code)
|
||||
.to.equal(
|
||||
'<p>The ampersand in <code is:raw>&nbsp;</code> must be encoded in code blocks.</p>'
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to encode ampersand characters in fenced code blocks', async () => {
|
||||
const { code } = await renderAstroMd(`
|
||||
\`\`\`md
|
||||
The ampersand in \` \` must be encoded in code blocks.
|
||||
\`\`\`
|
||||
`);
|
||||
|
||||
chai.expect(code).to.match(/^<pre is:raw.*<code>.*The ampersand in `&nbsp;`/);
|
||||
});
|
||||
|
||||
it('should be able to serialize function expression', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
`{frontmatter.list.map(item => <p id={item}>{item}</p>)}`,
|
||||
{}
|
||||
);
|
||||
|
||||
chai.expect(code).to.equal(`{frontmatter.list.map(item => <p id={item}>{item}</p>)}`);
|
||||
});
|
||||
|
||||
it('should unwrap HTML comments in inline code blocks', async () => {
|
||||
const { code } = await renderAstroMd(`\`{/*<!-- HTML comment -->*/}\``);
|
||||
|
||||
chai.expect(code).to.equal('<p><code is:raw><!-- HTML comment --></code></p>');
|
||||
});
|
||||
|
||||
it('should unwrap HTML comments in code fences', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
`
|
||||
\`\`\`
|
||||
<!-- HTML comment -->
|
||||
\`\`\`
|
||||
`
|
||||
);
|
||||
|
||||
chai.expect(code).to.match(/(?<!{\/\*)<!-- HTML comment -->(?!\*\/})/);
|
||||
});
|
||||
});
|
|
@ -1,98 +0,0 @@
|
|||
import { renderMarkdown } from '../dist/index.js';
|
||||
import chai from 'chai';
|
||||
|
||||
describe('strictness in Astro-flavored markdown', () => {
|
||||
const renderAstroMd = (text, opts) => renderMarkdown(text, { isAstroFlavoredMd: true, ...opts });
|
||||
|
||||
it('should allow self-closing HTML tags (void elements)', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
`Use self-closing void elements<br>like word<wbr>break and images: <img src="hi.jpg">`,
|
||||
{}
|
||||
);
|
||||
|
||||
chai
|
||||
.expect(code)
|
||||
.to.equal(
|
||||
`<p>Use self-closing void elements<br />like word<wbr />break and images: ` +
|
||||
`<img src="hi.jpg" /></p>`
|
||||
);
|
||||
});
|
||||
|
||||
it('should allow attribute names starting with ":" after element names', async () => {
|
||||
const { code } = await renderAstroMd(`<div :class="open ? '' : 'hidden'">Test</div>`, {});
|
||||
|
||||
chai.expect(code.trim()).to.equal(`<div :class="open ? '' : 'hidden'">Test</div>`);
|
||||
});
|
||||
|
||||
it('should allow attribute names starting with ":" after local element names', async () => {
|
||||
const { code } = await renderAstroMd(`<div.abc :class="open ? '' : 'hidden'">x</div.abc>`, {});
|
||||
|
||||
chai.expect(code.trim()).to.equal(`<div.abc :class="open ? '' : 'hidden'">x</div.abc>`);
|
||||
});
|
||||
|
||||
it('should allow attribute names starting with ":" after attribute names', async () => {
|
||||
const { code } = await renderAstroMd(`<input type="text" disabled :placeholder="hi">`, {});
|
||||
|
||||
chai.expect(code.trim()).to.equal(`<input type="text" disabled :placeholder="hi" />`);
|
||||
});
|
||||
|
||||
it('should allow attribute names starting with ":" after local attribute names', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
`<input type="text" x-test:disabled :placeholder="hi">`,
|
||||
{}
|
||||
);
|
||||
|
||||
chai.expect(code.trim()).to.equal(`<input type="text" x-test:disabled :placeholder="hi" />`);
|
||||
});
|
||||
|
||||
it('should allow attribute names starting with ":" after attribute values', async () => {
|
||||
const { code } = await renderAstroMd(`<input type="text" :placeholder="placeholder">`, {});
|
||||
|
||||
chai.expect(code.trim()).to.equal(`<input type="text" :placeholder="placeholder" />`);
|
||||
});
|
||||
|
||||
it('should allow attribute names starting with "@" after element names', async () => {
|
||||
const { code } = await renderAstroMd(`<button @click="handleClick">Test</button>`, {});
|
||||
|
||||
chai.expect(code.trim()).to.equal(`<button @click="handleClick">Test</button>`);
|
||||
});
|
||||
|
||||
it('should allow attribute names starting with "@" after local element names', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
`<button.local @click="handleClick">Test</button.local>`,
|
||||
{}
|
||||
);
|
||||
|
||||
chai.expect(code.trim()).to.equal(`<button.local @click="handleClick">Test</button.local>`);
|
||||
});
|
||||
|
||||
it('should allow attribute names starting with "@" after attribute names', async () => {
|
||||
const { code } = await renderAstroMd(`<button disabled @click="handleClick">Test</button>`, {});
|
||||
|
||||
chai.expect(code.trim()).to.equal(`<button disabled @click="handleClick">Test</button>`);
|
||||
});
|
||||
|
||||
it('should allow attribute names starting with "@" after local attribute names', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
`<button x-test:disabled @click="handleClick">Test</button>`,
|
||||
{}
|
||||
);
|
||||
|
||||
chai.expect(code.trim()).to.equal(`<button x-test:disabled @click="handleClick">Test</button>`);
|
||||
});
|
||||
|
||||
it('should allow attribute names starting with "@" after attribute values', async () => {
|
||||
const { code } = await renderAstroMd(
|
||||
`<button type="submit" @click="handleClick">Test</button>`,
|
||||
{}
|
||||
);
|
||||
|
||||
chai.expect(code.trim()).to.equal(`<button type="submit" @click="handleClick">Test</button>`);
|
||||
});
|
||||
|
||||
it('should allow attribute names containing dots', async () => {
|
||||
const { code } = await renderAstroMd(`<input x-on:input.debounce.500ms="fetchResults">`, {});
|
||||
|
||||
chai.expect(code.trim()).to.equal(`<input x-on:input.debounce.500ms="fetchResults" />`);
|
||||
});
|
||||
});
|
|
@ -3462,7 +3462,6 @@ importers:
|
|||
|
||||
packages/markdown/remark:
|
||||
specifiers:
|
||||
'@astrojs/micromark-extension-mdx-jsx': ^1.0.3
|
||||
'@astrojs/prism': ^1.0.0
|
||||
'@types/chai': ^4.3.1
|
||||
'@types/github-slugger': ^1.3.0
|
||||
|
@ -3471,19 +3470,11 @@ importers:
|
|||
'@types/mocha': ^9.1.1
|
||||
'@types/unist': ^2.0.6
|
||||
acorn: ^8.7.1
|
||||
acorn-jsx: ^5.3.2
|
||||
astro-scripts: workspace:*
|
||||
chai: ^4.3.6
|
||||
github-slugger: ^1.4.0
|
||||
hast-util-to-html: ^8.0.3
|
||||
import-meta-resolve: ^2.1.0
|
||||
mdast-util-from-markdown: ^1.2.0
|
||||
mdast-util-mdx-expression: ^1.2.1
|
||||
mdast-util-mdx-jsx: ^1.2.0
|
||||
micromark-extension-mdx-expression: ^1.0.3
|
||||
micromark-extension-mdx-md: ^1.0.0
|
||||
micromark-util-combine-extensions: ^1.0.0
|
||||
micromark-util-types: ^1.0.2
|
||||
mocha: ^9.2.2
|
||||
rehype-raw: ^6.1.1
|
||||
rehype-stringify: ^9.0.3
|
||||
|
@ -3493,23 +3484,14 @@ importers:
|
|||
remark-smartypants: ^2.0.0
|
||||
shiki: ^0.11.1
|
||||
unified: ^10.1.2
|
||||
unist-util-map: ^3.1.1
|
||||
unist-util-visit: ^4.1.0
|
||||
vfile: ^5.3.2
|
||||
dependencies:
|
||||
'@astrojs/micromark-extension-mdx-jsx': 1.0.3
|
||||
'@astrojs/prism': link:../../astro-prism
|
||||
acorn: 8.8.1
|
||||
acorn-jsx: 5.3.2_acorn@8.8.1
|
||||
github-slugger: 1.5.0
|
||||
hast-util-to-html: 8.0.3
|
||||
import-meta-resolve: 2.2.1
|
||||
mdast-util-from-markdown: 1.2.0
|
||||
mdast-util-mdx-expression: 1.3.1
|
||||
mdast-util-mdx-jsx: 1.2.0
|
||||
micromark-extension-mdx-expression: 1.0.3
|
||||
micromark-extension-mdx-md: 1.0.0
|
||||
micromark-util-combine-extensions: 1.0.0
|
||||
rehype-raw: 6.1.1
|
||||
rehype-stringify: 9.0.3
|
||||
remark-gfm: 3.0.1
|
||||
|
@ -3518,7 +3500,6 @@ importers:
|
|||
remark-smartypants: 2.0.0
|
||||
shiki: 0.11.1
|
||||
unified: 10.1.2
|
||||
unist-util-map: 3.1.2
|
||||
unist-util-visit: 4.1.1
|
||||
vfile: 5.3.6
|
||||
devDependencies:
|
||||
|
@ -3530,7 +3511,6 @@ importers:
|
|||
'@types/unist': 2.0.6
|
||||
astro-scripts: link:../../../scripts
|
||||
chai: 4.3.7
|
||||
micromark-util-types: 1.0.2
|
||||
mocha: 9.2.2
|
||||
|
||||
packages/telemetry:
|
||||
|
|
Loading…
Reference in a new issue