1// The document's first heading doubles as its name: it becomes the browser tab
2// title and, more usefully, the filename the browser offers by default when
3// the document is printed to PDF.
5const MAX_LENGTH = 80
7// Strip the inline markdown that would otherwise end up in a filename, along
8// with the characters that operating systems disallow there.
9function clean(s: string): string {
10 return s
11 .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1')
12 .replace(/[`*_~]/g, '')
13 .replace(/\s+#+\s*$/, '')
14 .replace(/[\\/:*?"<>|]/g, '-')
15 .replace(/\s+/g, ' ')
16 .trim()
17 .slice(0, MAX_LENGTH)
18 .trim()
19}
21// The first ATX heading outside of a fenced code block, falling back to the
22// first non-empty line. Fences are tracked because a comment line such as
23// `# load the data` inside a python block is not a heading.
24export function documentTitle(markdown: string): string {
25 let inFence = false
26 let firstLine = ''
27 for (const raw of markdown.split('\n')) {
28 const line = raw.trim()
29 if (/^(```|~~~)/.test(line)) {
30 inFence = !inFence
31 continue
32 }
33 if (inFence || !line) continue
34 const heading = /^#{1,6}\s+(.+)$/.exec(line)
35 if (heading) return clean(heading[1])
36 if (!firstLine) firstLine = line
37 }
38 return clean(firstLine)
39}