1
0

add initial marp implementation with sample content and build configuration

This commit is contained in:
2025-09-13 18:13:22 +02:00
parent dcacc9b409
commit e5f219507f
10319 changed files with 1402023 additions and 0 deletions

718
node_modules/mathjax-full/ts/ui/lazy/LazyHandler.ts generated vendored Normal file
View File

@@ -0,0 +1,718 @@
/*************************************************************
*
* Copyright (c) 2021-2022 The MathJax Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Mixin that implements lazy typesetting
*
* @author dpvc@mathjax.org (Davide Cervone)
*/
import {MathDocumentConstructor, ContainerList} from '../../core/MathDocument.js';
import {MathItem, STATE, newState} from '../../core/MathItem.js';
import {HTMLMathItem} from '../../handlers/html/HTMLMathItem.js';
import {HTMLDocument} from '../../handlers/html/HTMLDocument.js';
import {HTMLHandler} from '../../handlers/html/HTMLHandler.js';
import {handleRetriesFor} from '../../util/Retries.js';
import {OptionList} from '../../util/Options.js';
/**
* Add the needed function to the window object.
*/
declare const window: {
requestIdleCallback: (callback: () => void) => void;
addEventListener: ((type: string, handler: (event: Event) => void) => void);
matchMedia: (type: string) => {
addListener: (handler: (event: Event) => void) => void;
};
};
/**
* Generic constructor for Mixins
*/
export type Constructor<T> = new(...args: any[]) => T;
/**
* A set of lazy MathItems
*/
export type LazySet = Set<string>;
/*==========================================================================*/
/**
* The data to map expression marker IDs back to their MathItem.
*/
export class LazyList<N, T, D> {
/**
* The next ID to use
*/
protected id: number = 0;
/**
* The map from IDs to MathItems
*/
protected items: Map<string, LazyMathItem<N, T, D>> = new Map();
/**
* Add a MathItem to the list and return its ID
*
* @param {LazyMathItem} math The item to add
* @return {string} The id for the newly added item
*/
public add(math: LazyMathItem<N, T, D>): string {
const id = String(this.id++);
this.items.set(id, math);
return id;
}
/**
* Get the MathItem with the given ID
*
* @param {string} id The ID of the MathItem to get
* @return {LazyMathItem} The MathItem having that ID (if any)
*/
public get(id: string): LazyMathItem<N, T, D> {
return this.items.get(id);
}
/**
* Remove an item from the map
*
* @param {string} id The ID of the MathItem to remove
*/
public delete(id: string) {
return this.items.delete(id);
}
}
/*==========================================================================*/
newState('LAZYALWAYS', STATE.FINDMATH + 3);
/**
* The attribute to use for the ID on the marker node
*/
export const LAZYID = 'data-mjx-lazy';
/**
* The properties added to MathItem for lazy typesetting
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
export interface LazyMathItem<N, T, D> extends MathItem<N, T, D> {
/**
* True when the MathItem needs to be lazy compiled
*/
lazyCompile: boolean;
/**
* True when the MathItem needs to be lazy displayed
*/
lazyTypeset: boolean;
/**
* The DOM node used to mark the location of the math to be lazy typeset
*/
lazyMarker: N;
/**
* True if this item is a TeX MathItem
*/
lazyTex: boolean;
}
/**
* The mixin for adding lazy typesetting to MathItems
*
* @param {B} BaseMathItem The MathItem class to be extended
* @return {AssistiveMathItem} The augmented MathItem class
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
* @template B The MathItem class to extend
*/
export function LazyMathItemMixin<N, T, D, B extends Constructor<HTMLMathItem<N, T, D>>>(
BaseMathItem: B
): Constructor<LazyMathItem<N, T, D>> & B {
return class extends BaseMathItem {
/**
* True when this item should be skipped during compilation
* (i.e., it is not showing on screen, and has not needed to be
* compiled for a later TeX expression that is showing).
*/
public lazyCompile: boolean = true;
/**
* True when this item should be skipped during typesetting
* (i.e., it has not yet appeared on screen).
*/
public lazyTypeset: boolean = true;
/**
* The marker DOM node for this item.
*/
public lazyMarker: N;
/**
* True if this is a TeX expression.
*/
public lazyTex: boolean = false;
/**
* @override
*/
constructor(...args: any[]) {
super(...args);
if (!this.end.node) {
//
// This is a MathItem that isn't in the document
// (so either from semantic enrich, or from convert())
// and should be typeset as usual.
//
this.lazyCompile = this.lazyTypeset = false;
}
}
/**
* Initially don't compile math, just use an empty math item,
* then when the math comes into view (or is before something
* that comes into view), compile it properly and mark the item
* as only needing to be typeset.
*
* @override
*/
public compile(document: LazyMathDocument<N, T, D>) {
if (!this.lazyCompile) {
super.compile(document);
return;
}
if (this.state() < STATE.COMPILED) {
this.lazyTex = (this.inputJax.name === 'TeX');
this.root = document.mmlFactory.create('math');
this.state(STATE.COMPILED);
}
}
/**
* Only update the state if restore is true or false (null is used for setting lazy states)
* @override
*/
public state(state: number = null, restore: boolean = false) {
if (restore !== null) super.state(state, restore);
return super.state();
}
/**
* Initially, just insert a marker for where the math will go, and
* track it in the lazy list. Then, when it comes into view,
* typeset it properly.
*
* @override
*/
public typeset(document: LazyMathDocument<N, T, D>) {
if (!this.lazyTypeset) {
super.typeset(document);
return;
}
if (this.state() < STATE.TYPESET) {
const adaptor = document.adaptor;
if (!this.lazyMarker) {
const id = document.lazyList.add(this);
this.lazyMarker = adaptor.node('mjx-lazy', {[LAZYID]: id});
this.typesetRoot = adaptor.node('mjx-container', {}, [this.lazyMarker]);
}
this.state(STATE.TYPESET);
}
}
/**
* When the MathItem is added to the page, set the observer to watch
* for it coming into view so that it can be typeset.
*
* @override
*/
public updateDocument(document: LazyMathDocument<N, T, D>) {
super.updateDocument(document);
if (this.lazyTypeset) {
document.lazyObserver.observe(this.lazyMarker as any as Element);
}
}
};
}
/*==========================================================================*/
/**
* The properties added to MathDocument for lazy typesetting
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
export interface LazyMathDocument<N, T, D> extends HTMLDocument<N, T, D> {
/**
* The Intersection Observer used to track the appearance of the expression markers
*/
lazyObserver: IntersectionObserver;
/**
* The mapping of markers to MathItems
*/
lazyList: LazyList<N, T, D>;
/**
* The containers whose contents should always be typeset
*/
lazyAlwaysContainers: N[];
/**
* A function that will typeset all the remaining expressions (e.g., for printing)
*/
lazyTypesetAll(): Promise<void>;
/**
* Mark the math items that are to be always typeset
*/
lazyAlways(): void;
}
/**
* The mixin for adding lazy typesetting to MathDocuments
*
* @param {B} BaseDocument The MathDocument class to be extended
* @return {LazyMathDocument} The Lazy MathDocument class
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
* @template B The MathDocument class to extend
*/
export function LazyMathDocumentMixin<N, T, D,
B extends MathDocumentConstructor<HTMLDocument<N, T, D>>>(
BaseDocument: B
): MathDocumentConstructor<HTMLDocument<N, T, D>> & B {
return class BaseClass extends BaseDocument {
/**
* @override
*/
public static OPTIONS: OptionList = {
...BaseDocument.OPTIONS,
lazyMargin: '200px',
lazyAlwaysTypeset: null,
renderActions: {
...BaseDocument.OPTIONS.renderActions,
lazyAlways: [STATE.LAZYALWAYS, 'lazyAlways', '', false]
}
};
/**
* The Intersection Observer used to track the appearance of the expression markers
*/
public lazyObserver: IntersectionObserver;
/**
* The mapping of markers to MathItems
*/
public lazyList: LazyList<N, T, D>;
/**
* The containers whose contents should always be typeset
*/
public lazyAlwaysContainers: N[] = null;
/**
* Index of last container where math was found in lazyAlwaysContainers
*/
public lazyAlwaysIndex: number = 0;
/**
* A promise to make sure our compiling/typesetting is sequential
*/
protected lazyPromise: Promise<void> = Promise.resolve();
/**
* The function used to typeset a set of lazy MathItems.
* (uses requestIdleCallback if available, or setTimeout otherwise)
*/
protected lazyProcessSet: () => void;
/**
* True when a set of MathItems is queued for being processed
*/
protected lazyIdle: boolean = false;
/**
* The set of items that have come into view
*/
protected lazySet: LazySet = new Set();
/**
* Augment the MathItem class used for this MathDocument,
* then create the intersection observer and lazy list,
* and bind the lazyProcessSet function to this instance
* so it can be used as a callback more easily. Add the
* event listeners to typeset everything before printing.
*
* @override
* @constructor
*/
constructor(...args: any[]) {
super(...args);
//
// Use the LazyMathItem for math items
//
this.options.MathItem =
LazyMathItemMixin<N, T, D, Constructor<HTMLMathItem<N, T, D>>>(this.options.MathItem);
//
// Allocate a process bit for lazyAlways
//
const ProcessBits = (this.constructor as typeof HTMLDocument).ProcessBits;
!ProcessBits.has('lazyAlways') && ProcessBits.allocate('lazyAlways');
//
// Set up the lazy observer and other needed data
//
this.lazyObserver = new IntersectionObserver(this.lazyObserve.bind(this), {rootMargin: this.options.lazyMargin});
this.lazyList = new LazyList<N, T, D>();
const callback = this.lazyHandleSet.bind(this);
this.lazyProcessSet = (window && window.requestIdleCallback ?
() => window.requestIdleCallback(callback) :
() => setTimeout(callback, 10));
//
// Install print listeners to typeset the rest of the document before printing
//
if (window) {
let done = false;
const handler = () => {
!done && this.lazyTypesetAll();
done = true;
};
window.matchMedia('print').addListener(handler); // for Safari
window.addEventListener('beforeprint', handler); // for everyone else
}
}
/**
* Check all math items for those that should always be typeset
*/
public lazyAlways() {
if (!this.lazyAlwaysContainers || this.processed.isSet('lazyAlways')) return;
for (const item of this.math) {
const math = item as LazyMathItem<N, T, D>;
if (math.lazyTypeset && this.lazyIsAlways(math)) {
math.lazyCompile = math.lazyTypeset = false;
}
}
this.processed.set('lazyAlways');
}
/**
* Check if the MathItem is in one of the containers to always typeset.
* (start looking using the last container where math was found,
* in case the next math is in the same container).
*
* @param {LazyMathItem<N,T,D>} math The MathItem to test
* @return {boolean} True if one of the document's containers holds the MathItem
*/
protected lazyIsAlways(math: LazyMathItem<N, T, D>): boolean {
if (math.state() < STATE.LAZYALWAYS) {
math.state(STATE.LAZYALWAYS);
const node = math.start.node;
const adaptor = this.adaptor;
const start = this.lazyAlwaysIndex;
const end = this.lazyAlwaysContainers.length;
do {
const container = this.lazyAlwaysContainers[this.lazyAlwaysIndex];
if (adaptor.contains(container, node)) return true;
if (++this.lazyAlwaysIndex >= end) {
this.lazyAlwaysIndex = 0;
}
} while (this.lazyAlwaysIndex !== start);
}
return false;
}
/**
* @override
*/
public state(state: number, restore: boolean = false) {
super.state(state, restore);
if (state < STATE.LAZYALWAYS) {
this.processed.clear('lazyAlways');
}
return this;
}
/**
* Function to typeset all remaining expressions (for printing, etc.)
*
* @return {Promise} Promise that is resolved after the typesetting completes.
*/
public async lazyTypesetAll(): Promise<void> {
//
// The state we need to go back to (COMPILED or TYPESET).
//
let state = STATE.LAST;
//
// Loop through all the math...
//
for (const item of this.math) {
const math = item as LazyMathItem<N, T, D>;
//
// If it is not lazy compile or typeset, skip it.
//
if (!math.lazyCompile && !math.lazyTypeset) continue;
//
// Mark the state that we need to start at.
//
if (math.lazyCompile) {
math.state(STATE.COMPILED - 1);
state = STATE.COMPILED;
} else {
math.state(STATE.TYPESET - 1);
if (STATE.TYPESET < state) state = STATE.TYPESET;
}
//
// Mark it as not lazy and remove it from the observer.
//
math.lazyCompile = math.lazyTypeset = false;
math.lazyMarker && this.lazyObserver.unobserve(math.lazyMarker as any as Element);
}
//
// If something needs updating
//
if (state === STATE.LAST) return Promise.resolve();
//
// Reset the document state to the starting state that we need.
//
this.state(state - 1, null);
//
// Save the SVG font cache and set it to "none" temporarily
// (needed by Firefox, which doesn't seem to process the
// xlinks otherwise).
//
const fontCache = this.outputJax.options.fontCache;
if (fontCache) this.outputJax.options.fontCache = 'none';
//
// Typeset the math and put back the font cache when done.
//
this.reset();
return handleRetriesFor(() => this.render()).then(() => {
if (fontCache) this.outputJax.options.fontCache = fontCache;
});
}
/**
* The function used by the IntersectionObserver to monitor the markers coming into view.
* When one (or more) does, add its math item to or remove it from the set to be processed, and
* if added to the set, queue an idle task, if one isn't already pending.
*
* The idea is, if you start scrolling and reveal an expression marker, we add it into
* the set and queue an idle task. But if you keep scrolling and the idle task hasn't run
* yet, the mark may move out of view, and we don't want to waste time typesetting it, so
* we remove it from the set. When you stop scrolling and the idle task finally runs, it
* takes the expressions still in the set (those should be the ones still in view) and
* starts typesetting them after having created a new set to add expressions to. If one
* of the expressions loads an extension and the idle task has to pause, you can add new
* expressions into the new list (or remove them from there), but can't affect the
* current idle-task list. Those will be typeset even if they scroll out of view at that
* point.
*
* Note that it is possible that an expression is added to the set and then removed
* before the idle task runs, and it could be that the set is empty when it does. Then
* the idle task does nothing, and new expressions are added to the new set to be
* processed by the next idle task.
*
* @param {IntersectionObserverEntry[]} entries The markers that have come into or out of view.
*/
protected lazyObserve(entries: IntersectionObserverEntry[]) {
for (const entry of entries) {
const id = this.adaptor.getAttribute(entry.target as any as N, LAZYID);
const math = this.lazyList.get(id);
if (!math) continue;
if (!entry.isIntersecting) {
this.lazySet.delete(id);
continue;
}
this.lazySet.add(id);
if (!this.lazyIdle) {
this.lazyIdle = true;
this.lazyProcessSet();
}
}
}
/**
* Mark the MathItems in the set as needing compiling or typesetting,
* and for TeX items, make sure the earlier TeX items are typeset
* (in case they have automatic numbers, or define macros, etc.).
* Then rerender the page to update the visible equations.
*/
protected lazyHandleSet() {
const set = this.lazySet;
this.lazySet = new Set();
this.lazyPromise = this.lazyPromise.then(() => {
let state = this.compileEarlierItems(set) ? STATE.COMPILED : STATE.TYPESET;
state = this.resetStates(set, state);
this.state(state - 1, null); // reset processed bits to allow reprocessing
return handleRetriesFor(() => {
this.render();
this.lazyIdle = false;
});
});
}
/**
* Set the states of the MathItems in the set, depending on
* whether they need compiling or just typesetting, and
* update the state needed for the page rerendering.
*
* @param {LazySet} set The set of math items to update
* @param {number} state The state needed for the items
* @return {number} The updated state based on the items
*/
protected resetStates(set: LazySet, state: number): number {
for (const id of set.values()) {
const math = this.lazyList.get(id);
if (math.lazyCompile) {
math.state(STATE.COMPILED - 1);
state = STATE.COMPILED;
} else {
math.state(STATE.TYPESET - 1);
}
math.lazyCompile = math.lazyTypeset = false;
math.lazyMarker && this.lazyObserver.unobserve(math.lazyMarker as any as Element);
}
return state;
}
/**
* Mark any TeX items (earlier than the ones in the set) to be compiled.
*
* @param {LazySet} set The set of items that are newly visible
* @return {boolean} True if there are TeX items to be typeset
*/
protected compileEarlierItems(set: LazySet): boolean {
let math = this.earliestTex(set);
if (!math) return false;
let compile = false;
for (const item of this.math) {
const earlier = item as LazyMathItem<N, T, D>;
if (earlier === math || !earlier?.lazyCompile || !earlier.lazyTex) {
break;
}
earlier.lazyCompile = false;
earlier.lazyMarker && this.lazyObserver.unobserve(earlier.lazyMarker as any as Element);
earlier.state(STATE.COMPILED - 1);
compile = true;
}
return compile;
}
/**
* Find the earliest TeX math item in the set, if any.
*
* @param {LazySet} set The set of newly visble math items
* @return {LazyMathItem} The earliest TeX math item in the set, if any
*/
protected earliestTex(set: LazySet): LazyMathItem<N, T, D> {
let min: number = null;
let minMath = null;
for (const id of set.values()) {
const math = this.lazyList.get(id);
if (!math.lazyTex) continue;
if (min === null || parseInt(id) < min) {
min = parseInt(id);
minMath = math;
}
}
return minMath;
}
/**
* If any of the removed items are observed or in the lazy list, remove them.
*
* @override
*/
public clearMathItemsWithin(containers: ContainerList<N>) {
const items = super.clearMathItemsWithin(containers) as LazyMathItem<N, T, D>[];
for (const math of items) {
const marker = math.lazyMarker;
if (marker) {
this.lazyObserver.unobserve(marker as any as Element);
this.lazyList.delete(this.adaptor.getAttribute(marker, LAZYID));
}
}
return items;
}
/**
* @override
*/
public render() {
//
// Get the containers whose content should always be typeset
//
const always = this.options.lazyAlwaysTypeset;
this.lazyAlwaysContainers = !always ? null :
this.adaptor.getElements(Array.isArray(always) ? always : [always], this.document);
this.lazyAlwaysIndex = 0;
super.render();
return this;
}
};
}
/*==========================================================================*/
/**
* Add lazy typesetting support to a Handler instance
*
* @param {Handler} handler The Handler instance to enhance
* @return {Handler} The handler that was modified (for purposes of chaining extensions)
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
export function LazyHandler<N, T, D>(handler: HTMLHandler<N, T, D>): HTMLHandler<N, T, D> {
//
// Only update the document class if we can handle IntersectionObservers
//
if (typeof IntersectionObserver !== 'undefined') {
handler.documentClass =
LazyMathDocumentMixin<N, T, D, MathDocumentConstructor<HTMLDocument<N, T, D>>>(
handler.documentClass
) as typeof HTMLDocument;
}
return handler;
}

241
node_modules/mathjax-full/ts/ui/menu/MJContextMenu.ts generated vendored Normal file
View File

@@ -0,0 +1,241 @@
/*************************************************************
*
* Copyright (c) 2019-2022 The MathJax Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Implements a subclass of ContextMenu specific to MathJax
*
* @author dpvc@mathjax.org (Davide Cervone)
*/
import {MathItem} from '../../core/MathItem.js';
import {MmlNode} from '../../core/MmlTree/MmlNode.js';
import {SelectableInfo} from './SelectableInfo.js';
import {ContextMenu} from 'mj-context-menu/js/context_menu.js';
import {SubMenu} from 'mj-context-menu/js/sub_menu.js';
import {Submenu} from 'mj-context-menu/js/item_submenu.js';
import {Menu} from 'mj-context-menu/js/menu.js';
import {Item} from 'mj-context-menu/js/item.js';
/*==========================================================================*/
/**
* The subclass of ContextMenu that handles the needs of the MathJax
* contextual menu (in particular, tying it to a MathItem).
*/
export class MJContextMenu extends ContextMenu {
/**
* Static map to hold methods for re-computing dynamic submenus.
* @type {Map<string, (menu: MJContextMenu, sub: Submenu)}
*/
public static DynamicSubmenus: Map<string,
(menu: MJContextMenu, sub: Submenu) =>
SubMenu> = new Map();
/**
* The MathItem that has posted the menu
*/
public mathItem: MathItem<HTMLElement, Text, Document> = null;
/**
* The annotation selected in the Annotation submenu (neede for the info box to be able to show it)
*/
public annotation: string = '';
/**
* The info box for showing annotations (created by the Menu object that contains this MJContextMenu)
*/
public showAnnotation: SelectableInfo;
/**
* The function to copy the selected annotation (set by the containing Menu item)
*/
public copyAnnotation: () => void;
/**
* The annotation types to look for in a MathItem
*/
public annotationTypes: {[type: string]: string[]} = {};
/*======================================================================*/
/**
* Before posting the menu, set the name for the ShowAs and CopyToClipboard menus,
* enable/disable the semantics check item, and get the annotations for the MathItem
*
* @override
*/
public post(x?: any, y?: number) {
if (this.mathItem) {
if (y !== undefined) {
// FIXME: handle error output jax
const input = this.mathItem.inputJax.name;
const original = this.findID('Show', 'Original');
original.content = (input === 'MathML' ? 'Original MathML' : input + ' Commands');
const clipboard = this.findID('Copy', 'Original');
clipboard.content = original.content;
const semantics = this.findID('Settings', 'semantics');
input === 'MathML' ? semantics.disable() : semantics.enable();
this.getAnnotationMenu();
this.dynamicSubmenus();
}
super.post(x, y);
}
}
/**
* Clear the stored MathItem when the menu is removed
*
* @override
*/
public unpost() {
super.unpost();
this.mathItem = null;
}
/*======================================================================*/
/**
* Find an item in the menu (recursively descending into submenus, if needed)
*
* @param {string[]} names The menu IDs to look for
* @returns {Item} The menu item (or null if not found)
*/
public findID(...names: string[]) {
let menu = this as Menu;
let item = null as Item;
for (const name of names) {
if (menu) {
item = menu.find(name);
menu = (item instanceof Submenu ? item.submenu : null);
} else {
item = null;
}
}
return item;
}
/*======================================================================*/
/**
* Look up the annotations in the MathItem and set the ShowAs and CopyToClipboard menus
*/
protected getAnnotationMenu() {
const annotations = this.getAnnotations(this.getSemanticNode());
this.createAnnotationMenu('Show', annotations, () => this.showAnnotation.post());
this.createAnnotationMenu('Copy', annotations, () => this.copyAnnotation());
}
/**
* Find the top-most semantics element that encloses the contents of the expression (if any)
*
* @returns {MmlNode | null} The semantics node that was found (or null)
*/
protected getSemanticNode(): MmlNode | null {
let node: MmlNode = this.mathItem.root;
while (node && !node.isKind('semantics')) {
if (node.isToken || node.childNodes.length !== 1) return null;
node = node.childNodes[0] as MmlNode;
}
return node;
}
/**
* @param {MmlNode} node The semantics node whose annotations are to be obtained
* @returns {[string, string][]} Array of [type, text] where the type is the annotation type
* and text is the content of the annotation of that type
*/
protected getAnnotations(node: MmlNode): [string, string][] {
const annotations = [] as [string, string][];
if (!node) return annotations;
for (const child of node.childNodes as MmlNode[]) {
if (child.isKind('annotation')) {
const match = this.annotationMatch(child);
if (match) {
const value = child.childNodes.reduce((text, chars) => text + chars.toString(), '');
annotations.push([match, value]);
}
}
}
return annotations;
}
/**
* @param {MmlNode} child The annotation node to check if its encoding is one of the displayable ones
* @returns {string | null} The annotation type if it does, or null if it doesn't
*/
protected annotationMatch(child: MmlNode): string | null {
const encoding = child.attributes.get('encoding') as string;
for (const type of Object.keys(this.annotationTypes)) {
if (this.annotationTypes[type].indexOf(encoding) >= 0) {
return type;
}
}
return null;
}
/**
* Create a submenu from the available annotations and attach it to the proper menu item
*
* @param {string} id The id of the menu to attach to (Show or Copy)
* @param {[string, string][]} annotations The annotations to use for the submenu
* @param {() => void} action The action to perform when the annotation is selected
*/
protected createAnnotationMenu(id: string, annotations: [string, string][], action: () => void) {
const menu = this.findID(id, 'Annotation') as Submenu;
menu.submenu = this.factory.get('subMenu')(this.factory, {
items: annotations.map(([type, value]) => {
return {
type: 'command',
id: type,
content: type,
action: () => {
this.annotation = value;
action();
}
};
}),
id: 'annotations'
}, menu);
if (annotations.length) {
menu.enable();
} else {
menu.disable();
}
}
/*======================================================================*/
/**
* Renews the dynamic submenus.
*/
public dynamicSubmenus() {
for (const [id, method] of MJContextMenu.DynamicSubmenus) {
const menu = this.find(id) as Submenu;
if (!menu) continue;
const sub = method(this, menu);
menu.submenu = sub;
if (sub.items.length) {
menu.enable();
} else {
menu.disable();
}
}
}
}

