UNPKG

@jupyterlab/toc

Version:

JupyterLab - Table of Contents widget

269 lines 8.4 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 (sanitizer === null || sanitizer === void 0 ? void 0 : sanitizer.allowNamedProperties) ? header.id : header.getAttribute('data-jupyter-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 - Markdown text * @param parser - A Markdown parser instance used to render the content to HTML. * @returns List of headings * * @remarks * Renders Markdown to HTML and extracts `<h1>`–`<h6>` elements. * Returns an empty list if the parser is `null`. */ export async function parseHeadings(markdownText, parser) { if (!parser) { console.warn("Couldn't parse headings; Markdown parser is null"); return []; } const renderedHtml = await parser.render(markdownText); const headings = new Array(); const domParser = new DOMParser(); const htmlDocument = domParser.parseFromString(renderedHtml, 'text/html'); // Query all heading elements (h1-h6) const headingElements = htmlDocument.querySelectorAll('h1, h2, h3, h4, h5, h6'); headingElements.forEach((headingElement, lineIdx) => { var _a; const level = parseInt(headingElement.tagName.substring(1), 10); const headingText = ((_a = headingElement.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || ''; headings.push({ text: headingText, line: lineIdx, // Line index within the parsed HTML, not the original Markdown source line level: level, raw: headingElement.outerHTML, // Parsed HTML string, not raw Markdown skip: skipHeading.test(headingElement.outerHTML) }); }); return headings; } /** * @deprecated in favour of async parseHeadings() * * 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