// The document's first heading doubles as its name: it becomes the browser tab // title and, more usefully, the filename the browser offers by default when // the document is printed to PDF. const MAX_LENGTH = 80 // Strip the inline markdown that would otherwise end up in a filename, along // with the characters that operating systems disallow there. function clean(s: string): string { return s .replace(/\[([^\]]*)\]\([^)]*\)/g, '$1') .replace(/[`*_~]/g, '') .replace(/\s+#+\s*$/, '') .replace(/[\\/:*?"<>|]/g, '-') .replace(/\s+/g, ' ') .trim() .slice(0, MAX_LENGTH) .trim() } // The first ATX heading outside of a fenced code block, falling back to the // first non-empty line. Fences are tracked because a comment line such as // `# load the data` inside a python block is not a heading. export function documentTitle(markdown: string): string { let inFence = false let firstLine = '' for (const raw of markdown.split('\n')) { const line = raw.trim() if (/^(```|~~~)/.test(line)) { inFence = !inFence continue } if (inFence || !line) continue const heading = /^#{1,6}\s+(.+)$/.exec(line) if (heading) return clean(heading[1]) if (!firstLine) firstLine = line } return clean(firstLine) }