1177
node_modules/mathjax-full/ts/ui/menu/Menu.ts generated vendored Normal file

File diff suppressed because it is too large Load Diff

284
node_modules/mathjax-full/ts/ui/menu/MenuHandler.ts generated vendored Normal file
View File

@@ -0,0 +1,284 @@
/*************************************************************
*
* Copyright (c) 2019-2022 The MathJax Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Mixin that adds a context-menu to MathJax output
*
* @author dpvc@mathjax.org (Davide Cervone)
*/
import {mathjax} from '../../mathjax.js';
import {STATE, newState} from '../../core/MathItem.js';
import {MathDocumentConstructor} from '../../core/MathDocument.js';
import {Handler} from '../../core/Handler.js';
import {ComplexityMathDocument, ComplexityMathItem} from '../../a11y/complexity.js';
import {ExplorerMathDocument, ExplorerMathItem} from '../../a11y/explorer.js';
import {AssistiveMmlMathDocument, AssistiveMmlMathItem} from '../../a11y/assistive-mml.js';
import {expandable} from '../../util/Options.js';
import {Menu} from './Menu.js';
/*==========================================================================*/
/**
* Generic constructor for Mixins
*/
export type Constructor<T> = new(...args: any[]) => T;
/**
* Constructor for base MathItem for MenuMathItem
*/
export type A11yMathItemConstructor = {
new(...args: any[]): ComplexityMathItem<HTMLElement, Text, Document> &
ExplorerMathItem & AssistiveMmlMathItem<HTMLElement, Text, Document>;
};
/**
* Constructor for base document for MenuMathDocument
*/
export type A11yDocumentConstructor =
MathDocumentConstructor<ComplexityMathDocument<HTMLElement, Text, Document> &
ExplorerMathDocument & AssistiveMmlMathDocument<HTMLElement, Text, Document>>;
/*==========================================================================*/
/**
* Add STATE value for menus being added (after TYPESET and before INSERTED)
*/
newState('CONTEXT_MENU', 170);
/**
* The new function for MathItem that adds the context menu
*/
export interface MenuMathItem extends ComplexityMathItem<HTMLElement, Text, Document> {
/**
* @param {MenuMathDocument} document The document where the menu is being added
* @param {boolean} force True if menu should be added even if enableMenu is false
*/
addMenu(document: MenuMathDocument, force?: boolean): void;
/**
* @param {MenuMathDocument} document The document to check for if anything is being loaded
*/
checkLoading(document: MenuMathDocument): void;
}
/**
* The mixin for adding context menus to MathItems
*
* @param {B} BaseMathItem The MathItem class to be extended
* @return {MathMathItem} The extended MathItem class
*
* @template B The MathItem class to extend
*/
export function MenuMathItemMixin<B extends A11yMathItemConstructor>(
BaseMathItem: B
): Constructor<MenuMathItem> & B {
return class extends BaseMathItem {
/**
* @param {MenuMathDocument} document The document where the menu is being added
* @param {boolean} force True if menu should be added even if enableMenu is false
*/
public addMenu(document: MenuMathDocument, force: boolean = false) {
if (this.state() >= STATE.CONTEXT_MENU) return;
if (!this.isEscaped && (document.options.enableMenu || force)) {
document.menu.addMenu(this);
}
this.state(STATE.CONTEXT_MENU);
}
/**
* @param {MenuMathDocument} document The document to check for if anything is being loaded
*/
public checkLoading(document: MenuMathDocument) {
document.checkLoading();
}
};
}
/*==========================================================================*/
/**
* The properties needed in the MathDocument for context menus
*/
export interface MenuMathDocument extends ComplexityMathDocument<HTMLElement, Text, Document> {
/**
* The menu associated with this document
*/
menu: Menu;
/**
* Add context menus to the MathItems in the MathDocument
*
* @return {MenuMathDocument} The MathDocument (so calls can be chained)
*/
addMenu(): MenuMathDocument;
/**
* Checks if there are files being loaded by the menu, and restarts the typesetting if so
*
* @return {MenuMathDocument} The MathDocument (so calls can be chained)
*/
checkLoading(): MenuMathDocument;
}
/**
* The mixin for adding context menus to MathDocuments
*
* @param {B} BaseDocument The MathDocument class to be extended
* @return {MenuMathDocument} The extended MathDocument class
*
* @template B The MathDocument class to extend
*/
export function MenuMathDocumentMixin<B extends A11yDocumentConstructor>(
BaseDocument: B
): Constructor<MenuMathDocument> & B {
return class extends BaseDocument {
/**
* @override
*/
public static OPTIONS = {
//
// These options are from the a11y extensions, which may not be loaded
// initially, and so would cause "undefined option" error messages
// if a user tries to configure them. So we include them here.
// They are overridden by the options from the extensions when
// those are loaded (via ...BaseDocument.OPTIONS).
//
enableEnrichment: true,
enableComplexity: true,
enableExplorer: true,
enrichSpeech: 'none',
enrichError: (_doc: MenuMathDocument, _math: MenuMathItem, err: Error) =>
console.warn('Enrichment Error:', err),
...BaseDocument.OPTIONS,
MenuClass: Menu,
menuOptions: Menu.OPTIONS,
enableMenu: true,
sre: (BaseDocument.OPTIONS.sre || expandable({})),
a11y: (BaseDocument.OPTIONS.a11y || expandable({})),
renderActions: expandable({
...BaseDocument.OPTIONS.renderActions,
addMenu: [STATE.CONTEXT_MENU],
checkLoading: [STATE.UNPROCESSED + 1]
})
};
/**
* The menu associated with this document
*/
public menu: Menu;
/**
* Extend the MathItem class used for this MathDocument
*
* @override
* @constructor
*/
constructor(...args: any[]) {
super(...args);
this.menu = new this.options.MenuClass(this, this.options.menuOptions);
const ProcessBits = (this.constructor as typeof BaseDocument).ProcessBits;
if (!ProcessBits.has('context-menu')) {
ProcessBits.allocate('context-menu');
}
this.options.MathItem = MenuMathItemMixin<A11yMathItemConstructor>(this.options.MathItem);
}
/**
* Add context menus to the MathItems in the MathDocument
*
* @return {MenuMathDocument} The MathDocument (so calls can be chained)
*/
public addMenu(): MenuMathDocument {
if (!this.processed.isSet('context-menu')) {
for (const math of this.math) {
(math as MenuMathItem).addMenu(this);
}
this.processed.set('context-menu');
}
return this;
}
/**
* Checks if there are files being loaded by the menu, and restarts the typesetting if so
*
* @return {MenuMathDocument} The MathDocument (so calls can be chained)
*/
public checkLoading(): MenuMathDocument {
if (this.menu.isLoading) {
mathjax.retryAfter(this.menu.loadingPromise.catch((err) => console.log(err)));
}
const settings = this.menu.settings;
if (settings.collapsible) {
this.options.enableComplexity = true;
this.menu.checkComponent('a11y/complexity');
}
if (settings.explorer) {
this.options.enableEnrichment = true;
this.options.enableExplorer = true;
this.menu.checkComponent('a11y/explorer');
}
return this;
}
/**
* @override
*/
public state(state: number, restore: boolean = false) {
super.state(state, restore);
if (state < STATE.CONTEXT_MENU) {
this.processed.clear('context-menu');
}
return this;
}
/**
* @override
*/
public updateDocument() {
super.updateDocument();
(this.menu.menu.store as any).sort();
return this;
}
};
}
/*==========================================================================*/
/**
* Add context-menu support to a Handler instance
*
* @param {Handler} handler The Handler instance to enhance
* @return {Handler} The handler that was modified (for purposes of chaining extensions)
*/
export function MenuHandler(handler: Handler<HTMLElement, Text, Document>): Handler<HTMLElement, Text, Document> {
handler.documentClass = MenuMathDocumentMixin<A11yDocumentConstructor>(handler.documentClass as any);
return handler;
}

