Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
NODE_OPTIONS: --max-old-space-size=4096
VERCEL_ENV: production

- name: Audit Markdown components
- name: Audit generated Markdown
run: pnpm run check:markdown

- name: Check generated public links
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"unplugin-auto-import": "^21.0.0",
"unplugin-icons": "^23.0.1",
"viem": "^2.54.6",
"vocs": "https://pkg.pr.new/wevm/vocs/vocs@e1a70dc00507fade986b2f9b033f26906d037748",
"vocs": "https://pkg.pr.new/wevm/vocs/vocs@60cc0b17cd78f4cf8da0eb20f47e64f4d701e028",
"wagmi": "3.6.20",
"waku": "^1.0.0-beta.6",
"webauthx": "~0.1.2",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 22 additions & 3 deletions scripts/check-markdown-components.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -57,24 +57,43 @@ if (unresolvedIncludes.length > 0) {
}

const generatedComponents = new Set()
const generatedEsmFiles = new Set()
const generatedExpressions = new Set()
const generatedPresentationElements = []
for (const file of generatedFiles) {
const content = await readFile(file, 'utf8')
const parseableContent = maskHtmlComments(content)
const tree = unified().use(remarkParse).use(remarkMdx).parse(parseableContent)
visit(tree, (node) => {
if (node.type === 'mdxjsEsm') generatedEsmFiles.add(file)
if (node.type === 'mdxFlowExpression' || node.type === 'mdxTextExpression')
generatedExpressions.add(file)
if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') return
if (/^(?:meta|script|style|title)$/.test(node.name ?? ''))
generatedPresentationElements.push({ file, name: node.name })
if (!/^[A-Z][A-Za-z0-9]*(?:\.[A-Za-z0-9]+)*$/.test(node.name ?? '')) return
generatedComponents.add(node.name)
})
}

if (generatedComponents.size > 0) {
console.error('Generated Markdown component audit failed.')
if (
generatedComponents.size > 0 ||
generatedEsmFiles.size > 0 ||
generatedExpressions.size > 0 ||
generatedPresentationElements.length > 0
) {
console.error('Generated Markdown syntax audit failed.')
for (const name of generatedComponents) console.error(`- ${name}: unresolved component`)
for (const file of generatedEsmFiles) console.error(`- ${file}: executable import or export`)
for (const file of generatedExpressions) console.error(`- ${file}: executable expression`)
for (const { file, name } of generatedPresentationElements)
console.error(`- ${file}: presentation-only <${name}> element`)
process.exit(1)
}

console.log('Markdown output audit passed (no unresolved component types or includes).')
console.log(
'Markdown output audit passed (no unresolved components, includes, executable MDX, or presentation-only elements).',
)

function maskHtmlComments(content) {
const ranges = []
Expand Down
51 changes: 51 additions & 0 deletions src/lib/markdown-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,57 @@ Status: <Badge variant="red">Required</Badge>
expect(output).not.toMatch(/<\/?[A-Z]/)
})

test('removes executable and presentation-only MDX without dropping later content', async () => {
const output = await render(`
import { Demo } from './Demo'
export const data = [{ label: 'Example' }]

<style>{\`
.tabs { display: flex }
\`}</style>
<script>{\`window.example = true\`}</script>
<meta name="robots" content="index" />
<title>Browser title</title>

# Agent guide

The machine-readable content remains available.

| URL | Contents |
| --- | --- |
| /llms.txt | Documentation index |
`)

expect(output).not.toContain('import { Demo }')
expect(output).not.toContain('export const data')
expect(output).not.toContain('.tabs')
expect(output).not.toMatch(/<(?:meta|script|style|title)\b/)
expect(output).toContain('# Agent guide')
expect(output).toContain('The machine-readable content remains available.')
expect(output).toContain('/llms.txt')
expect(output).toContain('Documentation index')
})

test('keeps MDX-like syntax inside fenced examples', async () => {
const output = await render(`
\`\`\`mdx
import { Demo } from './Demo'
export const data = [{ label: 'Example' }]
<style>{styles}</style>
<script>{setup}</script>
<meta name="robots" content="index" />
<title>Browser title</title>
\`\`\`
`)

expect(output).toContain("import { Demo } from './Demo'")
expect(output).toContain("export const data = [{ label: 'Example' }]")
expect(output).toContain('<style>{styles}</style>')
expect(output).toContain('<script>{setup}</script>')
expect(output).toContain('<meta name="robots" content="index" />')
expect(output).toContain('<title>Browser title</title>')
})

test('expands code includes and removes region markers', async () => {
const output = await render(`
\`\`\`ts
Expand Down
5 changes: 4 additions & 1 deletion src/lib/markdown-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type MarkdownNode = {
}

const openApiSpecUrl = 'https://api.tempo.xyz/openapi.json'
const presentationOnlyElements = new Set(['meta', 'script', 'style', 'title'])

const interactiveDescriptions: Record<string, string> = {
ConnectWallet: 'Connect a wallet in the interactive web page.',
Expand Down Expand Up @@ -131,12 +132,15 @@ function rewriteNode(
headingDepth: number,
getSnippet: (fileName: string) => string | undefined,
): MarkdownNode[] {
if (node.type === 'mdxjsEsm') return []

if (node.type !== 'mdxJsxFlowElement' && node.type !== 'mdxJsxTextElement') {
if (node.type === 'code' && node.value) node.value = inlineCodeSnippets(node.value, getSnippet)
rewriteChildren(node, headingDepth, getSnippet)
return [node]
}

if (node.name && presentationOnlyElements.has(node.name)) return []
if (node.name === 'Cards') return renderCards(node, headingDepth, getSnippet)
if (node.name === 'Card') return [paragraph(cardContent(node))]
if (node.name === 'Tabs') return renderTabs(node, headingDepth, getSnippet)
Expand All @@ -152,7 +156,6 @@ function rewriteNode(
if (node.name && interactiveDescriptions[node.name])
return [paragraph([text(interactiveDescriptions[node.name])])]

if (node.name === 'title') return []
if (isLayoutElement(node)) {
rewriteChildren(node, headingDepth, getSnippet)
return node.children ?? []
Expand Down
Loading