mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(site): extract api reference from components (#464)
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { renderInlineMarkdown } from '../renderInlineMarkdown';
|
||||
|
||||
describe('renderInlineMarkdown', () => {
|
||||
it('returns plain text for a simple sentence', () => {
|
||||
expect(renderInlineMarkdown('Whether the button is disabled.')).toBe('Whether the button is disabled.');
|
||||
});
|
||||
|
||||
it('unwraps a single paragraph', () => {
|
||||
const result = renderInlineMarkdown('Hello **world**.');
|
||||
expect(result).not.toMatch(/^<p/);
|
||||
expect(result).toContain('<strong class="font-semibold">world</strong>');
|
||||
});
|
||||
|
||||
it('preserves multiple paragraphs', () => {
|
||||
const result = renderInlineMarkdown('First paragraph.\n\nSecond paragraph.');
|
||||
expect(result).toContain('<p');
|
||||
expect(result).toMatch(/First paragraph/);
|
||||
expect(result).toMatch(/Second paragraph/);
|
||||
});
|
||||
|
||||
it('renders inline code with correct classes', () => {
|
||||
const result = renderInlineMarkdown('Use `foo` here.');
|
||||
expect(result).toContain('<code');
|
||||
expect(result).toContain('font-mono');
|
||||
expect(result).toContain('text-code');
|
||||
expect(result).toContain('foo');
|
||||
});
|
||||
|
||||
it('renders strong text', () => {
|
||||
const result = renderInlineMarkdown('**bold text**');
|
||||
expect(result).toContain('<strong class="font-semibold">bold text</strong>');
|
||||
});
|
||||
|
||||
it('renders emphasized text', () => {
|
||||
const result = renderInlineMarkdown('*italic text*');
|
||||
expect(result).toContain('<em class="font-medium">italic text</em>');
|
||||
});
|
||||
|
||||
it('renders links with correct classes', () => {
|
||||
const result = renderInlineMarkdown('[link](https://example.com)');
|
||||
expect(result).toContain('href="https://example.com"');
|
||||
expect(result).toContain('underline');
|
||||
expect(result).toContain('intent:no-underline');
|
||||
});
|
||||
|
||||
it('renders unordered lists', () => {
|
||||
const result = renderInlineMarkdown('- item one\n- item two');
|
||||
expect(result).toContain('<ul');
|
||||
expect(result).toContain('list-disc');
|
||||
expect(result).toContain('<li');
|
||||
expect(result).toContain('item one');
|
||||
expect(result).toContain('item two');
|
||||
});
|
||||
|
||||
it('renders ordered lists', () => {
|
||||
const result = renderInlineMarkdown('1. first\n2. second');
|
||||
expect(result).toContain('<ol');
|
||||
expect(result).toContain('list-decimal');
|
||||
expect(result).toContain('first');
|
||||
expect(result).toContain('second');
|
||||
});
|
||||
|
||||
it('downgrades headings to paragraphs', () => {
|
||||
const result = renderInlineMarkdown('# Heading');
|
||||
expect(result).not.toContain('<h1');
|
||||
expect(result).toContain('Heading');
|
||||
});
|
||||
|
||||
it('suppresses horizontal rules', () => {
|
||||
const result = renderInlineMarkdown('before\n\n---\n\nafter');
|
||||
expect(result).not.toContain('<hr');
|
||||
});
|
||||
|
||||
it('renders a paragraph followed by a list', () => {
|
||||
const md = 'The volume level:\n\n- `0` — muted\n- `1` — max';
|
||||
const result = renderInlineMarkdown(md);
|
||||
expect(result).toContain('<p');
|
||||
expect(result).toContain('<ul');
|
||||
expect(result).toContain('<code');
|
||||
});
|
||||
|
||||
it('renders fenced code blocks as inline code', () => {
|
||||
const result = renderInlineMarkdown('```\nconst x = 1;\n```');
|
||||
expect(result).toContain('<code');
|
||||
expect(result).toContain('const x = 1;');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Marked, type MarkedExtension, type Tokens } from 'marked';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
import { shared } from '@/components/typography/styles';
|
||||
|
||||
const classes = {
|
||||
p: 'mt-3 first:mt-0',
|
||||
ul: twMerge(shared.ul, 'mt-3'),
|
||||
ol: twMerge(shared.ol, 'mt-3'),
|
||||
li: shared.li,
|
||||
code: shared.code,
|
||||
strong: shared.strong,
|
||||
em: shared.em,
|
||||
a: shared.a,
|
||||
} as const;
|
||||
|
||||
const renderer: MarkedExtension['renderer'] = {
|
||||
// --- Supported elements ---
|
||||
|
||||
paragraph({ tokens }) {
|
||||
return `<p class="${classes.p}">${this.parser.parseInline(tokens)}</p>`;
|
||||
},
|
||||
|
||||
list(token: Tokens.List) {
|
||||
const tag = token.ordered ? 'ol' : 'ul';
|
||||
const cls = token.ordered ? classes.ol : classes.ul;
|
||||
const body = token.items.map((item) => this.listitem(item)).join('\n');
|
||||
return `<${tag} class="${cls}">${body}</${tag}>`;
|
||||
},
|
||||
|
||||
listitem(item: Tokens.ListItem) {
|
||||
let body = this.parser.parse(item.tokens, !!item.loose);
|
||||
if (!item.loose) {
|
||||
body = body.replace(/^<p class="[^"]*">/, '').replace(/<\/p>$/, '');
|
||||
}
|
||||
return `<li class="${classes.li}">${body}</li>`;
|
||||
},
|
||||
|
||||
code({ text }) {
|
||||
return `<code class="${classes.code}">${text}</code>`;
|
||||
},
|
||||
|
||||
codespan({ text }) {
|
||||
return `<code class="${classes.code}">${text}</code>`;
|
||||
},
|
||||
|
||||
strong({ tokens }) {
|
||||
return `<strong class="${classes.strong}">${this.parser.parseInline(tokens)}</strong>`;
|
||||
},
|
||||
|
||||
em({ tokens }) {
|
||||
return `<em class="${classes.em}">${this.parser.parseInline(tokens)}</em>`;
|
||||
},
|
||||
|
||||
link({ href, tokens }) {
|
||||
return `<a href="${href}" class="${classes.a}">${this.parser.parseInline(tokens)}</a>`;
|
||||
},
|
||||
|
||||
// --- Unsupported elements — downgrade or suppress ---
|
||||
|
||||
heading({ tokens }) {
|
||||
return `<p class="${classes.p}">${this.parser.parseInline(tokens)}</p>`;
|
||||
},
|
||||
|
||||
blockquote({ tokens }) {
|
||||
return this.parser.parse(tokens);
|
||||
},
|
||||
|
||||
hr() {
|
||||
return '';
|
||||
},
|
||||
|
||||
image({ href, text }) {
|
||||
return text || href;
|
||||
},
|
||||
|
||||
table() {
|
||||
return '';
|
||||
},
|
||||
|
||||
tablerow() {
|
||||
return '';
|
||||
},
|
||||
|
||||
tablecell() {
|
||||
return '';
|
||||
},
|
||||
};
|
||||
|
||||
const marked = new Marked({ renderer });
|
||||
|
||||
/**
|
||||
* Unwrap a single `<p>` wrapper so simple descriptions sit inline.
|
||||
*
|
||||
* If the output is a lone `<p class="…">…</p>` with no other block-level
|
||||
* elements, strip the wrapper and return only the inner content.
|
||||
*/
|
||||
function unwrapSingleParagraph(html: string): string {
|
||||
const trimmed = html.trim();
|
||||
const match = trimmed.match(/^<p class="[^"]*">([\s\S]*)<\/p>$/);
|
||||
if (match && !trimmed.includes('<p', 1)) {
|
||||
return match[1]!;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
export function renderInlineMarkdown(markdown: string): string {
|
||||
const raw = marked.parse(markdown);
|
||||
if (typeof raw !== 'string') {
|
||||
return markdown;
|
||||
}
|
||||
return unwrapSingleParagraph(raw);
|
||||
}
|
||||
Reference in New Issue
Block a user