Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions __tests__/lib/mdxish/mdxish-jsx-to-mdast.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,33 @@ describe('mdxish-jsx-to-mdast transformer', () => {
expect(imageNode.data?.hProperties?.border).toBeUndefined();
});

it('should parse Image with unquoted attributes containing special characters', () => {
const md = '<Image src=https://example.com/image.png alt=test />';
const ast = processWithNewTypes(md);

expect(ast.children).toHaveLength(1);
expect(ast.children[0].type).toBe(NodeTypes.imageBlock);

const imageNode = ast.children[0] as ImageBlock;
expect(imageNode.data?.hProperties?.src).toBe('https://example.com/image.png');
expect(imageNode.data?.hProperties?.alt).toBe('test');
});

it('should parse Image with unquoted attributes spanning multiple lines', () => {
const md = `<Image
src=https://example.com/image.png
alt=test
/>`;
const ast = processWithNewTypes(md);

expect(ast.children).toHaveLength(1);
expect(ast.children[0].type).toBe(NodeTypes.imageBlock);

const imageNode = ast.children[0] as ImageBlock;
expect(imageNode.data?.hProperties?.src).toBe('https://example.com/image.png');
expect(imageNode.data?.hProperties?.alt).toBe('test');
});

it('should parse caption with markdown and HTML entities into children', () => {
const md = '<Image src="test.png" alt="test" caption="With **Default Handling** enabled, the `default` value &#x22;Buster&#x22; is used." />';
const ast = processWithNewTypes(md);
Expand Down Expand Up @@ -131,6 +158,20 @@ This is a warning message.
expect(calloutNode.children).toBeDefined();
expect(calloutNode.children.length).toBeGreaterThan(0);
});

it('should parse Callout with unquoted attributes containing special characters', () => {
const md = `<Callout icon=📘 theme=info>
content
</Callout>`;
const ast = processWithNewTypes(md);

expect(ast.children).toHaveLength(1);
expect(ast.children[0].type).toBe(NodeTypes.callout);

const calloutNode = ast.children[0] as Callout;
expect(calloutNode.data?.hProperties?.icon).toBe('📘');
expect(calloutNode.data?.hProperties?.theme).toBe('info');
});
});

describe('Embed component', () => {
Expand Down Expand Up @@ -197,6 +238,18 @@ This is a warning message.
expect(embedNode.data?.hProperties?.url).toBe(url);
});
});

it('should parse Embed with unquoted attributes containing special characters', () => {
const md = '<Embed url=https://example.com title=Example />';
const ast = processWithNewTypes(md);

expect(ast.children).toHaveLength(1);
expect(ast.children[0].type).toBe(NodeTypes.embedBlock);

const embedNode = ast.children[0] as EmbedBlock;
expect(embedNode.data?.hProperties?.url).toBe('https://example.com');
expect(embedNode.data?.hProperties?.title).toBe('Example');
});
});

describe('Anchor component', () => {
Expand Down Expand Up @@ -257,6 +310,17 @@ This is a warning message.
expect(anchorNode.data?.hProperties?.href).toBe('https://readme.com');
expect(anchorNode.children).toHaveLength(0);
});

it('should parse Anchor with unquoted attributes containing special characters', () => {
const md = '<Anchor href=https://readme.com>ReadMe</Anchor>';
const ast = processWithNewTypes(md);

const para = ast.children[0] as Paragraph;
const anchorNode = para.children.find(c => c.type === NodeTypes.anchor) as Anchor;
expect(anchorNode).toBeDefined();
expect(anchorNode.data?.hProperties?.href).toBe('https://readme.com');
expect(anchorNode.children[0]).toMatchObject({ type: 'text', value: 'ReadMe' });
});
});

describe('Recipe component', () => {
Expand All @@ -282,6 +346,18 @@ This is a warning message.
expect(recipeNode.emoji).toBe('🍳');
expect(recipeNode.backgroundColor).toBe('#fff');
});

it('should parse Recipe with unquoted attributes containing special characters', () => {
const md = '<Recipe slug=my-recipe title=Recipe link=https://example.com/recipe />';
const ast = processWithNewTypes(md);

expect(ast.children).toHaveLength(1);
expect(ast.children[0].type).toBe(NodeTypes.recipe);

const recipeNode = ast.children[0] as Recipe;
expect(recipeNode.slug).toBe('my-recipe');
expect(recipeNode.title).toBe('Recipe');
});
});

