Remove Astro-flavored Markdown from @astrojs/markdown-remark (#5785)

This commit is contained in:
Chris Swithinbank 2023-01-09 10:23:21 +01:00 committed by GitHub
parent 54076a41eb
commit 16107b6a10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 12 additions and 842 deletions

View file

@ -0,0 +1,5 @@
---
'@astrojs/markdown-remark': major
---
Drop support for legacy Astro-flavored Markdown

View file

@ -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,

View file

@ -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"
}
}

View file

@ -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 {

View file

@ -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],
};
}

View file

@ -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]);
}

View file

@ -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 });

View file

@ -1,22 +0,0 @@
import { SKIP, visit } from 'unist-util-visit';
export function escapeEntities(value: string): string {
return value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
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;
}
});
};
}

View file

@ -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;
});
};
}

View file

@ -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;
}
});
}
});
};
}

View file

@ -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);
});
};
}

View file

@ -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, '-->');
}
}

View file

@ -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;
}
});
};
}

View file

@ -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));
};
}

View file

@ -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);
}
}

View file

@ -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/` */

View file

@ -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>`
);
});
});
});

View file

@ -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>`);
});
});

View file

@ -3,21 +3,8 @@ import { expect } from 'chai';
describe('entities', () => {
it('should not unescape entities in regular Markdown', async () => {
const { code } = await renderMarkdown(`&lt;i&gt;This should NOT be italic&lt;/i&gt;`, {
isAstroFlavoredMd: false,
});
const { code } = await renderMarkdown(`&lt;i&gt;This should NOT be italic&lt;/i&gt;`, {});
expect(code).to.equal(`<p>&#x3C;i>This should NOT be italic&#x3C;/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">&lt;h1&gt;{x &amp;&amp; x.name || ''}!&lt;/h1&gt;\n</code></pre>`
);
});
});

View file

@ -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&lt;never, never&gt; (at TypeScript v\${frontmatter.version})\`)}><code is:raw>{}</code> is equivalent to <code is:raw>Record&lt;never, never&gt;</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 `&nbsp;` must be encoded in code blocks.',
{}
);
chai
.expect(code)
.to.equal(
'<p>The ampersand in <code is:raw>&amp;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 \`&nbsp;\` must be encoded in code blocks.
\`\`\`
`);
chai.expect(code).to.match(/^<pre is:raw.*<code>.*The ampersand in `&amp;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>&lt;!-- HTML comment --&gt;</code></p>');
});
it('should unwrap HTML comments in code fences', async () => {
const { code } = await renderAstroMd(
`
\`\`\`
<!-- HTML comment -->
\`\`\`
`
);
chai.expect(code).to.match(/(?<!{\/\*)&lt;!-- HTML comment --&gt;(?!\*\/})/);
});
});

View file

@ -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" />`);
});
});

View file

@ -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: