feat: 增加ipynb渲染

This commit is contained in:
camera-2018
2023-04-24 11:26:28 +08:00
parent 09cb1716d1
commit 43a6e8d116
27 changed files with 11696 additions and 40 deletions

1
utils/index.js Normal file
View File

@@ -0,0 +1 @@
export * from "./notebook";

View File

@@ -0,0 +1,50 @@
/**
* 调用 codemirror 插件渲染code并调用 jupyterlab 的 codemirror 主题样式做渲染
* codemirror 插件: https://codemirror.net/
*/
import { EditorState } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { python } from "@codemirror/lang-python";
import { sql } from "@codemirror/lang-sql";
import { Theme } from "./codemirror.theme";
// Codemirror 扩展配置
const extensionsConfig = [
EditorState.readOnly.of(true),
EditorView.editable.of(false),
Theme.getTheme("jupyter"), // 主题引入
/* 引入所需的编程语言 START */
python(),
sql(),
/* 引入所需的编程语言 END */
];
/**
* 调用codemirror插件渲染code
*
* @param {string} codeString 需要渲染的code字符串
* @param {Element} parent 父元素,渲染成功后得元素将作为其的子元素
* @returns {Element} 渲染完成后的父元素
*/
export function createCodemirror(codeString, parent) {
if (codeString instanceof Array) codeString = codeString.join("");
if (typeof codeString !== "string")
throw "Function createCodemirror: 参数 codeString 必须是字符串!";
if (!parent) console.warn("Function createCodemirror: 参数 parent 不能为空");
if (!("appendChild" in document.body)) {
console.warn(
"Function createCodemirror: 参数 parent 类型错误需为HTML元素"
);
codeString = document.body;
}
return new EditorView({
state: EditorState.create({
doc: codeString || "",
extensions: extensionsConfig,
}),
parent: parent || document.body,
});
}

View File

@@ -0,0 +1,180 @@
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/**
* 本文件为 Jupyter lab 源码。
* 用于设置 codemirror 相关的主题,提供了 codemirror 以及 jupyter 两种主题。其中 jupyter 为默认主题
*
* 源码TS版https://github.com/jupyterlab/jupyterlab/blob/master/packages/codemirror/src/editortheme.ts
*/
import {
defaultHighlightStyle,
HighlightStyle,
syntaxHighlighting,
} from "@codemirror/language";
import { EditorView } from "@codemirror/view";
import { tags as t } from "@lezer/highlight";
export const jupyterEditorTheme = EditorView.theme({
/**
* CodeMirror themes are handling the background/color in this way. This works
* fine for CodeMirror editors outside the notebook, but the notebook styles
* these things differently.
*/
"&": {
background: "var(--jp-layout-color0)",
color: "var(--jp-content-font-color1)",
},
/* In the notebook, we want this styling to be handled by its container */
".jp-CodeConsole &, .jp-Notebook &": {
background: "transparent",
},
".cm-content": {
caretColor: "var(--jp-editor-cursor-color)",
},
".cm-cursor, .cm-dropCursor": {
borderLeft:
"var(--jp-code-cursor-width0) solid var(--jp-editor-cursor-color)",
},
".cm-selectionBackground, .cm-content ::selection": {
backgroundColor: "var(--jp-editor-selected-background)",
},
"&.cm-focused .cm-selectionBackground": {
backgroundColor: "var(--jp-editor-selected-focused-background)",
},
".cm-gutters": {
borderRight: "1px solid var(--jp-border-color2)",
backgroundColor: "var(--jp-layout-color2)",
},
".cm-gutter, .cm-activeLine": {
backgroundColor: "var(--jp-layout-color2)",
},
".cm-searchMatch": {
backgroundColor: "var(--jp-search-unselected-match-background-color)",
color: "var(--jp-search-unselected-match-color)",
},
".cm-searchMatch.cm-searchMatch-selected": {
backgroundColor:
"var(--jp-search-selected-match-background-color) !important",
color: "var(--jp-search-selected-match-color) !important",
},
});
// The list of available tags for syntax highlighting is available at
// https://lezer.codemirror.net/docs/ref/#highlight.tags
export const jupyterHighlightStyle = HighlightStyle.define([
// Order matters - a rule will override the previous ones; important for example for in headings styles.
{ tag: t.meta, color: "var(--jp-mirror-editor-meta-color)" },
{ tag: t.heading, color: "var(--jp-mirror-editor-header-color)" },
{
tag: [t.heading1, t.heading2, t.heading3, t.heading4],
color: "var(--jp-mirror-editor-header-color)",
fontWeight: "bold",
},
{
tag: t.keyword,
color: "var(--jp-mirror-editor-keyword-color)",
fontWeight: "bold",
},
{ tag: t.atom, color: "var(--jp-mirror-editor-atom-color)" },
{ tag: t.number, color: "var(--jp-mirror-editor-number-color)" },
{
tag: [t.definition(t.name), t.function(t.definition(t.variableName))],
color: "var(--jp-mirror-editor-def-color)",
},
{ tag: t.variableName, color: "var(--jp-mirror-editor-variable-color)" },
{
tag: [t.special(t.variableName), t.self],
color: "var(--jp-mirror-editor-variable-2-color)",
},
{ tag: t.punctuation, color: "var(--jp-mirror-editor-punctuation-color)" },
{ tag: t.propertyName, color: "var(--jp-mirror-editor-property-color)" },
{
tag: t.operator,
color: "var(--jp-mirror-editor-operator-color)",
fontWeight: "bold",
},
{
tag: t.comment,
color: "var(--jp-mirror-editor-comment-color)",
fontStyle: "italic",
},
{ tag: t.string, color: "var(--jp-mirror-editor-string-color)" },
{
tag: [t.labelName, t.monospace, t.special(t.string)],
color: "var(--jp-mirror-editor-string-2-color)",
},
{ tag: t.bracket, color: "var(--jp-mirror-editor-bracket-color)" },
{ tag: t.tagName, color: "var(--jp-mirror-editor-tag-color)" },
{ tag: t.attributeName, color: "var(--jp-mirror-editor-attribute-color)" },
{ tag: t.quote, color: "var(--jp-mirror-editor-quote-color)" },
{
tag: t.link,
color: "var(--jp-mirror-editor-link-color)",
textDecoration: "underline",
},
{ tag: [t.separator, t.derefOperator, t.paren], color: "" },
{ tag: t.strong, fontWeight: "bold" },
{ tag: t.emphasis, fontStyle: "italic" },
{ tag: t.strikethrough, textDecoration: "line-through" },
]);
/**
* JupyterLab CodeMirror 6 theme
*/
export const jupyterTheme = [
jupyterEditorTheme,
syntaxHighlighting(jupyterHighlightStyle),
];
/**
* A namespace to handle CodeMirror 6 theme
*
* @alpha
*/
export var Theme;
(function (Theme) {
/**
* CodeMirror 6 themes
*/
const themeMap = new Map([
[
"codemirror",
[EditorView.baseTheme({}), syntaxHighlighting(defaultHighlightStyle)],
],
["jupyter", jupyterTheme],
]);
/**
* Get the default CodeMirror 6 theme for JupyterLab
*
* @alpha
* @returns Default theme
*/
function defaultTheme() {
return themeMap.get("jupyter");
}
Theme.defaultTheme = defaultTheme;
/**
* Register a new theme.
*
* @alpha
* @param name Theme name
* @param theme Codemirror 6 theme extension
*/
function registerTheme(name, theme) {
themeMap.set(name, theme);
}
Theme.registerTheme = registerTheme;
/**
* Get a theme.
*
* #### Notes
* It falls back to the default theme
*
* @alpha
* @param name Theme name
* @returns Theme extension
*/
function getTheme(name) {
let ext = themeMap.get(name);
return ext !== null && ext !== void 0 ? ext : this.defaultTheme();
}
Theme.getTheme = getTheme;
})(Theme || (Theme = {}));

348
utils/notebook/index.js Normal file
View File