100
node_modules/mathjax-full/ts/ui/menu/MmlVisitor.ts generated vendored Normal file
View File

@@ -0,0 +1,100 @@
/*************************************************************
*
* Copyright (c) 2019-2022 The MathJax Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview A visitor to serialize MathML taking menu settings into account
*
* @author dpvc@mathjax.org (Davide Cervone)
*/
import {MathItem} from '../../core/MathItem.js';
import {MmlNode} from '../../core/MmlTree/MmlNode.js';
import {SerializedMmlVisitor} from '../../core/MmlTree/SerializedMmlVisitor.js';
import {OptionList, userOptions} from '../../util/Options.js';
/*==========================================================================*/
/**
* The visitor to serialize MathML
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
export class MmlVisitor<N, T, D> extends SerializedMmlVisitor {
/**
* The options controlling the serialization
*/
public options: OptionList = {
texHints: true, // True means include classes for TeXAtom elements
semantics: false, // True means include original form as annotation in a semantics element
};
/**
* The MathItem currently being processed
*/
public mathItem: MathItem<N, T, D> = null;
/**
* @param {MmlNode} node The internal MathML node to serialize
* @param {MathItem} math The MathItem for this node
* @param {OptionList} options The options controlling the processing
* @override
*/
public visitTree(node: MmlNode, math: MathItem<N, T, D> = null, options: OptionList = {}) {
this.mathItem = math;
userOptions(this.options, options);
return this.visitNode(node, '');
}
/**
* @override
*/
public visitTeXAtomNode(node: MmlNode, space: string) {
if (this.options.texHints) {
return super.visitTeXAtomNode(node, space);
}
if (node.childNodes[0] && node.childNodes[0].childNodes.length === 1) {
return this.visitNode(node.childNodes[0], space);
}
return space + '<mrow' + this.getAttributes(node) + '>\n'
+ this.childNodeMml(node, space + ' ', '\n')
+ space + '</mrow>';
}
/**
* @param {MmlNode} node The math node to visit
* @param {string} space The number of spaces to use for indentation
* @returns {string} The serialized math element
*/
public visitMathNode(node: MmlNode, space: string): string {
if (!this.options.semantics || this.mathItem.inputJax.name !== 'TeX') {
return super.visitDefault(node, space);
}
const addRow = node.childNodes.length && node.childNodes[0].childNodes.length > 1;
return space + '<math' + this.getAttributes(node) + '>\n'
+ space + ' <semantics>\n'
+ (addRow ? space + ' <mrow>\n' : '')
+ this.childNodeMml(node, space + (addRow ? ' ' : ' '), '\n')
+ (addRow ? space + ' </mrow>\n' : '')
+ space + ' <annotation encoding="application/x-tex">' + this.mathItem.math + '</annotation>\n'
+ space + ' </semantics>\n'
+ space + '</math>';
}
}

