import React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import MarkdownContent, { getCodeBlockLanguage, getCodeBlockText, getMappedSource, parseCodeBlockMeta, resolveCodeBlockFenceInfo } from '../src/renderer/src/components/MarkdownContent' import MarkdownIt from 'markdown-it' import { getPrismLanguage, prismLanguageLoaders } from '../src/renderer/src/workbuddy/markdown/languages' let failures = 0 function check(label: string, condition: boolean, detail = ''): void { if (condition) console.log(` ✓ ${label}`) else { failures++ console.error(` ✗ ${label}${detail ? ` — ${detail}` : ''}`) } } function render(content: string, options: { streaming?: boolean; onQuote?: (markdown: string) => void } = {}): string { // Original OpacityText retains plain span wrappers after completion; compare textual structure without those wrappers. return renderToStaticMarkup().replace(/([^<]*)<\/span>/g, '$1') } async function main(): Promise { await prismLanguageLoaders.typescript() await prismLanguageLoaders.json() console.log('\n[1] Markdown structure and raw HTML fallback') const structure = render( '# Heading\n\nParagraph with **bold**, _italic_, ~~strike~~, and `inline`.\nLine two.\n\n> quoted\n\n---\n\n#### Small heading' ) check('renders headings, paragraphs, emphasis, strike, inline code, quote, break, and rule', structure.includes('

') && structure.includes('

') && structure.includes('bold') && structure.includes('italic') && structure.includes('strike') && structure.includes('markdown-content__inline-code') && structure.includes('')) const ordinaryHtml = render('bold text') check('ordinary raw HTML tags are stripped while their text remains', ordinaryHtml.includes('bold text') && !ordinaryHtml.includes('alert(1)after ') check('dangerous raw HTML containers suppress their contents', dangerousHtml.includes('before') && dangerousHtml.includes('after') && !dangerousHtml.includes('alert(1)') && !dangerousHtml.includes('hidden') && !dangerousHtml.includes('frame text')) const unclosedDangerousHtml = render('beforehidden to end') check('unclosed dangerous raw HTML suppresses content through the end', unclosedDangerousHtml.includes('before') && !unclosedDangerousHtml.includes('hidden to end')) const fallbackTags = render('line
next underlined') check('br becomes a hard break and u becomes underline markup', fallbackTags.includes('underlined')) const htmlInCode = render('`inline`\n\n```html\nfenced\n```') check('HTML inside inline and fenced code remains literal code', htmlInCode.includes('<b>inline</b>') && htmlInCode.includes('<') && htmlInCode.includes('</') && htmlInCode.includes('fenced'), htmlInCode) console.log('\n[2] Lists, tasks, and tables') const lists = render('- [x] complete\n- [ ] pending\n - nested item') check('task checkboxes are disabled and preserve checked state', lists.includes('type="checkbox"') && lists.includes('disabled=""') && lists.includes('checked=""')) check('nested lists render as nested list elements', (lists.match(/)/g) ?? []).length === 2 && lists.includes('

  • nested item
  • ')) const tableSource = '| Name | Value |\n| --- | ---: |\n| alpha | 2 |' const table = render(tableSource, { onQuote: () => undefined }) check('GFM table renders with a horizontal-scroll wrapper', table.includes(' token.type === 'fence') check('visible language labels retain js, ts, py, and txt while verified aliases normalize', getCodeBlockLanguage('js title=demo') === 'js' && getCodeBlockLanguage('ts') === 'ts' && getCodeBlockLanguage('py') === 'py' && getCodeBlockLanguage('txt') === 'txt' && getCodeBlockLanguage('c++') === 'cpp' && getCodeBlockLanguage('c#') === 'csharp' && getCodeBlockLanguage('echarts-data') === 'json' && getCodeBlockLanguage('shell') === 'bash' && getCodeBlockLanguage('html') === 'markup' && getCodeBlockLanguage('svg') === 'markup' && getCodeBlockLanguage('') === 'plaintext' && getCodeBlockLanguage('custom') === 'custom') check('short visible labels still select their Prism grammars', !!getPrismLanguage('js') && !!getPrismLanguage('ts')) check('copy text preserves leading spaces and trailing blank lines', !!codeToken && getCodeBlockText(codeToken) === rawCode) const fenced = render(`\`\`\`ts\n${rawCode}\`\`\``) check('fenced code preserves visible language label and uses Prism tokens', fenced.includes('>ts') && fenced.includes('language-ts') && fenced.includes('token') && fenced.includes('aria-label="复制"')) const metadata = parseCodeBlockMeta('ts:4:7:C:\\project\\src\\App.ts') check('language metadata parses Windows paths and file basenames', metadata?.metaLanguage === 'ts' && metadata.filePath === 'C:\\project\\src\\App.ts' && metadata.fileName === 'App.ts') const metadataFence = render('```ts:4:7:C:\\project\\src\\App.ts\nconst value: number = 1\n```') check('metadata fence shows filename and line range with the integrated host click handler', metadataFence.includes('markdown-content__code-file-title') && metadataFence.includes('title="4:7:C:\\project\\src\\App.ts"') && metadataFence.includes('App.ts') && metadataFence.includes('L4-L7') && metadataFence.includes('cr-code-block__file-title--clickable')) const inferredFenceInfo = resolveCodeBlockFenceInfo('1:0:C:\\project\\data\\payload.json') const inferredFence = render('```1:0:C:\\project\\data\\payload.json\n{"ok": true}\n```') check('metadata without a language infers Prism grammar from the file extension and clamps lines', inferredFenceInfo.language === 'json' && inferredFence.includes('title="1:1:C:\\project\\data\\payload.json"') && inferredFence.includes('language-json') && inferredFence.includes('token property')) const rangeMetadataFence = render('```echarts-data:2-4:charts/series.json\n{"series": []}\n```') check('language-range metadata format renders file context and normalized json language', rangeMetadataFence.includes('title="2:4:charts/series.json"') && rangeMetadataFence.includes('series.json') && rangeMetadataFence.includes('language-json')) const indented = render(' print("hello")') check('indented code uses the same plaintext code block renderer', indented.includes('markdown-content__code-block') && indented.includes('plaintext')) const streaming = render('```ts\nconst pending: number = 1', { streaming: true }) check('an incomplete streaming fence remains renderable', streaming.includes('data-streaming="true"') && streaming.includes('markdown-content__code-block')) check('streaming cues use six UTF-16 units before converting to characters, outside fenced code', !streaming.includes('markdown-content__loading-char') && (render('Prefix abcde🙂', { streaming: true }).match(/class="markdown-content__loading-char"/g) ?? []).length === 5) console.log('\n[4] Safe links and linkify settings') const links = render('[https](https://example.com) [mail](mailto:user@example.com) [bad](javascript:alert(1)) www.example.com user@example.com') check('HTTP(S) and mailto links open safely in a new tab', (links.match(/target="_blank" rel="noreferrer"/g) ?? []).length === 2) check('unsafe schemes never receive a runnable href', !/href="(?:javascript|data|file):/i.test(links)) const fuzzy = render('www.example.com user@example.com') check('fuzzy bare domains and email addresses stay unlinked', !fuzzy.includes('https://example.com中文/path'), cjkHostname) const cjkPath = render('https://example.com/path中文') check('autolink host repair does not split non-ASCII path text', cjkPath.includes('href="https://example.com/path%E4%B8%AD%E6%96%87"') && cjkPath.includes('>https://example.com/path中文')) const cjkEmphasis = render('**中文:**继续中文 and **😀文字**继续') check('CJK-friendly emphasis handles punctuation and astral characters', cjkEmphasis.includes('中文:继续中文') && cjkEmphasis.includes('😀文字继续'), cjkEmphasis) const localImage = render('![local](file:///private/image.png)') const remoteImage = render('![remote](https://example.com/image.png)') check('images initially show the source-derived loading fallback and do not expose local file URLs', remoteImage.includes('aria-busy="true"') && remoteImage.includes('remote') && !localImage.includes('src="file:')) console.log(failures === 0 ? '\nMarkdown regression checks passed.\n' : `\n${failures} Markdown regression check(s) failed.\n`) process.exitCode = failures === 0 ? 0 : 1 } void main().catch((error) => { console.error(error); process.exitCode = 1 })