describe('unknown components', () => {
Expand Down
58 changes: 58 additions & 0 deletions lib/mdast-util/pascalcase-html-block/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { HTML } from 'mdast';
import type { CompileContext, Extension as FromMarkdownExtension, Handle, Token } from 'mdast-util-from-markdown';

const contextMap = new WeakMap<Token, { chunks: string[] }>();

function findBlockToken(this: CompileContext): Token | undefined {
const events = this.tokenStack;
for (let i = events.length - 1; i >= 0; i -= 1) {
if (events[i][0].type === 'jsxComponentBlock') return events[i][0];
}
return undefined;
}

function enterBlock(this: CompileContext, token: Parameters<Handle>[0]): void {
contextMap.set(token, { chunks: [] });
this.enter({ type: 'html', value: '' } as HTML, token);
}

function exitBlockData(this: CompileContext, token: Parameters<Handle>[0]): void {
const blockToken = findBlockToken.call(this);
if (!blockToken) return;
const ctx = contextMap.get(blockToken);
if (ctx) ctx.chunks.push(this.sliceSerialize(token));
}

function exitBlock(this: CompileContext, token: Parameters<Handle>[0]): void {
const ctx = contextMap.get(token);
const node = this.stack[this.stack.length - 1] as HTML;
if (ctx) {
node.value = ctx.chunks.join('\n');
contextMap.delete(token);
}
this.exit(token);
}

function enterText(this: CompileContext, token: Parameters<Handle>[0]): void {
this.enter({ type: 'html', value: '' } as HTML, token);
}

function exitText(this: CompileContext, token: Parameters<Handle>[0]): void {
const node = this.stack[this.stack.length - 1] as HTML;
node.value = this.sliceSerialize(token);
this.exit(token);
}

export function jsxComponentBlockFromMarkdown(): FromMarkdownExtension {
return {
enter: {
jsxComponentBlock: enterBlock,
jsxComponentText: enterText,
},
exit: {
jsxComponentBlockData: exitBlockData,
jsxComponentBlock: exitBlock,
jsxComponentText: exitText,
},
};
}
8 changes: 6 additions & 2 deletions lib/mdxish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,12 @@ import { emptyTaskListItemFromMarkdown } from './mdast-util/empty-task-list-item
import { jsxTableFromMarkdown } from './mdast-util/jsx-table';
import { legacyVariableFromMarkdown } from './mdast-util/legacy-variable';
import { magicBlockFromMarkdown } from './mdast-util/magic-block';
import { jsxComponentBlockFromMarkdown } from './mdast-util/pascalcase-html-block';
import { jsxTable } from './micromark/jsx-table';
import { legacyVariable } from './micromark/legacy-variable';
import { looseHtmlEntity, looseHtmlEntityFromMarkdown } from './micromark/loose-html-entities';
import { magicBlock } from './micromark/magic-block';
import { jsxComponentBlock } from './micromark/pascalcase-html-block';
import { loadComponents } from './utils/mdxish/mdxish-load-components';
import { protectCodeBlocks, restoreCodeBlocks } from './utils/mdxish/protect-code-blocks';

Expand Down Expand Up @@ -151,20 +153,22 @@ export function mdxishAstProcessor(mdContent: string, opts: MdxishOpts = {}) {
.data(
'micromarkExtensions',
safeMode
? [jsxTable(), magicBlock(), legacyVariable(), looseHtmlEntity()]
: [jsxTable(), magicBlock(), mdxExprTextOnly, legacyVariable(), looseHtmlEntity()],
? [jsxComponentBlock(), jsxTable(), magicBlock(), legacyVariable(), looseHtmlEntity()]
: [jsxComponentBlock(), jsxTable(), magicBlock(), mdxExprTextOnly, legacyVariable(), looseHtmlEntity()],
)
.data(
'fromMarkdownExtensions',
safeMode
? [
jsxComponentBlockFromMarkdown(),
jsxTableFromMarkdown(),
magicBlockFromMarkdown(),
legacyVariableFromMarkdown(),
emptyTaskListItemFromMarkdown(),
looseHtmlEntityFromMarkdown(),
]
: [
jsxComponentBlockFromMarkdown(),
jsxTableFromMarkdown(),
magicBlockFromMarkdown(),
mdxExpressionFromMarkdown(),
Expand Down
1 change: 1 addition & 0 deletions lib/micromark/pascalcase-html-block/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { jsxComponentBlock } from './syntax';
Loading
Loading