@@ -0,0 +1,348 @@
import "@jupyterlab/theme-light-extension/style/theme.css";
import "../../.vitepress/theme/css/jupyterlab/index.css";
import { createCodemirror } from "./codemirror";
import { defaultSanitizer } from "./sanitizer";
import { MathJaxTypesetter } from "./lib/index.js";
import {
renderHTML,
renderImage,
renderLatex,
renderMarkdown,
renderSVG,
renderText,
} from "./renderers";
import defaultMarkdownParser from "./markdown.js"; // 引入cngbdb-ui的markdown渲染逻辑
export class Notebook {
#source; // notebook源数据
#cells; // notebook cell列表;cell表示一个最基础的渲染单元例如inputCell,outputCell,outputResultCell
#fragment; // notebook 渲染结果片段是个div元素
#trusted; // 当前渲染字符是安全或者但求运行环境是否可信涉及Script,SVG渲染
#sanitizer; // 字符串无害化处理
#shouldTypeset; // 是否对数学公式字符进行latex排版,这里默认为true
#latexTypesetter; // latex 插件实例
#markdownParser; // markdown 渲染工具
/**
* 构造函数
* @param {Object} sourceOfJson Notebook 源数据JSON 对象
* @param {Boolean} trusted 当前渲染字符是安全或者当前运行环境是否可信涉及Script,SVG渲染,默认为False
* @param {Boolean} shouldTypeset 是否对数学公式字符进行latex排版,默认为true
* @param {*} markdownParser markdown 渲染工具
*/
constructor(source, trusted, shouldTypeset, markdownParser) {
if (!source.cells || !(source.cells instanceof Array))
throw "The Notebook is Error! Cells attribute is required and is Array!";
this.#source = JSON.parse(JSON.stringify(source));
const { cells } = this.#source;
this.#cells = cells;
this.#fragment = document.createElement("div"); // 创建一个新的空白的div片段notebook渲染的结果都暂时存储在其中
/*---------- 默认配置项 START ----------*/
this.#trusted = trusted || false; // 当前运行环境是否安全可信涉及Script,SVG渲染
this.#sanitizer = defaultSanitizer; // 字符串无害化处理
this.#shouldTypeset = shouldTypeset || true; // 是否对数学公式字符进行latex排版,这里默认为true
this.#latexTypesetter = new MathJaxTypesetter({
url: "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js",
config: "TeX-AMS_HTML-full,Safe",
}); // latex 插件实例化
this.#markdownParser = markdownParser || defaultMarkdownParser; // markdown 渲染工具
/*---------- 默认配置项 END ----------*/
}
// notebook渲染成HTML的结果
get notebookHTML() {
return this.#fragment;
}
/**
* cells 渲染
* 此处是 notebook 渲染的总入口
*
* 每个 cell 由input和output两个模块组成
* input 分为三类数据即cell_type = markdown/code/raw。其中只有code会有output
* output 处理详见 renderCommonCell 方法;
*
* @author 王志杰
* @returns {DocumentFragment} 返回一个 DocumentFragment 对象
*/
async render() {
try {
for (let cell of this.#cells) {
let node = null;
let { cell_type, source } = cell;
cell.source = typeof source === "string" ? source : source.join("");
switch (cell_type) {
case "markdown":
node = await this.#renderMarkdownCell(cell);
break;
case "code":
node = await this.#renderCodeCell(cell);
break;
case "raw":
node = await this.#renderRawCell(cell);
break;
}
this.#fragment.appendChild(node);
}
} catch (error) {
console.error(error);
}
return this.#fragment;
}
/**
* 数据渲染总模块input和output的数据渲染都经由这里分发到具体的渲染模块
*
* @param {string} param0 type-数据模块类型所支持的类型为output_type全部类型
* @param {object} param1 options-渲染模块所需的参数
*/
async #renderCommonCell({ type, options }) {
// 对未设置的配置项,设置为全局默认配置对应的属性值
options.trusted = options.trusted || this.#trusted;
options.sanitizer = options.sanitizer || this.#sanitizer;
options.shouldTypeset = options.shouldTypeset || this.#shouldTypeset;
options.latexTypesetter = options.latexTypesetter || this.#latexTypesetter;
options.markdownParser = options.markdownParser || this.#markdownParser;
switch (type) {
case "text/html":
await renderHTML(options);
break;
case "markdown":
case "text/markdown":
await renderMarkdown(options);
break;
case "text/plain":
case "application/vnd.jupyter.stdout":
case "application/vnd.jupyter.stderr":
await renderText(options);
break;
case "text/latex":
await renderLatex(options);
break;
case "image/bmp":
case "image/png":
case "image/jpeg":
case "image/gif":
case "image/webp":
await renderImage(options);
break;
case "image/svg+xml":
await renderSVG(options);
break;
case "text/javascript":
case "application/javascript":
// 禁止输出 JavaScript
options.source = "JavaScript output is disabled in JupyterLab";
await renderText(options);
break;
default:
break;
}
}
/**
* 渲染markdown DOM
* @param {Object} cell
*/
async #renderMarkdownCell(cell) {
let { source, execution_count: executionCount } = cell;
let contentNode = document.createElement("div");
await this.#renderCommonCell({
type: "text/markdown",
options: { host: contentNode, source: source },
});
return this.#createContainerNode(
"inputMarkdown",
contentNode,
executionCount
);
}
/**
* 渲染Code DOM
* @param {Object} cell
*/
async #renderCodeCell(cell) {
let node = null;
let { source, outputs, execution_count: executionCount } = cell;
let contentNode = document.createElement("div");
contentNode.className =
"lm-Widget p-Widget jp-Cell jp-CodeCell jp-Notebook-cell ";
createCodemirror(source, contentNode); // input代码块渲染
node = this.#createContainerNode("inputCode", contentNode, executionCount);
await this.#renderOutputCell(outputs, contentNode.parentNode.parentNode);
return node;
}
async #renderRawCell(cell) {
let { source } = cell;
let node = document.createElement("div");
await this.#renderCommonCell({
type: "text/plain",
options: { host: node, source: source },
});
return node;
}
/**
* 渲染 outputs
* @param {Array} outputs
*/
async #renderOutputCell(outputs, parentNode) {
if (!outputs || !outputs.length) return;
const OutputAreaNode = document.createElement("div");
OutputAreaNode.className =
"lm-Widget jp-OutputArea jp-Cell-outputArea q-mt-sm";
parentNode.appendChild(OutputAreaNode);
for (let output of outputs) {
let sources = [];
switch (output.output_type) {
case "stream": // 文本流输出
sources = output.text;
for (const source of sources) {
let node = document.createElement("div");
await this.#renderCommonCell({
type: "application/vnd.jupyter." + output.name,
options: { host: node, source: source },
});
OutputAreaNode.appendChild(
this.#createContainerNode(
"application/vnd.jupyter." + output.name,
node,
""
)
);
}
break;
case "display_data":
case "execute_result": {
// 富文本输出
const { data: outputData, execution_count: executionCount } = output;
const keys = Object.keys(outputData);
const key = keys[0];
let source = outputData[key];
if (!source) return;
let node = document.createElement("div");
source = typeof source === "string" ? source : source.join("\n");
await this.#renderCommonCell({
type: key,
options: { host: node, source: source },
});
OutputAreaNode.appendChild(
this.#createContainerNode(key, node, executionCount)
);
break;
}
case "error": // 错误信息输出
sources = output.traceback;
for (const source of sources) {
let node = document.createElement("div");
await this.#renderCommonCell({
type: "application/vnd.jupyter.stderr",
options: { host: node, source: source },
});
OutputAreaNode.appendChild(
this.#createContainerNode(
"application/vnd.jupyter.stderr",
node,
""
)
);
}
break;
}
}
}
#createContainerNode(type, contentNode, executionCount) {
let node = document.createElement("div");
let areaNode = document.createElement("div");
let promptNode = document.createElement("div");
if (executionCount || executionCount === null) {
promptNode.innerText = `[${executionCount === null ? " " : executionCount
}]`;
}
// prompt class设置。prompt 样式分为input和output两种
["inputMarkdown", "inputCode"].includes(type)
? (promptNode.className =
"lm-Widget p-Widget jp-InputPrompt jp-InputArea-prompt")
: (promptNode.className =
"lm-Widget p-Widget jp-OutputPrompt jp-OutputArea-prompt");
switch (type) {
case "inputMarkdown": {
node.className =
"lm-Widget p-Widget jp-Cell jp-MarkdownCell jp-mod-rendered jp-Notebook-cell";
areaNode.className =
"lm-Widget p-Widget jp-InputArea jp-Cell-inputArea";
contentNode.className =
"lm-Widget p-Widget jp-RenderedHTMLCommon jp-RenderedMarkdown jp-MarkdownOutput";
contentNode.setAttribute("data-mime-type", "text/markdown");
break;
}
case "inputCode": {
node.className =
"lm-Widget p-Widget jp-Cell jp-CodeCell jp-mod-noOutputs jp-Notebook-cell";
areaNode.className =
"lm-Widget p-Widget jp-InputArea jp-Cell-inputArea";
contentNode.className =
"lm-Widget p-Widget jp-CodeMirrorEditor jp-Editor jp-InputArea-editor";
break;
}
case "application/vnd.jupyter.stdout": {
node.className = "lm-Widget lm-Panel jp-OutputArea-child";
areaNode.className =
"lm-Widget p-Widget lm-Panel p-Panel jp-OutputArea-child";
contentNode.className =
"lm-Widget p-Widget jp-RenderedText jp-OutputArea-output";
contentNode.setAttribute(
"data-mime-type",
"application/vnd.jupyter.stdout"
);
break;
}
case "application/vnd.jupyter.stderr": {
node.className = "lm-Widget lm-Panel jp-OutputArea-child";
areaNode.className =
"lm-Widget p-Widget lm-Panel p-Panel jp-OutputArea-child";
contentNode.className =
"lm-Widget p-Widget jp-RenderedText jp-OutputArea-output";
contentNode.setAttribute(
"data-mime-type",
"application/vnd.jupyter.stderr"
);
break;
}
default: {
const typeClassMap = new Map([
["image/bmp", "jp-RenderedImage"],
["image/png", "jp-RenderedImage"],
["image/jpeg", "jp-RenderedImage"],
["image/gif", "jp-RenderedImage"],
["image/webp", "jp-RenderedImage"],
["text/latex", "jp-RenderedLatex"],
["image/svg+xml", "jp-RenderedSVG"],
["text/markdown", "jp-RenderedHTMLCommon jp-RenderedHTML"],
]);
node.className =
"lm-Widget p-Widget lm-Panel p-Panel jp-OutputArea-child jp-OutputArea-executeResult";
areaNode.className =
"lm-Widget p-Widget lm-Panel p-Panel jp-OutputArea-child";
contentNode.className = `lm-Widget p-Widget ${typeClassMap.get(type) || "jp-RenderedHTMLCommon"
} jp-OutputArea-output`;
contentNode.setAttribute("data-mime-type", type);
break;
}
}
areaNode.appendChild(promptNode);
areaNode.appendChild(contentNode);
node.appendChild(areaNode);
return node;
}
}

194
utils/notebook/latex.js Normal file
View File

@@ -0,0 +1,194 @@
/* -----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
// Some magic for deferring mathematical expressions to MathJax
// by hiding them from the Markdown parser.
// Some of the code here is adapted with permission from Davide Cervone
// under the terms of the Apache2 license governing the MathJax project.
// Other minor modifications are also due to StackExchange and are used with
// permission.
/**
* 本文件为 Jupyter lab 源码。
* 用于 latex 字符渲染
*
* 源码TS版https://github.com/jupyterlab/jupyterlab/blob/master/packages/rendermime/src/latex.ts
*/
const inline = "$"; // the inline math delimiter
// MATHSPLIT contains the pattern for math delimiters and special symbols
// needed for searching for math in the text input.
const MATHSPLIT =
/(\$\$?|\\(?:begin|end)\{[a-z]*\*?\}|\\[{}$]|[{}]|(?:\n\s*)+|@@\d+@@|\\\\(?:\(|\)|\[|\]))/i;
/**
* Break up the text into its component parts and search
* through them for math delimiters, braces, linebreaks, etc.
* Math delimiters must match and braces must balance.
* Don't allow math to pass through a double linebreak
* (which will be a paragraph).
*/
export function removeMath(text) {
const math = []; // stores math strings for later
let start = null;
let end = null;
let last = null;
let braces = 0;
let deTilde;
// Except for extreme edge cases, this should catch precisely those pieces of the markdown
// source that will later be turned into code spans. While MathJax will not TeXify code spans,
// we still have to consider them at this point; the following issue has happened several times:
//
// `$foo` and `$bar` are variables. --> <code>$foo ` and `$bar</code> are variables.
const hasCodeSpans = text.includes("`") || text.includes("~~~");
if (hasCodeSpans) {
text = text
.replace(/~/g, "~T")
// note: the `fence` (three or more consecutive tildes or backticks)
// can be followed by an `info string` but this cannot include backticks,
// see specification: https://spec.commonmark.org/0.30/#info-string
.replace(
/^(?<fence>`{3,}|(~T){3,})[^`\n]*\n([\s\S]*?)^\k<fence>`*$/gm,
(wholematch) => wholematch.replace(/\$/g, "~D")
)
.replace(/(^|[^\\])(`+)([^\n]*?[^`\n])\2(?!`)/gm, (wholematch) =>
wholematch.replace(/\$/g, "~D")
);
deTilde = (text) => {
return text.replace(/~([TD])/g, (wholematch, character) =>
character === "T" ? "~" : inline
);
};
} else {
deTilde = (text) => {
return text;
};
}
let blocks = text.replace(/\r\n?/g, "\n").split(MATHSPLIT);
for (let i = 1, m = blocks.length; i < m; i += 2) {
const block = blocks[i];
if (block.charAt(0) === "@") {
//
// Things that look like our math markers will get
// stored and then retrieved along with the math.
//
blocks[i] = "@@" + math.length + "@@";
math.push(block);
} else if (start !== null) {
//
// If we are in math, look for the end delimiter,
// but don't go past double line breaks, and
// and balance braces within the math.
//
if (block === end) {
if (braces) {
last = i;
} else {
blocks = processMath(start, i, deTilde, math, blocks);
start = null;
end = null;
last = null;
}
} else if (block.match(/\n.*\n/)) {
if (last !== null) {
i = last;
blocks = processMath(start, i, deTilde, math, blocks);
}
start = null;
end = null;
last = null;
braces = 0;
} else if (block === "{") {
braces++;
} else if (block === "}" && braces) {
braces--;
}
} else {
//
// Look for math start delimiters and when
// found, set up the end delimiter.
//
if (block === inline || block === "$$") {
start = i;
end = block;
braces = 0;
} else if (block === "\\\\(" || block === "\\\\[") {
start = i;
end = block.slice(-1) === "(" ? "\\\\)" : "\\\\]";
braces = 0;
} else if (block.substr(1, 5) === "begin") {
start = i;
end = "\\end" + block.substr(6);
braces = 0;
}
}
}
if (start !== null && last !== null) {
blocks = processMath(start, last, deTilde, math, blocks);
start = null;
end = null;
last = null;
}
return { text: deTilde(blocks.join("")), math };
}
/**
* Put back the math strings that were saved,
* and clear the math array (no need to keep it around).
*/
export function replaceMath(text, math) {
/**
* Replace a math placeholder with its corresponding group.
* The math delimiters "\\(", "\\[", "\\)" and "\\]" are replaced
* removing one backslash in order to be interpreted correctly by MathJax.
*/
const process = (match, n) => {
let group = math[n];
if (
group.substr(0, 3) === "\\\\(" &&
group.substr(group.length - 3) === "\\\\)"
) {
group = "\\(" + group.substring(3, group.length - 3) + "\\)";
} else if (
group.substr(0, 3) === "\\\\[" &&
group.substr(group.length - 3) === "\\\\]"
) {
group = "\\[" + group.substring(3, group.length - 3) + "\\]";
}
return group;
};
// Replace all the math group placeholders in the text
// with the saved strings.
return text.replace(/@@(\d+)@@/g, process);
}
/**
* Process math blocks.
*
* The math is in blocks i through j, so
* collect it into one block and clear the others.
* Replace &, <, and > by named entities.
* For IE, put <br> at the ends of comments since IE removes \n.
* Clear the current math positions and store the index of the
* math, then push the math string onto the storage array.
* The preProcess function is called on all blocks if it has been passed in
*/
function processMath(i, j, preProcess, math, blocks) {
let block = blocks
.slice(i, j + 1)
.join("")
.replace(/&/g, "&amp;") // use HTML entity for &
.replace(/</g, "&lt;") // use HTML entity for <
.replace(/>/g, "&gt;"); // use HTML entity for >
if (navigator && navigator.appName === "Microsoft Internet Explorer") {
block = block.replace(/(%[^\n]*)\n/g, "$1<br/>\n");
}
while (j > i) {
blocks[j] = "";
j--;
}
blocks[i] = "@@" + math.length + "@@"; // replace the current block text with a unique tag to find later
if (preProcess) {
block = preProcess(block);
}
math.push(block);
return blocks;
}