82
node_modules/mathjax-full/ts/ui/menu/SelectableInfo.ts generated vendored Normal file
View File

@@ -0,0 +1,82 @@
/*************************************************************
*
* Copyright (c) 2019-2022 The MathJax Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview An info box that allows text selection and has copy-to-clipboard functions
*
* @author dpvc@mathjax.org (Davide Cervone)
*/
import {Info} from 'mj-context-menu/js/info.js';
import {HtmlClasses} from 'mj-context-menu/js/html_classes.js';
/*==========================================================================*/
/**
* The SelectableInfo class definition
*/
export class SelectableInfo extends Info {
/**
* Add a keypress event to handle "select all" so that only
* the info-box's text is selected (not the whole page)
*
* @override
*/
public addEvents(element: HTMLElement) {
element.addEventListener('keypress', (event: KeyboardEvent) => {
if (event.key === 'a' && (event.ctrlKey || event.metaKey)) {
this.selectAll();
this.stop(event);
}
});
}
/**
* Select all the main text of the info box
*/
public selectAll() {
const selection = document.getSelection();
selection.selectAllChildren(this.html.querySelector('pre'));
}
/**
* Implement the copy-to-clipboard action
*/
public copyToClipboard() {
this.selectAll();
try {
document.execCommand('copy');
} catch (err) {
alert('Can\'t copy to clipboard: ' + err.message);
}
document.getSelection().removeAllRanges();
}
/**
* Attach the copy-to-clipboard action to its button
*/
public generateHtml() {
super.generateHtml();
const footer = this.html.querySelector('span.' + HtmlClasses['INFOSIGNATURE']);
const button = footer.appendChild(document.createElement('input'));
button.type = 'button';
button.value = 'Copy to Clipboard';
button.addEventListener('click', (_event: MouseEvent) => this.copyToClipboard());
}
}

