markdown.ts 1.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
  1. /** Shared Markdown parsing and depth-first traversal for documentation gates. */
  2. import { fromMarkdown } from 'mdast-util-from-markdown'
  3. import { gfmFromMarkdown } from 'mdast-util-gfm'
  4. import { gfm } from 'micromark-extension-gfm'
  5. import type { Nodes } from 'mdast'
  6. /** One authored Markdown line outside fenced code. */
  7. export interface MarkdownProseLine {
  8. /** 1-based source line number. */
  9. index: number
  10. /** Source text without normalization. */
  11. raw: string
  12. }
  13. /** Parse GitHub-flavored Markdown with the repository's standard extensions. */
  14. export function parseMarkdown(source: string): Nodes {
  15. return fromMarkdown(source, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] })
  16. }
  17. /**
  18. * Visit a Markdown tree depth-first; returning false prunes a node's children.
  19. * @param node - current tree node.
  20. * @param visitor - callback invoked before each node's children.
  21. */
  22. export function visitMarkdown(node: Nodes, visitor: (node: Nodes) => boolean | void): void {
  23. if (visitor(node) === false) return
  24. if ('children' in node) {
  25. for (const child of node.children) visitMarkdown(child, visitor)
  26. }
  27. }
  28. /**
  29. * Return source lines outside backtick or tilde fences.
  30. * @param source - Markdown source whose prose should be retained verbatim.
  31. * @returns unfenced lines with their original 1-based locations.
  32. */
  33. export function markdownProseLines(source: string): MarkdownProseLine[] {
  34. let fence: { marker: '`' | '~'; length: number } | undefined
  35. const kept: MarkdownProseLine[] = []
  36. source.split('\n').forEach((raw, i) => {
  37. const token = /^ {0,3}(`{3,}|~{3,})/.exec(raw)?.[1]
  38. if (token !== undefined) {
  39. const marker = token[0] as '`' | '~'
  40. if (fence === undefined) {
  41. fence = { marker, length: token.length }
  42. } else if (marker === fence.marker && token.length >= fence.length) {
  43. fence = undefined
  44. }
  45. return
  46. }
  47. if (fence === undefined) kept.push({ index: i + 1, raw })
  48. })
  49. return kept
  50. }