|
49 | 49 |
|
50 | 50 | /** @typedef {MarkdownBaseNode & {
|
51 | 51 | * type: 'note',
|
52 |
| - * text: string, |
| 52 | + * text: string, |
53 | 53 | * noteType: string,
|
54 | 54 | * }} MarkdownNoteNode */
|
55 | 55 |
|
|
62 | 62 | * lines: string[],
|
63 | 63 | * }} MarkdownPropsNode */
|
64 | 64 |
|
| 65 | +/** @typedef {{ |
| 66 | + * value: string, groupId: string, spec: MarkdownNode |
| 67 | + * }} CodeGroup */ |
| 68 | + |
| 69 | +/** @typedef {function(CodeGroup[]): MarkdownNode[]} CodeGroupTransformer */ |
| 70 | + |
65 | 71 | /** @typedef {MarkdownTextNode | MarkdownLiNode | MarkdownCodeNode | MarkdownNoteNode | MarkdownHeaderNode | MarkdownNullNode | MarkdownPropsNode } MarkdownNode */
|
66 | 72 |
|
67 | 73 | function flattenWrappedLines(content) {
|
@@ -307,7 +313,7 @@ function innerRenderMdNode(indent, node, lastNode, result, maxColumns) {
|
307 | 313 | if (process.env.API_JSON_MODE)
|
308 | 314 | result.push(`${indent}\`\`\`${node.codeLang}`);
|
309 | 315 | else
|
310 |
| - result.push(`${indent}\`\`\`${codeLangToHighlighter(node.codeLang)}`); |
| 316 | + result.push(`${indent}\`\`\`${node.codeLang ? parseCodeLang(node.codeLang).highlighter : ''}`); |
311 | 317 | for (const line of node.lines)
|
312 | 318 | result.push(indent + line);
|
313 | 319 | result.push(`${indent}\`\`\``);
|
@@ -469,13 +475,82 @@ function filterNodesForLanguage(nodes, language) {
|
469 | 475 |
|
470 | 476 | /**
|
471 | 477 | * @param {string} codeLang
|
472 |
| - * @return {string} |
| 478 | + * @return {{ highlighter: string, language: string|undefined, codeGroup: string|undefined}} |
473 | 479 | */
|
474 |
| -function codeLangToHighlighter(codeLang) { |
475 |
| - const [lang] = codeLang.split(' '); |
476 |
| - if (lang === 'python') |
477 |
| - return 'py'; |
478 |
| - return lang; |
| 480 | +function parseCodeLang(codeLang) { |
| 481 | + if (codeLang === 'python async') |
| 482 | + return { highlighter: 'py', codeGroup: 'python-async', language: 'python' }; |
| 483 | + if (codeLang === 'python sync') |
| 484 | + return { highlighter: 'py', codeGroup: 'python-sync', language: 'python' }; |
| 485 | + |
| 486 | + const [highlighter] = codeLang.split(' '); |
| 487 | + if (!highlighter) |
| 488 | + throw new Error(`Cannot parse code block lang: "${codeLang}"`); |
| 489 | + |
| 490 | + const languageMatch = codeLang.match(/ lang=([\w\d]+)/); |
| 491 | + let language = languageMatch ? languageMatch[1] : undefined; |
| 492 | + if (!language) { |
| 493 | + if (highlighter === 'ts') |
| 494 | + language = 'js'; |
| 495 | + else if (['js', 'python', 'csharp', 'java'].includes(highlighter)) |
| 496 | + language = highlighter; |
| 497 | + } |
| 498 | + |
| 499 | + const tabMatch = codeLang.match(/ tab=([\w\d-]+)/); |
| 500 | + return { highlighter, language, codeGroup: tabMatch ? tabMatch[1] : '' }; |
| 501 | +} |
| 502 | + |
| 503 | +/** |
| 504 | + * @param {MarkdownNode[]} spec |
| 505 | + * @param {string} language |
| 506 | + * @param {CodeGroupTransformer} transformer |
| 507 | + * @returns {MarkdownNode[]} |
| 508 | + */ |
| 509 | +function processCodeGroups(spec, language, transformer) { |
| 510 | + /** @type {MarkdownNode[]} */ |
| 511 | + const newSpec = []; |
| 512 | + for (let i = 0; i < spec.length; ++i) { |
| 513 | + /** @type {{value: string, groupId: string, spec: MarkdownNode}[]} */ |
| 514 | + const tabs = []; |
| 515 | + for (;i < spec.length; i++) { |
| 516 | + const codeLang = spec[i].codeLang; |
| 517 | + if (!codeLang) |
| 518 | + break; |
| 519 | + let parsed; |
| 520 | + try { |
| 521 | + parsed = parseCodeLang(codeLang); |
| 522 | + } catch (e) { |
| 523 | + throw new Error(e.message + '\n while processing:\n' + render([spec[i]])); |
| 524 | + } |
| 525 | + if (!parsed.codeGroup) |
| 526 | + break; |
| 527 | + if (parsed.language && parsed.language !== language) |
| 528 | + continue; |
| 529 | + const [groupId, value] = parsed.codeGroup.split('-'); |
| 530 | + tabs.push({ groupId, value, spec: spec[i] }); |
| 531 | + } |
| 532 | + if (tabs.length) { |
| 533 | + if (tabs.length === 1) |
| 534 | + throw new Error(`Lonely tab "${tabs[0].spec.codeLang}". Make sure there are at least two tabs in the group.\n` + render([tabs[0].spec])); |
| 535 | + |
| 536 | + // Validate group consistency. |
| 537 | + const groupId = tabs[0].groupId; |
| 538 | + const values = new Set(); |
| 539 | + for (const tab of tabs) { |
| 540 | + if (tab.groupId !== groupId) |
| 541 | + throw new Error('Mixed group ids: ' + render(spec)); |
| 542 | + if (values.has(tab.value)) |
| 543 | + throw new Error(`Duplicated tab "${tab.value}"\n` + render(tabs.map(tab => tab.spec))); |
| 544 | + values.add(tab.value); |
| 545 | + } |
| 546 | + |
| 547 | + // Append transformed nodes. |
| 548 | + newSpec.push(...transformer(tabs)); |
| 549 | + } |
| 550 | + if (i < spec.length) |
| 551 | + newSpec.push(spec[i]); |
| 552 | + } |
| 553 | + return newSpec; |
479 | 554 | }
|
480 | 555 |
|
481 |
| -module.exports = { parse, render, clone, visitAll, visit, generateToc, filterNodesForLanguage, codeLangToHighlighter }; |
| 556 | +module.exports = { parse, render, clone, visitAll, visit, generateToc, filterNodesForLanguage, parseCodeLang, processCodeGroups }; |
0 commit comments