108
utils/notebook/lib/index.js Normal file
View File

@@ -0,0 +1,108 @@
/* -----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
/**
* @packageDocumentation
* @module mathjax2
*/
import { PromiseDelegate } from '@lumino/coreutils';
/**
* The MathJax Typesetter.
*/
export class MathJaxTypesetter {
/**
* Create a new MathJax typesetter.
*/
constructor(options) {
this._initPromise = new PromiseDelegate();
this._initialized = false;
this._url = options.url;
this._config = options.config;
}
/**
* Typeset the math in a node.
*
* #### Notes
* MathJax schedules the typesetting asynchronously,
* but there are not currently any callbacks or Promises
* firing when it is done.
*/
typeset(node) {
if (!this._initialized) {
this._init();
}
void this._initPromise.promise.then(() => {
MathJax.Hub.Queue(['Typeset', MathJax.Hub, node]);
try {
MathJax.Hub.Queue(['Require', MathJax.Ajax, '[MathJax]/extensions/TeX/AMSmath.js'], () => {
MathJax.InputJax.TeX.resetEquationNumbers();
});
}
catch (e) {
console.error('Error queueing resetEquationNumbers:', e);
}
});
}
/**
* Initialize MathJax.
*/
_init() {
const head = document.getElementsByTagName('head')[0];
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = `${this._url}?config=${this._config}&amp;delayStartupUntil=configured`;
script.charset = 'utf-8';
head.appendChild(script);
script.addEventListener('load', () => {
this._onLoad();
});
this._initialized = true;
}
/**
* Handle MathJax loading.
*/
_onLoad() {
MathJax.Hub.Config({
tex2jax: {
inlineMath: [
['$', '$'],
['\\(', '\\)']
],
displayMath: [
['$$', '$$'],
['\\[', '\\]']
],
processEscapes: true,
processEnvironments: true
},
// Center justify equations in code and markdown cells. Elsewhere
// we use CSS to left justify single line equations in code cells.
displayAlign: 'center',
CommonHTML: {
linebreaks: { automatic: true }
},
'HTML-CSS': {
availableFonts: [],
imageFont: null,
preferredFont: null,
webFont: 'STIX-Web',
styles: { '.MathJax_Display': { margin: 0 } },
linebreaks: { automatic: true }
},
skipStartupTypeset: true,
messageStyle: 'none'
});
MathJax.Hub.Register.StartupHook('End Config', () => {
var _a, _b, _c, _d, _e, _f;
// Disable `:hover span` styles which cause performance issues in Chromium browsers
// c-f https://github.com/jupyterlab/jupyterlab/issues/9757
// Note that we cannot overwrite them in config earlier due to how `CombineConfig`
// is implemented in MathJax 2 (it does not allow removing styles, just expanding).
(_c = (_b = (_a = MathJax.Hub) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.MathEvents) === null || _c === void 0 ? true : delete _c.styles['.MathJax_Hover_Arrow:hover span'];
(_f = (_e = (_d = MathJax.Hub) === null || _d === void 0 ? void 0 : _d.config) === null || _e === void 0 ? void 0 : _e.MathMenu) === null || _f === void 0 ? true : delete _f.styles['.MathJax_MenuClose:hover span'];
});
MathJax.Hub.Configured();
this._initPromise.resolve(void 0);
}
}

View File

@@ -0,0 +1,10 @@
import markdown from "markdown-it";
const MD = markdown({
html: true,
xhtmlOut: true,
breaks: true,
linkify: true,
});
export default MD;

701
utils/notebook/renderers.js Normal file
View File

@@ -0,0 +1,701 @@
/* eslint-disable no-func-assign */
/* eslint-disable no-unused-vars */
/* -----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
/**
* jupyter lab notebook output 渲染模块
* 本模块基于 @jupyterlab\rendermime\lib\renderers.js 二次开发
* 移除了部分功能a 标签url处理markdown 标题的自动生成对应链接等。
* 此外还去除了所需传参resolver、linkHandler、translator
*
* 源码TS版https://github.com/jupyterlab/jupyterlab/blob/master/packages/rendermime/src/renderers.ts
*
*/
import { removeMath, replaceMath } from "./latex";
import { URLExt } from "@jupyterlab/coreutils";
import escape from "lodash.escape";
/**
* Render HTML into a host node.
*
* @param options - The options for rendering.
*
* @returns A promise which resolves when rendering is complete.
*/
export function renderHTML(options) {
// Unpack the options.
let { host, source, trusted, sanitizer, shouldTypeset, latexTypesetter } =
options;
let originalSource = source;
// Bail early if the source is empty.
if (!source) {
host.textContent = "";
return Promise.resolve(undefined);
}
// Sanitize the source if it is not trusted. This removes all
// `<script>` tags as well as other potentially harmful HTML.
if (!trusted) {
originalSource = `${source}`;
source = sanitizer.sanitize(source);
}
// Set the inner HTML of the host.
host.innerHTML = source;
if (host.getElementsByTagName("script").length > 0) {
// If output it trusted, eval any script tags contained in the HTML.
// This is not done automatically by the browser when script tags are
// created by setting `innerHTML`.
if (trusted) {
Private.evalInnerHTMLScriptTags(host);
}
}
// Handle default behavior of nodes.
Private.handleDefaults(host);
// Patch the urls if a resolver is available.
let promise;
promise = Promise.resolve(undefined);
// Return the final rendered promise.
return promise.then(() => {
if (shouldTypeset && latexTypesetter) {
latexTypesetter.typeset(host);
}
});
}
/**
* Render an image into a host node.
*
* @param options - The options for rendering.
*
* @returns A promise which resolves when rendering is complete.
*/
export function renderImage(options) {
// Unpack the options.
const { host, mimeType, source, width, height, needsBackground, unconfined } =
options;
// Clear the content in the host.
host.textContent = "";
// Create the image element.
const img = document.createElement("img");
// Set the source of the image.
img.src = `data:${mimeType};base64,${source}`;
// Set the size of the image if provided.
if (typeof height === "number") {
img.height = height;
}
if (typeof width === "number") {
img.width = width;
}
if (needsBackground === "light") {
img.classList.add("jp-needs-light-background");
} else if (needsBackground === "dark") {
img.classList.add("jp-needs-dark-background");
}
if (unconfined === true) {
img.classList.add("jp-mod-unconfined");
}
// Add the image to the host.
host.appendChild(img);
// Return the rendered promise.
return Promise.resolve(undefined);
}
/**
* Render LaTeX into a host node.
*
* @param options - The options for rendering.
*
* @returns A promise which resolves when rendering is complete.
*/
export function renderLatex(options) {
// Unpack the options.
const { host, source, shouldTypeset, latexTypesetter } = options;
// Set the source on the node.
host.textContent = source;
// Typeset the node if needed.
if (shouldTypeset && latexTypesetter) {
latexTypesetter.typeset(host);
}
// Return the rendered promise.
return Promise.resolve(undefined);
}
/**
* Render Markdown into a host node.
*
* @param options - The options for rendering.
*
* @returns A promise which resolves when rendering is complete.
*/
export async function renderMarkdown(options) {
// Unpack the options.
const { host, source, markdownParser, ...others } = options;
// Clear the content if there is no source.
if (!source) {
host.textContent = "";
return;
}
let html = "";
if (markdownParser) {
// Separate math from normal markdown text.
const parts = removeMath(source);
// Convert the markdown to HTML.
html = await markdownParser.render(parts["text"]);
// Replace math.
html = replaceMath(html, parts["math"]);
} else {
// Fallback if the application does not have any markdown parser.
html = `<pre>${source}</pre>`;
}
// Render HTML.
await renderHTML({
host,
source: html,
...others,
});
}
/**
* The namespace for the `renderMarkdown` function statics.
*/
(function (renderMarkdown) {
/**
* Create a normalized id for a header element.
*
* @param header Header element
* @returns Normalized id
*/
function createHeaderId(header) {
var _a;
return (
(_a = header.textContent) !== null && _a !== void 0 ? _a : ""
).replace(/ /g, "-");
}
renderMarkdown.createHeaderId = createHeaderId;
})(renderMarkdown || (renderMarkdown = {}));
/**
* Render SVG into a host node.
*
* @param options - The options for rendering.
*
* @returns A promise which resolves when rendering is complete.
*/
export function renderSVG(options) {
// Unpack the options.
let { host, source, trusted, unconfined } = options;
// Clear the content if there is no source.
if (!source) {
host.textContent = "";
return Promise.resolve(undefined);
}
// Display a message if the source is not trusted.
if (!trusted) {
host.textContent =
"Cannot display an untrusted SVG. Maybe you need to run the cell?";
return Promise.resolve(undefined);
}
// Add missing SVG namespace (if actually missing)
const patt = "<svg[^>]+xmlns=[^>]+svg";
if (source.search(patt) < 0) {
source = source.replace("<svg", '<svg xmlns="http://www.w3.org/2000/svg"');
}
// Render in img so that user can save it easily
const img = new Image();
img.src = `data:image/svg+xml,${encodeURIComponent(source)}`;
host.appendChild(img);
if (unconfined === true) {
host.classList.add("jp-mod-unconfined");
}
return Promise.resolve();
}
/**
* Replace URLs with links.
*
* @param content - The text content of a node.
*
* @returns A list of text nodes and anchor elements.
*/
function autolink(content) {
// Taken from Visual Studio Code:
// https://github.com/microsoft/vscode/blob/9f709d170b06e991502153f281ec3c012add2e42/src/vs/workbench/contrib/debug/browser/linkDetector.ts#L17-L18
const controlCodes = "\\u0000-\\u0020\\u007f-\\u009f";
const webLinkRegex = new RegExp(
"(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|data:|www\\.)[^\\s" +
controlCodes +
'"]{2,}[^\\s' +
controlCodes +
"\"'(){}\\[\\],:;.!?]",
"ug"
);
const nodes = [];
let lastIndex = 0;
let match;
while (null != (match = webLinkRegex.exec(content))) {
if (match.index !== lastIndex) {
nodes.push(
document.createTextNode(content.slice(lastIndex, match.index))
);
}
let url = match[0];
// Special case when the URL ends with ">" or "<"
const lastChars = url.slice(-1);
const endsWithGtLt = [">", "<"].indexOf(lastChars) !== -1;
const len = endsWithGtLt ? url.length - 1 : url.length;
const anchor = document.createElement("a");
url = url.slice(0, len);
anchor.href = url.startsWith("www.") ? "https://" + url : url;
anchor.rel = "noopener";
anchor.target = "_blank";
anchor.appendChild(document.createTextNode(url.slice(0, len)));
nodes.push(anchor);
lastIndex = match.index + len;
}
if (lastIndex !== content.length) {
nodes.push(
document.createTextNode(content.slice(lastIndex, content.length))
);
}
return nodes;
}
/**
* Split a shallow node (node without nested nodes inside) at a given text content position.
*
* @param node the shallow node to be split
* @param at the position in textContent at which the split should occur
*/
function splitShallowNode(node, at) {
var _a, _b;
const pre = node.cloneNode();
pre.textContent =
(_a = node.textContent) === null || _a === void 0
? void 0
: _a.substr(0, at);
const post = node.cloneNode();
post.textContent =
(_b = node.textContent) === null || _b === void 0 ? void 0 : _b.substr(at);
return {
pre: pre,
post: post,
};
}
/**
* Render text into a host node.
*
* @param options - The options for rendering.
*
* @returns A promise which resolves when rendering is complete.
*/
export function renderText(options) {
var _a, _b;
// Unpack the options.
const { host, sanitizer, source } = options;
// Create the HTML content.
const content = sanitizer.sanitize(Private.ansiSpan(source), {
allowedTags: ["span"],
});
// Set the sanitized content for the host node.
const pre = document.createElement("pre");
pre.innerHTML = content;
const preTextContent = pre.textContent;
if (preTextContent) {
// Note: only text nodes and span elements should be present after sanitization in the `<pre>` element.
const linkedNodes = autolink(preTextContent);
let inAnchorElement = false;
const combinedNodes = [];
const preNodes = Array.from(pre.childNodes);
while (preNodes.length && linkedNodes.length) {
// Use non-null assertions to workaround TypeScript context awareness limitation
// (if any of the arrays were empty, we would not enter the body of the loop).
let preNode = preNodes.shift();
let linkNode = linkedNodes.shift();
// This should never happen because we modify the arrays in flight so they should end simultaneously,
// but this makes the coding assistance happy and might make it easier to conceptualize.
if (typeof preNode === "undefined") {
combinedNodes.push(linkNode);
break;
}
if (typeof linkNode === "undefined") {
combinedNodes.push(preNode);
break;
}
let preLen =
(_a = preNode.textContent) === null || _a === void 0
? void 0
: _a.length;
let linkLen =
(_b = linkNode.textContent) === null || _b === void 0
? void 0
: _b.length;
if (preLen && linkLen) {
if (preLen > linkLen) {
// Split pre node and only keep the shorter part
let { pre: keep, post: postpone } = splitShallowNode(
preNode,
linkLen
);
preNodes.unshift(postpone);
preNode = keep;
} else if (linkLen > preLen) {
let { pre: keep, post: postpone } = splitShallowNode(
linkNode,
preLen
);
linkedNodes.unshift(postpone);
linkNode = keep;
}
}
const lastCombined = combinedNodes[combinedNodes.length - 1];
// If we are already in an anchor element and the anchor element did not change,
// we should insert the node from <pre> which is either Text node or coloured span Element
// into the anchor content as a child
if (inAnchorElement && linkNode.href === lastCombined.href) {
lastCombined.appendChild(preNode);
} else {
// the `linkNode` is either Text or AnchorElement;
const isAnchor = linkNode.nodeType !== Node.TEXT_NODE;
// if we are NOT about to start an anchor element, just add the pre Node
if (!isAnchor) {
combinedNodes.push(preNode);
inAnchorElement = false;
} else {
// otherwise start a new anchor; the contents of the `linkNode` and `preNode` should be the same,
// so we just put the neatly formatted `preNode` inside the anchor node (`linkNode`)
// and append that to combined nodes.
linkNode.textContent = "";
linkNode.appendChild(preNode);
combinedNodes.push(linkNode);
inAnchorElement = true;
}
}
}
// TODO: replace with `.replaceChildren()` once the target ES version allows it
pre.innerHTML = "";
for (const child of combinedNodes) {
pre.appendChild(child);
}
}
host.appendChild(pre);
// Return the rendered promise.
return Promise.resolve(undefined);
}
/**
* The namespace for module implementation details.
*/
var Private;
(function (Private) {
/**
* Eval the script tags contained in a host populated by `innerHTML`.
*
* When script tags are created via `innerHTML`, the browser does not
* evaluate them when they are added to the page. This function works
* around that by creating new equivalent script nodes manually, and
* replacing the originals.
*/
function evalInnerHTMLScriptTags(host) {
// Create a snapshot of the current script nodes.
const scripts = Array.from(host.getElementsByTagName("script"));
// Loop over each script node.
for (const script of scripts) {
// Skip any scripts which no longer have a parent.
if (!script.parentNode) {
continue;
}
// Create a new script node which will be clone.
const clone = document.createElement("script");
// Copy the attributes into the clone.
const attrs = script.attributes;
for (let i = 0, n = attrs.length; i < n; ++i) {
const { name, value } = attrs[i];
clone.setAttribute(name, value);
}
// Copy the text content into the clone.
clone.textContent = script.textContent;
// Replace the old script in the parent.
script.parentNode.replaceChild(clone, script);
}
}
Private.evalInnerHTMLScriptTags = evalInnerHTMLScriptTags;
/**
* Handle the default behavior of nodes.
*/
function handleDefaults(node) {
// Handle anchor elements.
const anchors = node.getElementsByTagName("a");
for (let i = 0; i < anchors.length; i++) {
const el = anchors[i];
// skip when processing a elements inside svg
// which are of type SVGAnimatedString
if (!(el instanceof HTMLAnchorElement)) {
continue;
}
const path = el.href;
const isLocal = URLExt.isLocal(path);
// set target attribute if not already present
if (!el.target) {
el.target = isLocal ? "_self" : "_blank";
}
// set rel as 'noopener' for non-local anchors
if (!isLocal) {
el.rel = "noopener";
}
}
// Handle image elements.
const imgs = node.getElementsByTagName("img");
for (let i = 0; i < imgs.length; i++) {
if (!imgs[i].alt) {
imgs[i].alt = "Image";
}
}
}
Private.handleDefaults = handleDefaults;
const ANSI_COLORS = [
"ansi-black",
"ansi-red",
"ansi-green",
"ansi-yellow",
"ansi-blue",
"ansi-magenta",
"ansi-cyan",
"ansi-white",
"ansi-black-intense",
"ansi-red-intense",
"ansi-green-intense",
"ansi-yellow-intense",
"ansi-blue-intense",
"ansi-magenta-intense",
"ansi-cyan-intense",
"ansi-white-intense",
];
/**
* Create HTML tags for a string with given foreground, background etc. and
* add them to the `out` array.
*/
function pushColoredChunk(chunk, fg, bg, bold, underline, inverse, out) {
if (chunk) {
const classes = [];
const styles = [];
if (bold && typeof fg === "number" && 0 <= fg && fg < 8) {
fg += 8; // Bold text uses "intense" colors
}
if (inverse) {
[fg, bg] = [bg, fg];
}
if (typeof fg === "number") {
classes.push(ANSI_COLORS[fg] + "-fg");
} else if (fg.length) {
styles.push(`color: rgb(${fg})`);
} else if (inverse) {
classes.push("ansi-default-inverse-fg");
}
if (typeof bg === "number") {
classes.push(ANSI_COLORS[bg] + "-bg");
} else if (bg.length) {
styles.push(`background-color: rgb(${bg})`);
} else if (inverse) {
classes.push("ansi-default-inverse-bg");
}
if (bold) {
classes.push("ansi-bold");
}
if (underline) {
classes.push("ansi-underline");
}
if (classes.length || styles.length) {
out.push("<span");
if (classes.length) {
out.push(` class="${classes.join(" ")}"`);
}
if (styles.length) {
out.push(` style="${styles.join("; ")}"`);
}
out.push(">");
out.push(chunk);
out.push("</span>");
} else {
out.push(chunk);
}
}
}
/**
* Convert ANSI extended colors to R/G/B triple.
*/
function getExtendedColors(numbers) {
let r;
let g;
let b;
const n = numbers.shift();
if (n === 2 && numbers.length >= 3) {
// 24-bit RGB
r = numbers.shift();
g = numbers.shift();
b = numbers.shift();
if ([r, g, b].some((c) => c < 0 || 255 < c)) {
throw new RangeError("Invalid range for RGB colors");
}
} else if (n === 5 && numbers.length >= 1) {
// 256 colors
const idx = numbers.shift();
if (idx < 0) {
throw new RangeError("Color index must be >= 0");
} else if (idx < 16) {
// 16 default terminal colors
return idx;
} else if (idx < 232) {
// 6x6x6 color cube, see https://stackoverflow.com/a/27165165/500098
r = Math.floor((idx - 16) / 36);
r = r > 0 ? 55 + r * 40 : 0;
g = Math.floor(((idx - 16) % 36) / 6);
g = g > 0 ? 55 + g * 40 : 0;
b = (idx - 16) % 6;
b = b > 0 ? 55 + b * 40 : 0;
} else if (idx < 256) {
// grayscale, see https://stackoverflow.com/a/27165165/500098
r = g = b = (idx - 232) * 10 + 8;
} else {
throw new RangeError("Color index must be < 256");
}
} else {
throw new RangeError("Invalid extended color specification");
}
return [r, g, b];
}
/**
* Transform ANSI color escape codes into HTML <span> tags with CSS
* classes such as "ansi-green-intense-fg".
* The actual colors used are set in the CSS file.
* This also removes non-color escape sequences.
* This is supposed to have the same behavior as nbconvert.filters.ansi2html()
*/
function ansiSpan(str) {
const ansiRe = /\x1b\[(.*?)([@-~])/g; // eslint-disable-line no-control-regex
let fg = [];
let bg = [];
let bold = false;
let underline = false;
let inverse = false;
let match;
const out = [];
const numbers = [];
let start = 0;
str = escape(str);
str += "\x1b[m"; // Ensure markup for trailing text
// tslint:disable-next-line
while ((match = ansiRe.exec(str))) {
if (match[2] === "m") {
const items = match[1].split(";");
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item === "") {
numbers.push(0);
} else if (item.search(/^\d+$/) !== -1) {
numbers.push(parseInt(item, 10));
} else {
// Ignored: Invalid color specification
numbers.length = 0;
break;
}
}
} else {
// Ignored: Not a color code
}
const chunk = str.substring(start, match.index);
pushColoredChunk(chunk, fg, bg, bold, underline, inverse, out);
start = ansiRe.lastIndex;
while (numbers.length) {
const n = numbers.shift();
switch (n) {
case 0:
fg = bg = [];
bold = false;
underline = false;
inverse = false;
break;
case 1:
case 5:
bold = true;
break;
case 4:
underline = true;
break;
case 7:
inverse = true;
break;
case 21:
case 22:
bold = false;
break;
case 24:
underline = false;
break;
case 27:
inverse = false;
break;
case 30:
case 31:
case 32:
case 33:
case 34:
case 35:
case 36:
case 37:
fg = n - 30;
break;
case 38:
try {
fg = getExtendedColors(numbers);
} catch (e) {
numbers.length = 0;
}
break;
case 39:
fg = [];
break;
case 40:
case 41:
case 42:
case 43:
case 44:
case 45:
case 46:
case 47:
bg = n - 40;
break;
case 48:
try {
bg = getExtendedColors(numbers);
} catch (e) {
numbers.length = 0;
}
break;
case 49:
bg = [];
break;
case 90:
case 91:
case 92:
case 93:
case 94:
case 95:
case 96:
case 97:
fg = n - 90 + 8;
break;
case 100:
case 101:
case 102:
case 103:
case 104:
case 105:
case 106:
case 107:
bg = n - 100 + 8;
break;
default:
// Unknown codes are ignored
}
}
}
return out.join("");
}
Private.ansiSpan = ansiSpan;
})(Private || (Private = {}));