127
node_modules/mathjax-full/ts/ui/safe/SafeHandler.ts generated vendored Normal file
View File

@@ -0,0 +1,127 @@
/*************************************************************
*
* Copyright (c) 2020-2022 The MathJax Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview MathItem, MathDocument, and Handler for the safe extension
*
* @author dpvc@mathjax.org (Davide Cervone)
*/
import {MathItem} from '../../core/MathItem.js';
import {MathDocument, MathDocumentConstructor} from '../../core/MathDocument.js';
import {Handler} from '../../core/Handler.js';
import {Safe} from './safe.js';
/*==========================================================================*/
/**
* Generic constructor for Mixins
*/
export type Constructor<T> = new(...args: any[]) => T;
/*==========================================================================*/
/**
* The properties needed in the MathDocument for sanitizing the internal MathML
*/
export interface SafeMathDocument<N, T, D> extends MathDocument<N, T, D> {
/**
* The Safe object for this document
*/
safe: Safe<N, T, D>;
}
/**
* The mixin for adding safe render action to MathDocuments
*
* @param {B} BaseDocument The MathDocument class to be extended
* @return {SafeMathDocument<N,T,D>} The extended MathDocument class
*/
export function SafeMathDocumentMixin<N, T, D, B extends MathDocumentConstructor<MathDocument<N, T, D>>>(
BaseDocument: B
): Constructor<SafeMathDocument<N, T, D>> & B {
return class extends BaseDocument {
/**
* @override
*/
public static OPTIONS = {
...BaseDocument.OPTIONS,
safeOptions: {
...Safe.OPTIONS,
},
SafeClass: Safe
};
/**
* An instance of the Safe object
*/
public safe: Safe<N, T, D>;
/**
* Extend the MathItem class used for this MathDocument
*
* @override
* @constructor
*/
constructor(...args: any[]) {
super(...args);
this.safe = new this.options.SafeClass(this, this.options.safeOptions);
const ProcessBits = (this.constructor as typeof BaseDocument).ProcessBits;
if (!ProcessBits.has('safe')) {
ProcessBits.allocate('safe');
}
for (const jax of this.inputJax) {
if (jax.name.match(/MathML/)) {
(jax as any).mathml.filterAttribute = this.safe.mmlAttribute.bind(this.safe);
(jax as any).mathml.filterClassList = this.safe.mmlClassList.bind(this.safe);
} else if (jax.name.match(/TeX/)) {
jax.postFilters.add(this.sanitize.bind(jax), -5.5);
}
}
}
/**
* @param {{document:SafeDocument<N,T,D>}} data The document to use for the filter
* (note: this has been bound to the input jax)
*/
protected sanitize(data: {math: MathItem<N, T, D>, document: SafeMathDocument<N, T, D>}) {
data.math.root = (this as any).parseOptions.root;
data.document.safe.sanitize(data.math, data.document);
}
};
}
/*==========================================================================*/
/**
* Add context-menu support to a Handler instance
*
* @param {Handler} handler The Handler instance to enhance
* @return {Handler} The handler that was modified (for purposes of chaining extensions)
*/
export function SafeHandler<N, T, D>(handler: Handler<N, T, D>): Handler<N, T, D> {
handler.documentClass = SafeMathDocumentMixin(handler.documentClass);
return handler;
}

