Files
ycode/scripts/recovered-parser-regression.ts

158 lines
7.1 KiB
TypeScript

import assert from 'node:assert/strict'
import { createMarkdownParser } from '../src/renderer/src/workbuddy/markdown/parser'
import {
parseCodeBlockMeta,
resolveCodeBlockFenceInfo
} from '../src/renderer/src/workbuddy/markdown/code-fence'
import {
ensurePrismLanguage,
getMdReadyVersion,
getMdSyntaxVersion,
getPrismLanguage,
isPrismLanguageLoaded,
onPrismLanguageLoaded,
subscribeMdReady
} from '../src/renderer/src/workbuddy/markdown/languages'
const md = createMarkdownParser()
const fileMeta = parseCodeBlockMeta('ts:4:8:src/app.ts')
assert.deepEqual(fileMeta, {
startLine: 4,
endLine: 8,
filePath: 'src/app.ts',
fileName: 'app.ts',
metaLanguage: 'ts'
})
assert.deepEqual(resolveCodeBlockFenceInfo('4:8:README'), {
language: '',
meta: '4:8:README',
fileMeta: {
startLine: 4,
endLine: 8,
filePath: 'README',
fileName: 'README',
metaLanguage: undefined
}
})
assert.equal(resolveCodeBlockFenceInfo('').language, '')
const codeTokens = md.parse('```ts:4:8:src/app.ts\nconst answer = 42\n```', {})
const fence = codeTokens.find((token) => token.type === 'fence')
assert.ok(fence)
assert.equal(fence.info, 'ts')
assert.equal(fence.attrGet('data-meta'), 'ts:4:8:src/app.ts')
assert.equal(fence.attrGet('data-id'), `code-${codeTokens.indexOf(fence)}`)
assert.equal(fence.attrGet('data-content'), 'const answer = 42\n')
assert.match(md.render('```js\nconst answer = 42\n```'), /class="token keyword"|class="token keyword keyword"/)
assert.doesNotMatch(md.render('reply answer'), /class="loading-dot"/)
const indentedCode = md.parse(' plain code\n', {})
const convertedCode = indentedCode.find((token) => token.type === 'fence')
assert.ok(convertedCode)
assert.equal(convertedCode.info, 'plaintext')
assert.equal(convertedCode.tag, 'code')
const equalsHeading = md.parse('Recovered heading\n===\n', {})
assert.ok(equalsHeading.some((token) => token.type === 'heading_open' && token.tag === 'h1'))
const dashHeading = md.parse('Not a recovered equals heading\n---\n', {})
assert.ok(!dashHeading.some((token) => token.type === 'heading_open' && token.tag === 'h2'))
const taskTokens = md.parse('- [x] completed\n- [ ] pending\n', {})
assert.equal(taskTokens.filter((token) => token.type === 'list_item_open' && token.attrGet('class') === 'cr-task-item').length, 2)
assert.equal(taskTokens.find((token) => token.type === 'bullet_list_open')?.attrGet('class'), 'cr-task-list cr-task-list')
const checkboxes = taskTokens.flatMap((token) => token.children ?? []).filter((token) => token.type === 'html_inline')
assert.deepEqual(checkboxes.map((token) => token.content), [
'<input class="cr-task-checkbox" type="checkbox" disabled checked> ',
'<input class="cr-task-checkbox" type="checkbox" disabled> '
])
const tableSource = '| a | b |\n| - | - |\n| c | d |\n\ntrailing paragraph'
const tableTokens = md.parse(tableSource, {})
const table = tableTokens.find((token) => token.type === 'table_open')
assert.ok(table)
assert.equal(table.attrGet('data-id'), `table-${tableTokens.indexOf(table)}`)
assert.equal(table.attrGet('data-content'), '| a | b |\n| - | - |\n| c | d |')
const html = md.render('left<br>middle <u>under</u> <script>ignored()</script>right')
assert.match(html, /left<br>/)
assert.match(html, /<u>under<\/u>/)
assert.doesNotMatch(html, /script|ignored\(\)/)
assert.match(html, /right/)
assert.doesNotMatch(md.render('visible <span class="streaming'), /streaming/)
const customSchemeParser = createMarkdownParser({
customUrlSchemes: [{ scheme: 'workbuddy:', linkifyValidate: (text, pos) => text.length - pos }]
})
assert.match(customSchemeParser.render('workbuddy://item/7'), /href="workbuddy:\/\/item\/7"/)
assert.doesNotMatch(createMarkdownParser({
customUrlSchemes: [{ scheme: 'javascript:', linkifyValidate: (text, pos) => text.length - pos }]
}).render('javascript:alert(1)'), /href="javascript:/)
const unicodeLink = md.parse('https://example.com日本', {})
const unicodeInline = unicodeLink.find((token) => token.type === 'inline')
const linkOpen = unicodeInline?.children?.find((token) => token.type === 'link_open' && token.markup === 'linkify')
assert.ok(linkOpen)
assert.equal(linkOpen.attrGet('href'), 'https://example.com')
assert.ok(unicodeInline?.children?.some((token) => token.type === 'text' && token.content === '日本'))
const loadingParser = createMarkdownParser({ loading: true })
const loadingTokens = loadingParser.parse('reply answer', {})
const loadingInline = loadingTokens.find((token) => token.type === 'inline')
const loadingDots = loadingInline?.children?.filter((token) => token.type === 'loadingDot') ?? []
assert.deepEqual(loadingDots.map((token) => token.attrGet('content')), ['a', 'n', 's', 'w', 'e', 'r'])
assert.deepEqual(loadingDots.map((token) => token.attrGet('level')), ['6', '5', '4', '3', '2', '1'])
assert.match(loadingParser.render('reply answer'), /class="loading-dot"/)
const surrogateTail = loadingParser.parse('prefixabcdef😀', {})
const surrogateInline = surrogateTail.find((token) => token.type === 'inline')
const surrogateDots = surrogateInline?.children?.filter((token) => token.type === 'loadingDot') ?? []
assert.equal(surrogateDots.length, 5)
assert.equal(surrogateDots.at(-1)?.attrGet('content'), '😀')
const nestedLoading = loadingParser.parse('prefix **123456**', {})
const nestedInline = nestedLoading.find((token) => token.type === 'inline')
const nestedDots = nestedInline?.children?.filter((token) => token.type === 'loadingDot') ?? []
assert.deepEqual(nestedDots.map((token) => token.attrGet('content')), ['1', '2', '3', '4', '5', '6'])
assert.equal(nestedInline?.children?.[0]?.content, 'prefix ')
const completedCodeTokens = loadingParser.parse('finished answer\n\n```js\nconst x = 1\n```', {})
assert.ok(!completedCodeTokens.some((token) => token.children?.some((child) => child.type === 'loadingDot')))
assert.equal(isPrismLanguageLoaded('js'), true)
assert.equal(getMdReadyVersion(), 0)
assert.equal(getMdSyntaxVersion(), 0)
let readinessNotifications = 0
const unsubscribeReady = subscribeMdReady(() => { readinessNotifications++ })
async function waitForPrismLanguage(language: string): Promise<void> {
if (isPrismLanguageLoaded(language)) return
await new Promise<void>((resolve, reject) => {
let unsubscribe = (): void => {}
const timeout = setTimeout(() => {
unsubscribe()
reject(new Error(`Prism language did not load: ${language}`))
}, 4000)
unsubscribe = onPrismLanguageLoaded(() => {
if (!isPrismLanguageLoaded(language)) return
clearTimeout(timeout)
unsubscribe()
resolve()
})
ensurePrismLanguage(language)
})
}
async function verifyPrismReadiness(): Promise<void> {
for (const language of ['cpp', 'php', 'tsx', 'scala']) {
const readyBefore = getMdReadyVersion()
await waitForPrismLanguage(language)
assert.ok(getPrismLanguage(language))
assert.equal(getMdReadyVersion(), readyBefore + 1)
}
unsubscribeReady()
assert.equal(readinessNotifications, 4)
assert.equal(getPrismLanguage('ts'), getPrismLanguage('typescript'))
console.log('Recovered markdown parser regression checks passed')
}
void verifyPrismReadiness().catch((error) => { console.error(error); process.exitCode = 1 })