@jupyterlab/toc
Version:
JupyterLab - Table of Contents widget
228 lines • 6.68 kB
JavaScript
// 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