288
node_modules/mathjax-full/ts/ui/safe/SafeMethods.ts generated vendored Normal file
View File

@@ -0,0 +1,288 @@
/*************************************************************
*
* Copyright (c) 2020-2022 The MathJax Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Support functions for the safe extension
*
* @author dpvc@mathjax.org (Davide Cervone)
*/
import {length2em} from '../../util/lengths.js';
import {Safe, FilterFunction} from './safe.js';
/**
* The default attribute-filtering functions
*/
export const SafeMethods: {[name: string]: FilterFunction<any, any, any>} = {
/**
* Filter HREF URL's
*
* @param {Safe<N,T,D>} safe The Safe object being used
* @param {string} url The URL being tested
* @return {string|null} The URL if OK and null if not
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
filterURL<N, T, D>(safe: Safe<N, T, D>, url: string): string | null {
const protocol = (url.match(/^\s*([a-z]+):/i) || [null, ''])[1].toLowerCase();
const allow = safe.allow.URLs;
return (allow === 'all' || (allow === 'safe' &&
(safe.options.safeProtocols[protocol] || !protocol))) ? url : null;
},
/**
* Filter a class list
*
* @param {Safe<N,T,D>} safe The Safe object being used
* @param {string} list The class list being tested
* @return {string|null} The class list if OK and null if not
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
filterClassList<N, T, D>(safe: Safe<N, T, D>, list: string): string | null {
const classes = list.trim().replace(/\s\s+/g, ' ').split(/ /);
return classes.map((name) => this.filterClass(safe, name) || '').join(' ').trim().replace(/\s\s+/g, '');
},
/**
* Filter a class name
*
* @param {Safe<N,T,D>} safe The Safe object being used
* @param {string} CLASS The class being tested
* @return {string|null} The class if OK and null if not
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
filterClass<N, T, D>(safe: Safe<N, T, D>, CLASS: string): string | null {
const allow = safe.allow.classes;
return (allow === 'all' || (allow === 'safe' && CLASS.match(safe.options.classPattern))) ? CLASS : null;
},
/**
* Filter ids
*
* @param {Safe<N,T,D>} safe The Safe object being used
* @param {string} id The id being tested
* @return {string|null} The id if OK and null if not
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
filterID<N, T, D>(safe: Safe<N, T, D>, id: string): string | null {
const allow = safe.allow.cssIDs;
return (allow === 'all' || (allow === 'safe' && id.match(safe.options.idPattern))) ? id : null;
},
/**
* Filter style strings
*
* @param {Safe<N,T,D>} safe The Safe object being used
* @param {string} styles The style string being tested
* @return {string} The sanitized style string
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
filterStyles<N, T, D>(safe: Safe<N, T, D>, styles: string): string {
if (safe.allow.styles === 'all') return styles;
if (safe.allow.styles !== 'safe') return null;
const adaptor = safe.adaptor;
const options = safe.options;
try {
//
// Create div1 with styles set to the given styles, and div2 with blank styles
//
const div1 = adaptor.node('div', {style: styles});
const div2 = adaptor.node('div');
//
// Check each allowed style and transfer OK ones to div2
// If the style has Top/Right/Bottom/Left, look at all four separately
//
for (const style of Object.keys(options.safeStyles)) {
if (options.styleParts[style]) {
for (const sufix of ['Top', 'Right', 'Bottom', 'Left']) {
const name = style + sufix;
const value = this.filterStyle(safe, name, div1);
if (value) {
adaptor.setStyle(div2, name, value);
}
}
} else {
const value = this.filterStyle(safe, style, div1);
if (value) {
adaptor.setStyle(div2, style, value);
}
}
}
//
// Return the div2 style string
//
styles = adaptor.allStyles(div2);
} catch (err) {
styles = '';
}
return styles;
},
/**
* Filter an individual name:value style pair
*
* @param {Safe<N,T,D>} safe The Safe object being used
* @param {string} style The style name being tested
* @param {N} div The temp DIV node containing the style object to be tested
* @return {string|null} The sanitized style string or null if invalid
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
filterStyle<N, T, D>(safe: Safe<N, T, D>, style: string, div: N): string | null {
const value = safe.adaptor.getStyle(div, style);
if (typeof value !== 'string' || value === '' || value.match(/^\s*calc/) ||
(value.match(/javascript:/) && !safe.options.safeProtocols.javascript) ||
(value.match(/data:/) && !safe.options.safeProtocols.data)) {
return null;
}
const name = style.replace(/Top|Right|Left|Bottom/, '');
if (!safe.options.safeStyles[style] && !safe.options.safeStyles[name]) {
return null;
}
return this.filterStyleValue(safe, style, value, div);
},
/**
* Filter a style's value, handling compound values (e.g., borders that have widths as well as styles and colors)
*
* @param {Safe<N,T,D>} safe The Safe object being used
* @param {string} style The style name being tested
* @param {string} value The value of the style to test
* @param {N} div The temp DIV node containing the style object to be tested
* @return {string|null} The sanitized style string or null if invalid
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
filterStyleValue<N, T, D>(safe: Safe<N, T, D>, style: string, value: string, div: N): string | null {
const name = safe.options.styleLengths[style];
if (!name) {
return value;
}
if (typeof name !== 'string') {
return this.filterStyleLength(safe, style, value);
}
const length = this.filterStyleLength(safe, name, safe.adaptor.getStyle(div, name));
if (!length) {
return null;
}
safe.adaptor.setStyle(div, name, length);
return safe.adaptor.getStyle(div, style);
},
/**
* Filter a length value
*
* @param {Safe<N,T,D>} safe The Safe object being used
* @param {string} style The style name being tested
* @param {string} value The value of the style to test
* @return {string|null} The sanitized length value
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
filterStyleLength<N, T, D>(safe: Safe<N, T, D>, style: string, value: string): string | null {
if (!value.match(/^(.+)(em|ex|ch|rem|px|mm|cm|in|pt|pc|%)$/)) return null;
const em = length2em(value, 1);
const lengths = safe.options.styleLengths[style];
const [m, M] = (Array.isArray(lengths) ? lengths : [-safe.options.lengthMax, safe.options.lengthMax]);
return (m <= em && em <= M ? value : (em < m ? m : M).toFixed(3).replace(/\.?0+$/, '') + 'em');
},
/**
* Filter a font size
*
* @param {Safe<N,T,D>} safe The Safe object being used
* @param {string} size The font size to test
* @return {string|null} The sanitized style string or null if invalid
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
filterFontSize<N, T, D>(safe: Safe<N, T, D>, size: string): string | null {
return this.filterStyleLength(safe, 'fontSize', size);
},
/**
* Filter scriptsizemultiplier
*
* @param {Safe<N,T,D>} safe The Safe object being used
* @param {string} size The script size multiplier to test
* @return {string} The sanitized size
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
filterSizeMultiplier<N, T, D>(safe: Safe<N, T, D>, size: string): string {
const [m, M] = safe.options.scriptsizemultiplierRange || [-Infinity, Infinity];
return Math.min(M, Math.max(m, parseFloat(size))).toString();
},
/**
* Filter scriptLevel
*
* @param {Safe<N,T,D>} safe The Safe object being used
* @param {string} size The scriptlevel to test
* @return {string|null} The sanitized scriptlevel or null
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
filterScriptLevel<N, T, D>(safe: Safe<N, T, D>, level: string): string | null {
const [m, M] = safe.options.scriptlevelRange || [-Infinity, Infinity];
return Math.min(M, Math.max(m, parseInt(level))).toString();
},
/**
* Filter a data-* attribute
*
* @param {Safe<N,T,D>} safe The Safe object being used
* @param {string} value The attribute's value
* @param {string} id The attribute's id (e.g., data-mjx-variant)
* @return {number|null} The sanitized value or null
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
filterData<N, T, D>(safe: Safe<N, T, D>, value: string, id: string): string | null {
return (id.match(safe.options.dataPattern) ? value : null);
}
};

270
node_modules/mathjax-full/ts/ui/safe/safe.ts generated vendored Normal file
View File

@@ -0,0 +1,270 @@
/*************************************************************
*
* Copyright (c) 2020-2022 The MathJax Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Support for the safe extension
*
* @author dpvc@mathjax.org (Davide Cervone)
*/
import {Property} from '../../core/Tree/Node.js';
import {MmlNode} from '../../core/MmlTree/MmlNode.js';
import {MathItem} from '../../core/MathItem.js';
import {MathDocument} from '../../core/MathDocument.js';
import {OptionList, expandable} from '../../util/Options.js';
import {DOMAdaptor} from '../../core/DOMAdaptor.js';
import {SafeMethods} from './SafeMethods.js';
/**
* Function type for filtering attributes
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
export type FilterFunction<N, T, D> = (safe: Safe<N, T, D>, value: Property, ...args: any[]) => Property;
/**
* The Safe object for sanitizing the internal MathML representation of an expression
*
* @template N The HTMLElement node class
* @template T The Text node class
* @template D The Document class
*/
export class Safe<N, T, D> {
/**
* The options controlling the handling of the safe extension
*/
public static OPTIONS: OptionList = {
allow: {
//
// Values can be "all", "safe", or "none"
//
URLs: 'safe', // safe are in safeProtocols below
classes: 'safe', // safe start with mjx- (can be set by pattern below)
cssIDs: 'safe', // safe start with mjx- (can be set by pattern below)
styles: 'safe' // safe are in safeStyles below
},
//
// Largest padding/border/margin, etc. in em's
//
lengthMax: 3,
//
// Valid range for scriptsizemultiplier
//
scriptsizemultiplierRange: [.6, 1],
//
// Valid range for scriptlevel
//
scriptlevelRange: [-2, 2],
//
// Pattern for allowed class names
//
classPattern: /^mjx-[-a-zA-Z0-9_.]+$/,
//
// Pattern for allowed ids
//
idPattern: /^mjx-[-a-zA-Z0-9_.]+$/,
//
// Pattern for data attributes
//
dataPattern: /^data-mjx-/,
//
// Which URL protocols are allowed
//
safeProtocols: expandable({
http: true,
https: true,
file: true,
javascript: false,
data: false
}),
//
// Which styles are allowed
//
safeStyles: expandable({
color: true,
backgroundColor: true,
border: true,
cursor: true,
margin: true,
padding: true,
textShadow: true,
fontFamily: true,
fontSize: true,
fontStyle: true,
fontWeight: true,
opacity: true,
outline: true
}),
//
// CSS styles that have Top/Right/Bottom/Left versions
//
styleParts: expandable({
border: true,
padding: true,
margin: true,
outline: true
}),
//
// CSS styles that are lengths needing max/min testing
// A string value means test that style value;
// An array gives [min,max] in em's
// Otherwise use [-lengthMax,lengthMax] from above
//
styleLengths: expandable({
borderTop: 'borderTopWidth',
borderRight: 'borderRightWidth',
borderBottom: 'borderBottomWidth',
borderLeft: 'borderLeftWidth',
paddingTop: true,
paddingRight: true,
paddingBottom: true,
paddingLeft: true,
marginTop: true,
marginRight: true,
marginBottom: true,
marginLeft: true,
outlineTop: true,
outlineRight: true,
outlineBottom: true,
outlineLeft: true,
fontSize: [.707, 1.44]
})
};
/**
* The attribute-to-filter-method mapping
*/
public filterAttributes: Map<string, string> = new Map([
//
// Methods called for MathML attribute processing
//
['href', 'filterURL'],
['src', 'filterURL'],
['altimg', 'filterURL'],
['class', 'filterClassList'],
['style', 'filterStyles'],
['id', 'filterID'],
['fontsize', 'filterFontSize'],
['mathsize', 'filterFontSize'],
['scriptminsize', 'filterFontSize'],
['scriptsizemultiplier', 'filterSizeMultiplier'],
['scriptlevel', 'filterScriptLevel'],
['data-', 'filterData']
]);
/**
* The safe options from the document option list
*/
public options: OptionList;
/**
* Shorthand for options.allow
*/
public allow: OptionList;
/**
* The DOM adaptor from the document
*/
public adaptor: DOMAdaptor<N, T, D>;
/**
* The methods for filtering the MathML attributes
*/
public filterMethods: {[name: string]: FilterFunction<N, T, D>} = {
...SafeMethods
};
/**
* @param {MathDocument<N,T,D>} document The MathDocument we are sanitizing
* @param {OptionList} options The safeOptions from the document
*/
constructor(document: MathDocument<N, T, D>, options: OptionList) {
this.adaptor = document.adaptor;
this.options = options;
this.allow = this.options.allow;
}
/**
* Sanitize a MathItem's root MathML tree
*
* @param {MathItem<N,T,D>} math The MathItem to sanitize
* @param {MathDocument<N,T,D>} document The MathDocument in which it lives
*/
public sanitize(math: MathItem<N, T, D>, document: MathDocument<N, T, D>) {
try {
math.root.walkTree(this.sanitizeNode.bind(this));
} catch (err) {
document.options.compileError(document, math, err);
}
}
/**
* Sanitize a node's attributes
*
* @param {MmlNode} node The node to sanitize
*/
protected sanitizeNode(node: MmlNode) {
const attributes = node.attributes.getAllAttributes();
for (const id of Object.keys(attributes)) {
const method = this.filterAttributes.get(id);
if (method) {
const value = this.filterMethods[method](this, attributes[id]);
if (value) {
if (value !== (typeof value === 'number' ? parseFloat(attributes[id] as string) : attributes[id])) {
attributes[id] = value;
}
} else {
delete attributes[id];
}
}
}
}
/**
* Sanitize a MathML input attribute
*
* @param {string} id The name of the attribute
* @param {string} value The value of the attribute
* @return {string|null} The sanitized value
*/
public mmlAttribute(id: string, value: string): string | null {
if (id === 'class') return null;
const method = this.filterAttributes.get(id);
const filter = (method || (id.substr(0, 5) === 'data-' ? this.filterAttributes.get('data-') : null));
if (!filter) {
return value;
}
const result = this.filterMethods[filter](this, value, id);
return (typeof result === 'number' || typeof result === 'boolean' ? String(result) : result);
}
/**
* Sanitize a list of class names
*
* @param {string[]} list The list of class names
* @return {string[]} The sanitized list
*/
public mmlClassList(list: string[]): string[] {
return list.map((name) => this.filterMethods.filterClass(this, name) as string)
.filter((value) => value !== null);
}
}