UNPKG

@jupyterlab/toc

Version:

JupyterLab - Table of Contents widget

228 lines 6.68 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { Sanitizer } from '@jupyterlab/apputils'; import { renderMarkdown } from '@jupyterlab/rendermime'; /** * Build the heading html id. * * @param raw Raw markdown heading * @param level Heading level * @param sanitizer HTML sanitizer */ export async function getHeadingId(markdownParser, raw, level, sanitizer) { try { const host = document.createElement('div'); await renderMarkdown({ markdownParser, host, source: raw, trusted: false, sanitizer: sanitizer !== null && sanitizer !== void 0 ? sanitizer : new Sanitizer(), shouldTypeset: false, resolver: null, linkHandler: null, latexTypesetter: null }); const header = host.querySelector(`h${level}`); if (!header) { return null; } return header.id; } catch (reason) { console.error('Failed to parse a heading.', reason); } return null; } /** * Parses the provided string and returns a list of headings. * * @param text - Input text * @returns List of headings */ export function getHeadings(text) { // Split the text into lines: const lines = text.split('\n'); // Iterate over the lines to get the header level and text for each line: const headings = new Array(); let isCodeBlock; let openingFence = 0; let fenceType; let lineIdx = 0; // Don't check for Markdown headings if in a YAML frontmatter block. // We can only start a frontmatter block on the first line of the file. // At other positions in a markdown file, '---' represents a horizontal rule. if (lines[lineIdx] === '---') { // Search for another '---' and treat that as the end of the frontmatter. // If we don't find one, treat the file as containing no frontmatter. for (let frontmatterEndLineIdx = lineIdx + 1; frontmatterEndLineIdx < lines.length; frontmatterEndLineIdx++) { if (lines[frontmatterEndLineIdx] === '---') { lineIdx = frontmatterEndLineIdx + 1; break; } } } for (; lineIdx < lines.length; lineIdx++) { const line = lines[lineIdx]; if (line === '') { // Bail early continue; } // Don't check for Markdown headings if in a code block if (line.startsWith('```') || line.startsWith('~~~')) { const closingFence = extractLeadingFences(line); if (closingFence === 0) continue; if (openingFence === 0) { fenceType = line.charAt(0); isCodeBlock = !isCodeBlock; openingFence = closingFence; continue; } else if (fenceType === line.charAt(0) && closingFence >= openingFence) { isCodeBlock = !isCodeBlock; openingFence = 0; fenceType = ''; } } if (isCodeBlock) { continue; } const heading = parseHeading(line, lines[lineIdx + 1]); // append the next line to capture alternative style Markdown headings if (heading) { headings.push({ ...heading, line: lineIdx }); } } return headings; } // Returns the length of ``` or ~~~ fences. function extractLeadingFences(line) { let match; if (line.startsWith('`')) match = line.match(/^(`{3,})/); else match = line.match(/^(~{3,})/); return match ? match[0].length : 0; } const MARKDOWN_MIME_TYPE = [ 'text/x-ipythongfm', 'text/x-markdown', 'text/x-gfm', 'text/markdown' ]; /** * Returns whether a MIME type corresponds to a Markdown flavor. * * @param mime - MIME type string * @returns boolean indicating whether a provided MIME type corresponds to a Markdown flavor * * @example * const bool = isMarkdown('text/markdown'); * // returns true * * @example * const bool = isMarkdown('text/plain'); * // returns false */ export function isMarkdown(mime) { return MARKDOWN_MIME_TYPE.includes(mime); } /** * Parses a heading, if one exists, from a provided string. * * ## Notes * * - Heading examples: * * - Markdown heading: * * ``` * # Foo * ``` * * - Markdown heading (alternative style): * * ``` * Foo * === * ``` * * ``` * Foo * --- * ``` * * - HTML heading: * * ``` * <h3>Foo</h3> * ``` * * @private * @param line - Line to parse * @param nextLine - The line after the one to parse * @returns heading info * * @example * const out = parseHeading('### Foo\n'); * // returns {'text': 'Foo', 'level': 3} * * @example * const out = parseHeading('Foo\n===\n'); * // returns {'text': 'Foo', 'level': 1} * * @example * const out = parseHeading('<h4>Foo</h4>\n'); * // returns {'text': 'Foo', 'level': 4} * * @example * const out = parseHeading('Foo'); * // returns null */ function parseHeading(line, nextLine) { // Case: Markdown heading let match = line.match(/^([#]{1,6}) (.*)/); if (match) { return { text: cleanTitle(match[2]), level: match[1].length, raw: line, skip: skipHeading.test(match[0]) }; } // Case: Markdown heading (alternative style) if (nextLine) { match = nextLine.match(/^ {0,3}([=]{2,}|[-]{2,})\s*$/); if (match) { return { text: cleanTitle(line), level: match[1][0] === '=' ? 1 : 2, raw: [line, nextLine].join('\n'), skip: skipHeading.test(line) }; } } // Case: HTML heading (WARNING: this is not particularly robust, as HTML headings can span multiple lines) match = line.match(/<h([1-6]).*>(.*)<\/h\1>/i); if (match) { return { text: match[2], level: parseInt(match[1], 10), skip: skipHeading.test(match[0]), raw: line }; } return null; } function cleanTitle(heading) { // take special care to parse Markdown links into raw text return heading.replace(/\[(.+)\]\(.+\)/g, '$1'); } /** * Ignore title with html tag with a class name equal to `jp-toc-ignore` or `tocSkip` */ const skipHeading = /<\w+\s(.*?\s)?class="(.*?\s)?(jp-toc-ignore|tocSkip)(\s.*?)?"(\s.*?)?>/; //# sourceMappingURL=markdown.js.map