948
utils/notebook/sanitizer.js Normal file
View File

@@ -0,0 +1,948 @@
/* eslint-disable no-useless-escape */
/* eslint-disable camelcase */
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/**
* 本文件为 Jupyter lab 源码。
* 用于字符串无害化处理
*
* 源码TS版https://github.com/jupyterlab/jupyterlab/blob/master/packages/apputils/src/sanitizer.ts
*/
import sanitize from "sanitize-html";
/**
* Helper class that contains regular expressions for inline CSS style validation.
*
* Which properties (and values) to allow is largely based on the Google Caja project:
* https://github.com/google/caja
*
* The regular expressions are largly based on the syntax definition found at
* https://developer.mozilla.org/en-US/docs/Web/CSS.
*/
class CssProp {
static reg(r) {
return new RegExp("^" + r + "$", "i");
}
}
/*
* Numeric base expressions used to help build more complex regular expressions
*/
CssProp.N = {
integer: `[+-]?[0-9]+`,
integer_pos: `[+]?[0-9]+`,
integer_zero_ff: `([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])`,
number: `[+-]?([0-9]*[.])?[0-9]+(e-?[0-9]*)?`,
number_pos: `[+]?([0-9]*[.])?[0-9]+(e-?[0-9]*)?`,
number_zero_hundred: `[+]?(([0-9]|[1-9][0-9])([.][0-9]+)?|100)`,
number_zero_one: `[+]?(1([.][0]+)?|0?([.][0-9]+)?)`,
};
/*
* Base expressions of common CSS syntax elements
*/
CssProp.B = {
angle: `(${CssProp.N.number}(deg|rad|grad|turn)|0)`,
frequency: `${CssProp.N.number}(Hz|kHz)`,
ident: String.raw`-?([_a-z]|[\xA0-\xFF]|\\[0-9a-f]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])([_a-z0-9-]|[\xA0-\xFF]|\\[0-9a-f]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])*`,
len_or_perc: `(0|${CssProp.N.number}(px|em|rem|ex|in|cm|mm|pt|pc|%))`,
length: `(${CssProp.N.number}(px|em|rem|ex|in|cm|mm|pt|pc)|0)`,
length_pos: `(${CssProp.N.number_pos}(px|em|rem|ex|in|cm|mm|pt|pc)|0)`,
percentage: `${CssProp.N.number}%`,
percentage_pos: `${CssProp.N.number_pos}%`,
percentage_zero_hundred: `${CssProp.N.number_zero_hundred}%`,
string: String.raw`(\"([^\n\r\f\\"]|\\\n|\r\n|\r|\f|\\[0-9a-f]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])*\")|(\'([^\n\r\f\\']|\\\n|\r\n|\r|\f|\\[0-9a-f]{1,6}(\r\n|[ \t\r\n\f])?|\\[^\r\n\f0-9a-f])*\')`,
time: `${CssProp.N.number}(s|ms)`,
url: `url\\(.*?\\)`,
z_index: `[+-]?[0-9]{1,7}`,
};
/*
* Atomic (i.e. not dependant on other regular expressions) sub RegEx segments
*/
CssProp.A = {
absolute_size: `xx-small|x-small|small|medium|large|x-large|xx-large`,
attachment: `scroll|fixed|local`,
bg_origin: `border-box|padding-box|content-box`,
border_style: `none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset`,
box: `border-box|padding-box|content-box`,
display_inside: `auto|block|table|flex|grid`,
display_outside: `block-level|inline-level|none|table-row-group|table-header-group|table-footer-group|table-row|table-cell|table-column-group|table-column|table-caption`,
ending_shape: `circle|ellipse`,
generic_family: `serif|sans-serif|cursive|fantasy|monospace`,
generic_voice: `male|female|child`,
relative_size: `smaller|larger`,
repeat_style: `repeat-x|repeat-y|((?:repeat|space|round|no-repeat)(?:\\s*(?:repeat|space|round|no-repeat))?)`,
side_or_corner: `(left|right)?\\s*(top|bottom)?`,
single_animation_direction: `normal|reverse|alternate|alternate-reverse`,
single_animation_fill_mode: `none|forwards|backwards|both`,
single_animation_play_state: `running|paused`,
};
/*
* Color definition sub expressions
*/
CssProp._COLOR = {
hex: `\\#(0x)?[0-9a-f]+`,
name: `aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|transparent|violet|wheat|white|whitesmoke|yellow|yellowgreen`,
rgb: String.raw`rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)`,
rgba: String.raw`rgba\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(${CssProp.N.integer_zero_ff}|${CssProp.N.number_zero_one}|${CssProp.B.percentage_zero_hundred})\s*\)`,
};
/*
* Compound (i.e. dependant on other (sub) regular expressions) sub RegEx segments
*/
CssProp._C = {
alpha: `${CssProp.N.integer_zero_ff}|${CssProp.N.number_zero_one}|${CssProp.B.percentage_zero_hundred}`,
alphavalue: CssProp.N.number_zero_one,
bg_position: `((${CssProp.B.len_or_perc}|left|center|right|top|bottom)\\s*){1,4}`,
bg_size: `(${CssProp.B.length_pos}|${CssProp.B.percentage}|auto){1,2}|cover|contain`,
border_width: `thin|medium|thick|${CssProp.B.length}`,
bottom: `${CssProp.B.length}|auto`,
color: `${CssProp._COLOR.hex}|${CssProp._COLOR.rgb}|${CssProp._COLOR.rgba}|${CssProp._COLOR.name}`,
color_stop_length: `(${CssProp.B.len_or_perc}\\s*){1,2}`,
linear_color_hint: `${CssProp.B.len_or_perc}`,
family_name: `${CssProp.B.string}|(${CssProp.B.ident}\\s*)+`,
image_decl: CssProp.B.url,
left: `${CssProp.B.length}|auto`,
loose_quotable_words: `(${CssProp.B.ident})+`,
margin_width: `${CssProp.B.len_or_perc}|auto`,
padding_width: `${CssProp.B.length_pos}|${CssProp.B.percentage_pos}`,
page_url: CssProp.B.url,
position: `((${CssProp.B.len_or_perc}|left|center|right|top|bottom)\\s*){1,4}`,
right: `${CssProp.B.length}|auto`,
shadow: "",
size: `closest-side|farthest-side|closest-corner|farthest-corner|${CssProp.B.length}|(${CssProp.B.len_or_perc})\\s+(${CssProp.B.len_or_perc})`,
top: `${CssProp.B.length}|auto`,
};
CssProp._C1 = {
image_list: `image\\(\\s*(${CssProp.B.url})*\\s*(${CssProp.B.url}|${CssProp._C.color})\\s*\\)`,
linear_color_stop: `(${CssProp._C.color})(\\s*${CssProp._C.color_stop_length})?`,
shadow: `((${CssProp._C.color})\\s+((${CssProp.B.length})\\s*){2,4}(\s+inset)?)|((inset\\s+)?((${CssProp.B.length})\\s*){2,4}\\s*(${CssProp._C.color})?)`,
};
CssProp._C2 = {
color_stop_list: `((${CssProp._C1.linear_color_stop})(\\s*(${CssProp._C.linear_color_hint}))?\\s*,\\s*)+(${CssProp._C1.linear_color_stop})`,
shape: `rect\\(\\s*(${CssProp._C.top})\\s*,\\s*(${CssProp._C.right})\\s*,\\s*(${CssProp._C.bottom})\\s*,\\s*(${CssProp._C.left})\\s*\\)`,
};
CssProp._C3 = {
linear_gradient: `linear-gradient\\((((${CssProp.B.angle})|to\\s+(${CssProp.A.side_or_corner}))\\s*,\\s*)?\\s*(${CssProp._C2.color_stop_list})\\s*\\)`,
radial_gradient: `radial-gradient\\(((((${CssProp.A.ending_shape})|(${CssProp._C.size}))\\s*)*\\s*(at\\s+${CssProp._C.position})?\\s*,\\s*)?\\s*(${CssProp._C2.color_stop_list})\\s*\\)`,
};
CssProp._C4 = {
image: `${CssProp.B.url}|${CssProp._C3.linear_gradient}|${CssProp._C3.radial_gradient}|${CssProp._C1.image_list}`,
bg_image: `(${CssProp.B.url}|${CssProp._C3.linear_gradient}|${CssProp._C3.radial_gradient}|${CssProp._C1.image_list})|none`,
};
CssProp.C = {
...CssProp._C,
...CssProp._C1,
...CssProp._C2,
...CssProp._C3,
...CssProp._C4,
};
/*
* Property value regular expressions not dependant on other sub expressions
*/
CssProp.AP = {
border_collapse: `collapse|separate`,
box: `normal|none|contents`,
box_sizing: `content-box|padding-box|border-box`,
caption_side: `top|bottom`,
clear: `none|left|right|both`,
direction: `ltr|rtl`,
empty_cells: `show|hide`,
float: `left|right|none`,
font_stretch: `normal|wider|narrower|ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded`,
font_style: `normal|italic|oblique`,
font_variant: `normal|small-caps`,
font_weight: `normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900`,
list_style_position: `inside|outside`,
list_style_type: `disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-greek|lower-latin|upper-latin|armenian|georgian|lower-alpha|upper-alpha|none`,
overflow: `visible|hidden|scroll|auto`,
overflow_wrap: `normal|break-word`,
overflow_x: `visible|hidden|scroll|auto|no-display|no-content`,
page_break_after: `auto|always|avoid|left|right`,
page_break_before: `auto|always|avoid|left|right`,
page_break_inside: `avoid|auto`,
position: `static|relative|absolute`,
resize: `none|both|horizontal|vertical`,
speak: `normal|none|spell-out`,
speak_header: `once|always`,
speak_numeral: `digits|continuous`,
speak_punctuation: `code|none`,
table_layout: `auto|fixed`,
text_align: `left|right|center|justify`,
text_decoration: `none|((underline|overline|line-through|blink)\\s*)+`,
text_transform: `capitalize|uppercase|lowercase|none`,
text_wrap: `normal|unrestricted|none|suppress`,
unicode_bidi: `normal|embed|bidi-override`,
visibility: `visible|hidden|collapse`,
white_space: `normal|pre|nowrap|pre-wrap|pre-line`,
word_break: `normal|keep-all|break-all`,
};
/*
* Compound propertiy value regular expressions (i.e. dependant on other sub expressions)
*/
CssProp._CP = {
background_attachment: `${CssProp.A.attachment}(,\\s*${CssProp.A.attachment})*`,
background_color: CssProp.C.color,
background_origin: `${CssProp.A.box}(,\\s*${CssProp.A.box})*`,
background_repeat: `${CssProp.A.repeat_style}(,\\s*${CssProp.A.repeat_style})*`,
border: `((${CssProp.C.border_width}|${CssProp.A.border_style}|${CssProp.C.color})\\s*){1,3}`,
border_radius: `((${CssProp.B.len_or_perc})\\s*){1,4}(\\/\\s*((${CssProp.B.len_or_perc})\\s*){1,4})?`,
border_spacing: `${CssProp.B.length}\\s*(${CssProp.B.length})?`,
border_top_color: CssProp.C.color,
border_top_style: CssProp.A.border_style,
border_width: `((${CssProp.C.border_width})\\s*){1,4}`,
color: CssProp.C.color,
cursor: `(${CssProp.B.url}(\\s*,\\s*)?)*(auto|crosshair|default|pointer|move|e-resize|ne-resize|nw-resize|n-resize|se-resize|sw-resize|s-resize|w-resize|text|wait|help|progress|all-scroll|col-resize|hand|no-drop|not-allowed|row-resize|vertical-text)`,
display: `inline|block|list-item|run-in|inline-list-item|inline-block|table|inline-table|table-cell|table-caption|flex|inline-flex|grid|inline-grid|${CssProp.A.display_inside}|${CssProp.A.display_outside}|inherit|inline-box|inline-stack`,
display_outside: CssProp.A.display_outside,
elevation: `${CssProp.B.angle}|below|level|above|higher|lower`,
font_family: `(${CssProp.C.family_name}|${CssProp.A.generic_family})(,\\s*(${CssProp.C.family_name}|${CssProp.A.generic_family}))*`,
height: `${CssProp.B.length}|${CssProp.B.percentage}|auto`,
letter_spacing: `normal|${CssProp.B.length}`,
list_style_image: `${CssProp.C.image}|none`,
margin_right: CssProp.C.margin_width,
max_height: `${CssProp.B.length_pos}|${CssProp.B.percentage_pos}|none|auto`,
min_height: `${CssProp.B.length_pos}|${CssProp.B.percentage_pos}|auto`,
opacity: CssProp.C.alphavalue,
outline_color: `${CssProp.C.color}|invert`,
outline_width: CssProp.C.border_width,
padding: `((${CssProp.C.padding_width})\\s*){1,4}`,
padding_top: CssProp.C.padding_width,
pitch_range: CssProp.N.number,
right: `${CssProp.B.length}|${CssProp.B.percentage}|auto`,
stress: CssProp.N.number,
text_indent: `${CssProp.B.length}|${CssProp.B.percentage}`,
text_shadow: `none|${CssProp.C.shadow}(,\\s*(${CssProp.C.shadow}))*`,
volume: `${CssProp.N.number_pos}|${CssProp.B.percentage_pos}|silent|x-soft|soft|medium|loud|x-loud`,
word_wrap: CssProp.AP.overflow_wrap,
zoom: `normal|${CssProp.N.number_pos}|${CssProp.B.percentage_pos}`,
backface_visibility: CssProp.AP.visibility,
background_clip: `${CssProp.A.box}(,\\s*(${CssProp.A.box}))*`,
background_position: `${CssProp.C.bg_position}(,\\s*(${CssProp.C.bg_position}))*`,
border_bottom_color: CssProp.C.color,
border_bottom_style: CssProp.A.border_style,
border_color: `((${CssProp.C.color})\\s*){1,4}`,
border_left_color: CssProp.C.color,
border_right_color: CssProp.C.color,
border_style: `((${CssProp.A.border_style})\\s*){1,4}`,
border_top_left_radius: `(${CssProp.B.length}|${CssProp.B.percentage})(\\s*(${CssProp.B.length}|${CssProp.B.percentage}))?`,
border_top_width: CssProp.C.border_width,
box_shadow: `none|${CssProp.C.shadow}(,\\s*(${CssProp.C.shadow}))*`,
clip: `${CssProp.C.shape}|auto`,
display_inside: CssProp.A.display_inside,
font_size: `${CssProp.A.absolute_size}|${CssProp.A.relative_size}|${CssProp.B.length_pos}|${CssProp.B.percentage_pos}`,
line_height: `normal|${CssProp.N.number_pos}|${CssProp.B.length_pos}|${CssProp.B.percentage_pos}`,
margin_left: CssProp.C.margin_width,
max_width: `${CssProp.B.length_pos}|${CssProp.B.percentage_pos}|none|auto`,
outline_style: CssProp.A.border_style,
padding_bottom: CssProp.C.padding_width,
padding_right: CssProp.C.padding_width,
perspective: `none|${CssProp.B.length}`,
richness: CssProp.N.number,
text_overflow: `((clip|ellipsis|${CssProp.B.string})\\s*){1,2}`,
top: `${CssProp.B.length}|${CssProp.B.percentage}|auto`,
width: `${CssProp.B.length_pos}|${CssProp.B.percentage_pos}|auto`,
z_index: `auto|${CssProp.B.z_index}`,
// Simplified background
background: `(((${CssProp.C.bg_position}\\s*(\\/\\s*${CssProp.C.bg_size})?)|(${CssProp.A.repeat_style})|(${CssProp.A.attachment})|(${CssProp.A.bg_origin})|(${CssProp.C.bg_image})|(${CssProp.C.color}))\\s*)+`,
background_size: `${CssProp.C.bg_size}(,\\s*${CssProp.C.bg_size})*`,
border_bottom_left_radius: `(${CssProp.B.length}|${CssProp.B.percentage})(\\s*(${CssProp.B.length}|${CssProp.B.percentage}))?`,
border_bottom_width: CssProp.C.border_width,
border_left_style: CssProp.A.border_style,
border_right_style: CssProp.A.border_style,
border_top: `((${CssProp.C.border_width}|${CssProp.A.border_style}|${CssProp.C.color})\\s*){1,3}`,
bottom: `${CssProp.B.len_or_perc}|auto`,
list_style: `((${CssProp.AP.list_style_type}|${CssProp.AP.list_style_position}|${CssProp.C.image}|none})\\s*){1,3}`,
margin_top: CssProp.C.margin_width,
outline: `((${CssProp.C.color}|invert|${CssProp.A.border_style}|${CssProp.C.border_width})\\s*){1,3}`,
overflow_y: CssProp.AP.overflow_x,
pitch: `${CssProp.B.frequency}|x-low|low|medium|high|x-high`,
vertical_align: `baseline|sub|super|top|text-top|middle|bottom|text-bottom|${CssProp.B.len_or_perc}`,
word_spacing: `normal|${CssProp.B.length}`,
background_image: `${CssProp.C.bg_image}(,\\s*${CssProp.C.bg_image})*`,
border_bottom_right_radius: `(${CssProp.B.length}|${CssProp.B.percentage})(\\s*(${CssProp.B.length}|${CssProp.B.percentage}))?`,
border_left_width: CssProp.C.border_width,
border_right_width: CssProp.C.border_width,
left: `${CssProp.B.len_or_perc}|auto`,
margin_bottom: CssProp.C.margin_width,
pause_after: `${CssProp.B.time}|${CssProp.B.percentage}`,
speech_rate: `${CssProp.N.number}|x-slow|slow|medium|fast|x-fast|faster|slower`,
transition_duration: `${CssProp.B.time}(,\\s*${CssProp.B.time})*`,
border_bottom: `((${CssProp.C.border_width}|${CssProp.A.border_style}|${CssProp.C.color})\\s*){1,3}`,
border_right: `((${CssProp.C.border_width}|${CssProp.A.border_style}|${CssProp.C.color})\\s*){1,3}`,
margin: `((${CssProp.C.margin_width})\\s*){1,4}`,
padding_left: CssProp.C.padding_width,
border_left: `((${CssProp.C.border_width}|${CssProp.A.border_style}|${CssProp.C.color})\\s*){1,3}`,
quotes: `(${CssProp.B.string}\\s*${CssProp.B.string})+|none`,
border_top_right_radius: `(${CssProp.B.length}|${CssProp.B.percentage})(\\s*(${CssProp.B.length}|${CssProp.B.percentage}))?`,
min_width: `${CssProp.B.length_pos}|${CssProp.B.percentage_pos}|auto`,
};
CssProp._CP1 = {
font: `(((((${CssProp.AP.font_style}|${CssProp.AP.font_variant}|${CssProp.AP.font_weight})\\s*){1,3})?\\s*(${CssProp._CP.font_size})\\s*(\\/\\s*(${CssProp._CP.line_height}))?\\s+(${CssProp._CP.font_family}))|caption|icon|menu|message-box|small-caption|status-bar)`,
};
CssProp.CP = { ...CssProp._CP, ...CssProp._CP1 };
// CSS Property value validation regular expressions for use with sanitize-html
CssProp.BORDER_COLLAPSE = CssProp.reg(CssProp.AP.border_collapse);
CssProp.BOX = CssProp.reg(CssProp.AP.box);
CssProp.BOX_SIZING = CssProp.reg(CssProp.AP.box_sizing);
CssProp.CAPTION_SIDE = CssProp.reg(CssProp.AP.caption_side);
CssProp.CLEAR = CssProp.reg(CssProp.AP.clear);
CssProp.DIRECTION = CssProp.reg(CssProp.AP.direction);
CssProp.EMPTY_CELLS = CssProp.reg(CssProp.AP.empty_cells);
CssProp.FLOAT = CssProp.reg(CssProp.AP.float);
CssProp.FONT_STRETCH = CssProp.reg(CssProp.AP.font_stretch);
CssProp.FONT_STYLE = CssProp.reg(CssProp.AP.font_style);
CssProp.FONT_VARIANT = CssProp.reg(CssProp.AP.font_variant);
CssProp.FONT_WEIGHT = CssProp.reg(CssProp.AP.font_weight);
CssProp.LIST_STYLE_POSITION = CssProp.reg(CssProp.AP.list_style_position);
CssProp.LIST_STYLE_TYPE = CssProp.reg(CssProp.AP.list_style_type);
CssProp.OVERFLOW = CssProp.reg(CssProp.AP.overflow);
CssProp.OVERFLOW_WRAP = CssProp.reg(CssProp.AP.overflow_wrap);
CssProp.OVERFLOW_X = CssProp.reg(CssProp.AP.overflow_x);
CssProp.PAGE_BREAK_AFTER = CssProp.reg(CssProp.AP.page_break_after);
CssProp.PAGE_BREAK_BEFORE = CssProp.reg(CssProp.AP.page_break_before);
CssProp.PAGE_BREAK_INSIDE = CssProp.reg(CssProp.AP.page_break_inside);
CssProp.POSITION = CssProp.reg(CssProp.AP.position);
CssProp.RESIZE = CssProp.reg(CssProp.AP.resize);
CssProp.SPEAK = CssProp.reg(CssProp.AP.speak);
CssProp.SPEAK_HEADER = CssProp.reg(CssProp.AP.speak_header);
CssProp.SPEAK_NUMERAL = CssProp.reg(CssProp.AP.speak_numeral);
CssProp.SPEAK_PUNCTUATION = CssProp.reg(CssProp.AP.speak_punctuation);
CssProp.TABLE_LAYOUT = CssProp.reg(CssProp.AP.table_layout);
CssProp.TEXT_ALIGN = CssProp.reg(CssProp.AP.text_align);
CssProp.TEXT_DECORATION = CssProp.reg(CssProp.AP.text_decoration);
CssProp.TEXT_TRANSFORM = CssProp.reg(CssProp.AP.text_transform);
CssProp.TEXT_WRAP = CssProp.reg(CssProp.AP.text_wrap);
CssProp.UNICODE_BIDI = CssProp.reg(CssProp.AP.unicode_bidi);
CssProp.VISIBILITY = CssProp.reg(CssProp.AP.visibility);
CssProp.WHITE_SPACE = CssProp.reg(CssProp.AP.white_space);
CssProp.WORD_BREAK = CssProp.reg(CssProp.AP.word_break);
CssProp.BACKGROUND_ATTACHMENT = CssProp.reg(CssProp.CP.background_attachment);
CssProp.BACKGROUND_COLOR = CssProp.reg(CssProp.CP.background_color);
CssProp.BACKGROUND_ORIGIN = CssProp.reg(CssProp.CP.background_origin);
CssProp.BACKGROUND_REPEAT = CssProp.reg(CssProp.CP.background_repeat);
CssProp.BORDER = CssProp.reg(CssProp.CP.border);
CssProp.BORDER_RADIUS = CssProp.reg(CssProp.CP.border_radius);
CssProp.BORDER_SPACING = CssProp.reg(CssProp.CP.border_spacing);
CssProp.BORDER_TOP_COLOR = CssProp.reg(CssProp.CP.border_top_color);
CssProp.BORDER_TOP_STYLE = CssProp.reg(CssProp.CP.border_top_style);
CssProp.BORDER_WIDTH = CssProp.reg(CssProp.CP.border_width);
CssProp.COLOR = CssProp.reg(CssProp.CP.color);
CssProp.CURSOR = CssProp.reg(CssProp.CP.cursor);
CssProp.DISPLAY = CssProp.reg(CssProp.CP.display);
CssProp.DISPLAY_OUTSIDE = CssProp.reg(CssProp.CP.display_outside);
CssProp.ELEVATION = CssProp.reg(CssProp.CP.elevation);
CssProp.FONT_FAMILY = CssProp.reg(CssProp.CP.font_family);
CssProp.HEIGHT = CssProp.reg(CssProp.CP.height);
CssProp.LETTER_SPACING = CssProp.reg(CssProp.CP.letter_spacing);
CssProp.LIST_STYLE_IMAGE = CssProp.reg(CssProp.CP.list_style_image);
CssProp.MARGIN_RIGHT = CssProp.reg(CssProp.CP.margin_right);
CssProp.MAX_HEIGHT = CssProp.reg(CssProp.CP.max_height);
CssProp.MIN_HEIGHT = CssProp.reg(CssProp.CP.min_height);
CssProp.OPACITY = CssProp.reg(CssProp.CP.opacity);
CssProp.OUTLINE_COLOR = CssProp.reg(CssProp.CP.outline_color);
CssProp.OUTLINE_WIDTH = CssProp.reg(CssProp.CP.outline_width);
CssProp.PADDING = CssProp.reg(CssProp.CP.padding);
CssProp.PADDING_TOP = CssProp.reg(CssProp.CP.padding_top);
CssProp.PITCH_RANGE = CssProp.reg(CssProp.CP.pitch_range);
CssProp.RIGHT = CssProp.reg(CssProp.CP.right);
CssProp.STRESS = CssProp.reg(CssProp.CP.stress);
CssProp.TEXT_INDENT = CssProp.reg(CssProp.CP.text_indent);
CssProp.TEXT_SHADOW = CssProp.reg(CssProp.CP.text_shadow);
CssProp.VOLUME = CssProp.reg(CssProp.CP.volume);
CssProp.WORD_WRAP = CssProp.reg(CssProp.CP.word_wrap);
CssProp.ZOOM = CssProp.reg(CssProp.CP.zoom);
CssProp.BACKFACE_VISIBILITY = CssProp.reg(CssProp.CP.backface_visibility);
CssProp.BACKGROUND_CLIP = CssProp.reg(CssProp.CP.background_clip);
CssProp.BACKGROUND_POSITION = CssProp.reg(CssProp.CP.background_position);
CssProp.BORDER_BOTTOM_COLOR = CssProp.reg(CssProp.CP.border_bottom_color);
CssProp.BORDER_BOTTOM_STYLE = CssProp.reg(CssProp.CP.border_bottom_style);
CssProp.BORDER_COLOR = CssProp.reg(CssProp.CP.border_color);
CssProp.BORDER_LEFT_COLOR = CssProp.reg(CssProp.CP.border_left_color);
CssProp.BORDER_RIGHT_COLOR = CssProp.reg(CssProp.CP.border_right_color);
CssProp.BORDER_STYLE = CssProp.reg(CssProp.CP.border_style);
CssProp.BORDER_TOP_LEFT_RADIUS = CssProp.reg(CssProp.CP.border_top_left_radius);
CssProp.BORDER_TOP_WIDTH = CssProp.reg(CssProp.CP.border_top_width);
CssProp.BOX_SHADOW = CssProp.reg(CssProp.CP.box_shadow);
CssProp.CLIP = CssProp.reg(CssProp.CP.clip);
CssProp.DISPLAY_INSIDE = CssProp.reg(CssProp.CP.display_inside);
CssProp.FONT_SIZE = CssProp.reg(CssProp.CP.font_size);
CssProp.LINE_HEIGHT = CssProp.reg(CssProp.CP.line_height);
CssProp.MARGIN_LEFT = CssProp.reg(CssProp.CP.margin_left);
CssProp.MAX_WIDTH = CssProp.reg(CssProp.CP.max_width);
CssProp.OUTLINE_STYLE = CssProp.reg(CssProp.CP.outline_style);
CssProp.PADDING_BOTTOM = CssProp.reg(CssProp.CP.padding_bottom);
CssProp.PADDING_RIGHT = CssProp.reg(CssProp.CP.padding_right);
CssProp.PERSPECTIVE = CssProp.reg(CssProp.CP.perspective);
CssProp.RICHNESS = CssProp.reg(CssProp.CP.richness);
CssProp.TEXT_OVERFLOW = CssProp.reg(CssProp.CP.text_overflow);
CssProp.TOP = CssProp.reg(CssProp.CP.top);
CssProp.WIDTH = CssProp.reg(CssProp.CP.width);
CssProp.Z_INDEX = CssProp.reg(CssProp.CP.z_index);
CssProp.BACKGROUND = CssProp.reg(CssProp.CP.background);
CssProp.BACKGROUND_SIZE = CssProp.reg(CssProp.CP.background_size);
CssProp.BORDER_BOTTOM_LEFT_RADIUS = CssProp.reg(
CssProp.CP.border_bottom_left_radius
);
CssProp.BORDER_BOTTOM_WIDTH = CssProp.reg(CssProp.CP.border_bottom_width);
CssProp.BORDER_LEFT_STYLE = CssProp.reg(CssProp.CP.border_left_style);
CssProp.BORDER_RIGHT_STYLE = CssProp.reg(CssProp.CP.border_right_style);
CssProp.BORDER_TOP = CssProp.reg(CssProp.CP.border_top);
CssProp.BOTTOM = CssProp.reg(CssProp.CP.bottom);
CssProp.LIST_STYLE = CssProp.reg(CssProp.CP.list_style);
CssProp.MARGIN_TOP = CssProp.reg(CssProp.CP.margin_top);
CssProp.OUTLINE = CssProp.reg(CssProp.CP.outline);
CssProp.OVERFLOW_Y = CssProp.reg(CssProp.CP.overflow_y);
CssProp.PITCH = CssProp.reg(CssProp.CP.pitch);
CssProp.VERTICAL_ALIGN = CssProp.reg(CssProp.CP.vertical_align);
CssProp.WORD_SPACING = CssProp.reg(CssProp.CP.word_spacing);
CssProp.BACKGROUND_IMAGE = CssProp.reg(CssProp.CP.background_image);
CssProp.BORDER_BOTTOM_RIGHT_RADIUS = CssProp.reg(
CssProp.CP.border_bottom_right_radius
);
CssProp.BORDER_LEFT_WIDTH = CssProp.reg(CssProp.CP.border_left_width);
CssProp.BORDER_RIGHT_WIDTH = CssProp.reg(CssProp.CP.border_right_width);
CssProp.LEFT = CssProp.reg(CssProp.CP.left);
CssProp.MARGIN_BOTTOM = CssProp.reg(CssProp.CP.margin_bottom);
CssProp.PAUSE_AFTER = CssProp.reg(CssProp.CP.pause_after);
CssProp.SPEECH_RATE = CssProp.reg(CssProp.CP.speech_rate);
CssProp.TRANSITION_DURATION = CssProp.reg(CssProp.CP.transition_duration);
CssProp.BORDER_BOTTOM = CssProp.reg(CssProp.CP.border_bottom);
CssProp.BORDER_RIGHT = CssProp.reg(CssProp.CP.border_right);
CssProp.MARGIN = CssProp.reg(CssProp.CP.margin);
CssProp.PADDING_LEFT = CssProp.reg(CssProp.CP.padding_left);
CssProp.BORDER_LEFT = CssProp.reg(CssProp.CP.border_left);
CssProp.FONT = CssProp.reg(CssProp.CP.font);
CssProp.QUOTES = CssProp.reg(CssProp.CP.quotes);
CssProp.BORDER_TOP_RIGHT_RADIUS = CssProp.reg(
CssProp.CP.border_top_right_radius
);
CssProp.MIN_WIDTH = CssProp.reg(CssProp.CP.min_width);
/**
* A class to sanitize HTML strings.
*/
export class Sanitizer {
constructor() {
this._options = {
// HTML tags that are allowed to be used. Tags were extracted from Google Caja
allowedTags: [
"a",
"abbr",
"acronym",
"address",
"area",
"article",
"aside",
"audio",
"b",
"bdi",
"bdo",
"big",
"blockquote",
"br",
"button",
"canvas",
"caption",
"center",
"cite",
"code",
"col",
"colgroup",
"colspan",
"command",
"data",
"datalist",
"dd",
"del",
"details",
"dfn",
"dir",
"div",
"dl",
"dt",
"em",
"fieldset",
"figcaption",
"figure",
"font",
"footer",
"form",
"h1",
"h2",
"h3",
"h4",
"h5",
"h6",
"header",
"hgroup",
"hr",
"i",
// 'iframe' is allowed by Google Caja, but disallowed by default by sanitize-html
// , 'iframe'
"img",
"input",
"ins",
"kbd",
"label",
"legend",
"li",
"map",
"mark",
"menu",
"meter",
"nav",
"nobr",
"ol",
"optgroup",
"option",
"output",
"p",
"pre",
"progress",
"q",
"rowspan",
"s",
"samp",
"section",
"select",
"small",
"source",
"span",
"strike",
"strong",
"sub",
"summary",
"sup",
"table",
"tbody",
"td",
"textarea",
"tfoot",
"th",
"thead",
"time",
"tr",
"track",
"tt",
"u",
"ul",
"var",
"video",
"wbr",
],
// Attributes that HTML tags are allowed to have, extracted from Google Caja.
// See https://github.com/jupyterlab/jupyterlab/issues/1812#issuecomment-285848435
allowedAttributes: {
"*": [
"class",
"dir",
"draggable",
"hidden",
"id",
"inert",
"itemprop",
"itemref",
"itemscope",
"lang",
"spellcheck",
"style",
"title",
"translate",
],
// 'rel' and 'target' were *not* allowed by Google Caja
a: [
"accesskey",
"coords",
"href",
"hreflang",
"name",
"rel",
"shape",
"tabindex",
"target",
"type",
],
area: [
"accesskey",
"alt",
"coords",
"href",
"nohref",
"shape",
"tabindex",
],
// 'autoplay' was *not* allowed by Google Caja
audio: [
"autoplay",
"controls",
"loop",
"mediagroup",
"muted",
"preload",
"src",
],
bdo: ["dir"],
blockquote: ["cite"],
br: ["clear"],
button: [
"accesskey",
"data-commandlinker-args",
"data-commandlinker-command",
"disabled",
"name",
"tabindex",
"type",
"value",
],
canvas: ["height", "width"],
caption: ["align"],
col: ["align", "char", "charoff", "span", "valign", "width"],
colgroup: ["align", "char", "charoff", "span", "valign", "width"],
command: [
"checked",
"command",
"disabled",
"icon",
"label",
"radiogroup",
"type",
],
data: ["value"],
del: ["cite", "datetime"],
details: ["open"],
dir: ["compact"],
div: ["align"],
dl: ["compact"],
fieldset: ["disabled"],
font: ["color", "face", "size"],
form: [
"accept",
"autocomplete",
"enctype",
"method",
"name",
"novalidate",
],
h1: ["align"],
h2: ["align"],
h3: ["align"],
h4: ["align"],
h5: ["align"],
h6: ["align"],
hr: ["align", "noshade", "size", "width"],
iframe: [
"align",
"frameborder",
"height",
"marginheight",
"marginwidth",
"width",
],
img: [
"align",
"alt",
"border",
"height",
"hspace",
"ismap",
"name",
"src",
"usemap",
"vspace",
"width",
],
input: [
"accept",
"accesskey",
"align",
"alt",
"autocomplete",
"checked",
"disabled",
"inputmode",
"ismap",
"list",
"max",
"maxlength",
"min",
"multiple",
"name",
"placeholder",
"readonly",
"required",
"size",
"src",
"step",
"tabindex",
"type",
"usemap",
"value",
],
ins: ["cite", "datetime"],
label: ["accesskey", "for"],
legend: ["accesskey", "align"],
li: ["type", "value"],
map: ["name"],
menu: ["compact", "label", "type"],
meter: ["high", "low", "max", "min", "value"],
ol: ["compact", "reversed", "start", "type"],
optgroup: ["disabled", "label"],
option: ["disabled", "label", "selected", "value"],
output: ["for", "name"],
p: ["align"],
pre: ["width"],
progress: ["max", "min", "value"],
q: ["cite"],
select: [
"autocomplete",
"disabled",
"multiple",
"name",
"required",
"size",
"tabindex",
],
source: ["type"],
table: [
"align",
"bgcolor",
"border",
"cellpadding",
"cellspacing",
"frame",
"rules",
"summary",
"width",
],
tbody: ["align", "char", "charoff", "valign"],
td: [
"abbr",
"align",
"axis",
"bgcolor",
"char",
"charoff",
"colspan",
"headers",
"height",
"nowrap",
"rowspan",
"scope",
"valign",
"width",
],
textarea: [
"accesskey",
"autocomplete",
"cols",
"disabled",
"inputmode",
"name",
"placeholder",
"readonly",
"required",
"rows",
"tabindex",
"wrap",
],
tfoot: ["align", "char", "charoff", "valign"],
th: [
"abbr",
"align",
"axis",
"bgcolor",
"char",
"charoff",
"colspan",
"headers",
"height",
"nowrap",
"rowspan",
"scope",
"valign",
"width",
],
thead: ["align", "char", "charoff", "valign"],
tr: ["align", "bgcolor", "char", "charoff", "valign"],
track: ["default", "kind", "label", "srclang"],
ul: ["compact", "type"],
video: [
"autoplay",
"controls",
"height",
"loop",
"mediagroup",
"muted",
"poster",
"preload",
"src",
"width",
],
},
// Inline CSS styles that HTML tags may have (and their allowed values)
allowedStyles: {
// To simplify the data, all styles are allowed on all tags that allow the style attribute
"*": {
"backface-visibility": [CssProp.BACKFACE_VISIBILITY],
background: [CssProp.BACKGROUND],
"background-attachment": [CssProp.BACKGROUND_ATTACHMENT],
"background-clip": [CssProp.BACKGROUND_CLIP],
"background-color": [CssProp.BACKGROUND_COLOR],
"background-image": [CssProp.BACKGROUND_IMAGE],
"background-origin": [CssProp.BACKGROUND_ORIGIN],
"background-position": [CssProp.BACKGROUND_POSITION],
"background-repeat": [CssProp.BACKGROUND_REPEAT],
"background-size": [CssProp.BACKGROUND_SIZE],
border: [CssProp.BORDER],
"border-bottom": [CssProp.BORDER_BOTTOM],
"border-bottom-color": [CssProp.BORDER_BOTTOM_COLOR],
"border-bottom-left-radius": [CssProp.BORDER_BOTTOM_LEFT_RADIUS],
"border-bottom-right-radius": [CssProp.BORDER_BOTTOM_RIGHT_RADIUS],
"border-bottom-style": [CssProp.BORDER_BOTTOM_STYLE],
"border-bottom-width": [CssProp.BORDER_BOTTOM_WIDTH],
"border-collapse": [CssProp.BORDER_COLLAPSE],
"border-color": [CssProp.BORDER_COLOR],
"border-left": [CssProp.BORDER_LEFT],
"border-left-color": [CssProp.BORDER_LEFT_COLOR],
"border-left-style": [CssProp.BORDER_LEFT_STYLE],
"border-left-width": [CssProp.BORDER_LEFT_WIDTH],
"border-radius": [CssProp.BORDER_RADIUS],
"border-right": [CssProp.BORDER_RIGHT],
"border-right-color": [CssProp.BORDER_RIGHT_COLOR],
"border-right-style": [CssProp.BORDER_RIGHT_STYLE],
"border-right-width": [CssProp.BORDER_RIGHT_WIDTH],
"border-spacing": [CssProp.BORDER_SPACING],
"border-style": [CssProp.BORDER_STYLE],
"border-top": [CssProp.BORDER_TOP],
"border-top-color": [CssProp.BORDER_TOP_COLOR],
"border-top-left-radius": [CssProp.BORDER_TOP_LEFT_RADIUS],
"border-top-right-radius": [CssProp.BORDER_TOP_RIGHT_RADIUS],
"border-top-style": [CssProp.BORDER_TOP_STYLE],
"border-top-width": [CssProp.BORDER_TOP_WIDTH],
"border-width": [CssProp.BORDER_WIDTH],
bottom: [CssProp.BOTTOM],
box: [CssProp.BOX],
"box-shadow": [CssProp.BOX_SHADOW],
"box-sizing": [CssProp.BOX_SIZING],
"caption-side": [CssProp.CAPTION_SIDE],
clear: [CssProp.CLEAR],
clip: [CssProp.CLIP],
color: [CssProp.COLOR],
cursor: [CssProp.CURSOR],
direction: [CssProp.DIRECTION],
display: [CssProp.DISPLAY],
"display-inside": [CssProp.DISPLAY_INSIDE],
"display-outside": [CssProp.DISPLAY_OUTSIDE],
elevation: [CssProp.ELEVATION],
"empty-cells": [CssProp.EMPTY_CELLS],
float: [CssProp.FLOAT],
font: [CssProp.FONT],
"font-family": [CssProp.FONT_FAMILY],
"font-size": [CssProp.FONT_SIZE],
"font-stretch": [CssProp.FONT_STRETCH],
"font-style": [CssProp.FONT_STYLE],
"font-variant": [CssProp.FONT_VARIANT],
"font-weight": [CssProp.FONT_WEIGHT],
height: [CssProp.HEIGHT],
left: [CssProp.LEFT],
"letter-spacing": [CssProp.LETTER_SPACING],
"line-height": [CssProp.LINE_HEIGHT],
"list-style": [CssProp.LIST_STYLE],
"list-style-image": [CssProp.LIST_STYLE_IMAGE],
"list-style-position": [CssProp.LIST_STYLE_POSITION],
"list-style-type": [CssProp.LIST_STYLE_TYPE],
margin: [CssProp.MARGIN],
"margin-bottom": [CssProp.MARGIN_BOTTOM],
"margin-left": [CssProp.MARGIN_LEFT],
"margin-right": [CssProp.MARGIN_RIGHT],
"margin-top": [CssProp.MARGIN_TOP],
"max-height": [CssProp.MAX_HEIGHT],
"max-width": [CssProp.MAX_WIDTH],
"min-height": [CssProp.MIN_HEIGHT],
"min-width": [CssProp.MIN_WIDTH],
opacity: [CssProp.OPACITY],
outline: [CssProp.OUTLINE],
"outline-color": [CssProp.OUTLINE_COLOR],
"outline-style": [CssProp.OUTLINE_STYLE],
"outline-width": [CssProp.OUTLINE_WIDTH],
overflow: [CssProp.OVERFLOW],
"overflow-wrap": [CssProp.OVERFLOW_WRAP],
"overflow-x": [CssProp.OVERFLOW_X],
"overflow-y": [CssProp.OVERFLOW_Y],
padding: [CssProp.PADDING],
"padding-bottom": [CssProp.PADDING_BOTTOM],
"padding-left": [CssProp.PADDING_LEFT],
"padding-right": [CssProp.PADDING_RIGHT],
"padding-top": [CssProp.PADDING_TOP],
"page-break-after": [CssProp.PAGE_BREAK_AFTER],
"page-break-before": [CssProp.PAGE_BREAK_BEFORE],
"page-break-inside": [CssProp.PAGE_BREAK_INSIDE],
"pause-after": [CssProp.PAUSE_AFTER],
perspective: [CssProp.PERSPECTIVE],
pitch: [CssProp.PITCH],
"pitch-range": [CssProp.PITCH_RANGE],
position: [CssProp.POSITION],
quotes: [CssProp.QUOTES],
resize: [CssProp.RESIZE],
richness: [CssProp.RICHNESS],
right: [CssProp.RIGHT],
speak: [CssProp.SPEAK],
"speak-header": [CssProp.SPEAK_HEADER],
"speak-numeral": [CssProp.SPEAK_NUMERAL],
"speak-punctuation": [CssProp.SPEAK_PUNCTUATION],
"speech-rate": [CssProp.SPEECH_RATE],
stress: [CssProp.STRESS],
"table-layout": [CssProp.TABLE_LAYOUT],
"text-align": [CssProp.TEXT_ALIGN],
"text-decoration": [CssProp.TEXT_DECORATION],
"text-indent": [CssProp.TEXT_INDENT],
"text-overflow": [CssProp.TEXT_OVERFLOW],
"text-shadow": [CssProp.TEXT_SHADOW],
"text-transform": [CssProp.TEXT_TRANSFORM],
"text-wrap": [CssProp.TEXT_WRAP],
top: [CssProp.TOP],
"unicode-bidi": [CssProp.UNICODE_BIDI],
"vertical-align": [CssProp.VERTICAL_ALIGN],
visibility: [CssProp.VISIBILITY],
volume: [CssProp.VOLUME],
"white-space": [CssProp.WHITE_SPACE],
width: [CssProp.WIDTH],
"word-break": [CssProp.WORD_BREAK],
"word-spacing": [CssProp.WORD_SPACING],
"word-wrap": [CssProp.WORD_WRAP],
"z-index": [CssProp.Z_INDEX],
zoom: [CssProp.ZOOM],
},
},
transformTags: {
// Set the "rel" attribute for <a> tags to "nofollow".
a: sanitize.simpleTransform("a", { rel: "nofollow" }),
// Set the "disabled" attribute for <input> tags.
input: sanitize.simpleTransform("input", { disabled: "disabled" }),
},
allowedSchemesByTag: {
// Allow 'attachment:' img src (used for markdown cell attachments).
img: sanitize.defaults.allowedSchemes.concat(["attachment"]),
},
// Override of the default option, so we can skip 'src' attribute validation.
// 'src' Attributes are validated to be URIs, which does not allow for embedded (image) data.
// Since embedded data is no longer deemed to be a threat, validation can be skipped.
// See https://github.com/jupyterlab/jupyterlab/issues/5183
allowedSchemesAppliedToAttributes: ["href", "cite"],
};
}
/**
* Sanitize an HTML string.
*
* @param dirty - The dirty text.
*
* @param options - The optional sanitization options.
*
* @returns The sanitized string.
*/
sanitize(dirty, options) {
return sanitize(dirty, { ...this._options, ...(options || {}) });
}
}
/**
* The default instance of an `ISanitizer` meant for use by user code.
*/
export const defaultSanitizer = new Sanitizer();

View File

@@ -0,0 +1,15 @@
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
/* Left-justify the MathJax preview in cell outputs. */
.jp-OutputArea-output.jp-RenderedLatex .MathJax_Preview .MJXp-display {
padding: var(--jp-code-padding);
text-align: left !important;
}
/* Left-justify the MathJax display equation in cell outputs. */
.jp-OutputArea-output.jp-RenderedLatex .MathJax_Display {
text-align: left !important;
}

View File

@@ -0,0 +1,8 @@
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
/* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */
@import url('./base.css');

View File

@@ -0,0 +1,8 @@
/*-----------------------------------------------------------------------------
| Copyright (c) Jupyter Development Team.
| Distributed under the terms of the Modified BSD License.
|----------------------------------------------------------------------------*/
/* This file was auto-generated by ensurePackage() in @jupyterlab/buildutils */
import './base.css';