/************************************************************* * * Copyright (c) 2017 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 Utility functions for standard pre and post filters. * * @author sorge@mathjax.org (Volker Sorge) */ import {TEXCLASS, MmlNode, TextNode} from '../../core/MmlTree/MmlNode.js'; import NodeUtil from './NodeUtil.js'; import ParseOptions from './ParseOptions.js'; import {MmlMo} from '../../core/MmlTree/MmlNodes/mo.js'; import {Attributes} from '../../core/MmlTree/Attributes.js'; namespace FilterUtil { /** * Visitor to set stretchy attributes to false on elements, if they are * not used as delimiters. Also wraps non-stretchy infix delimiters into a * TeXAtom. * @param {MmlNode} node The node to rewrite. * @param {ParseOptions} options The parse options. */ export let cleanStretchy = function(arg: {math: any, data: ParseOptions}) { let options = arg.data; for (let mo of options.getList('fixStretchy')) { if (NodeUtil.getProperty(mo, 'fixStretchy')) { let symbol = NodeUtil.getForm(mo); if (symbol && symbol[3] && symbol[3]['stretchy']) { NodeUtil.setAttribute(mo, 'stretchy', false); } const parent = mo.parent; if (!NodeUtil.getTexClass(mo) && (!symbol || !symbol[2])) { const texAtom = options.nodeFactory.create('node', 'TeXAtom', [mo]); parent.replaceChild(texAtom, mo); texAtom.inheritAttributesFrom(mo); } NodeUtil.removeProperties(mo, 'fixStretchy'); } } }; /** * Visitor that removes superfluous attributes from nodes. I.e., if a node has * an attribute, which is also an inherited attribute it will be removed. This * is necessary as attributes are set bottom up in the parser. * @param {MmlNode} mml The node to clean. * @param {ParseOptions} options The parse options. */ export let cleanAttributes = function(arg: {data: ParseOptions}) { let node = arg.data.root as MmlNode; node.walkTree((mml: MmlNode, d: any) => { let attribs = mml.attributes as any; for (const key of attribs.getExplicitNames()) { if (attribs.attributes[key] === mml.attributes.getInherited(key)) { delete attribs.attributes[key]; } } }, {}); }; /** * Combine adjacent elements that are relations (since MathML treats the * spacing very differently) * @param {MmlNode} mml The node in which to combine relations. * @param {ParseOptions} options The parse options. */ export let combineRelations = function(arg: {data: ParseOptions}) { for (let mo of arg.data.getList('mo')) { if (mo.getProperty('relationsCombined') || !mo.parent || (mo.parent && !NodeUtil.isType(mo.parent, 'mrow')) || NodeUtil.getTexClass(mo) !== TEXCLASS.REL) { // @test Prime, PrimeSup, Named Function continue; } let mml = mo.parent; let m2: MmlNode; let children = mml.childNodes as (MmlNode|TextNode)[]; let next = children.indexOf(mo) + 1; let variantForm = NodeUtil.getProperty(mo, 'variantForm'); while (next < children.length && (m2 = children[next]) && NodeUtil.isType(m2, 'mo') && NodeUtil.getTexClass(m2) === TEXCLASS.REL) { if (variantForm === NodeUtil.getProperty(m2, 'variantForm') && _compareExplicit(mo, m2)) { // @test Shift Left, Less Equal, // Multirel Font X, Multirel Mathvariant X NodeUtil.appendChildren(mo, NodeUtil.getChildren(m2)); // This treatment means we might loose some inheritance structure, but // no properties. _copyExplicit(['stretchy', 'rspace'], mo, m2); NodeUtil.setProperties(mo, m2.getAllProperties()); children.splice(next, 1); m2.parent = null; m2.setProperty('relationsCombined', true); } else { // @test Preset Rspace Lspace if (mo.attributes.getExplicit('rspace') == null) { // @test Mulitrel Mathvariant 3, Mulitrel Mathvariant 4 NodeUtil.setAttribute(mo, 'rspace', '0pt'); } if (m2.attributes.getExplicit('lspace') == null) { // @test Mulitrel Mathvariant 3, Mulitrel Mathvariant 4 NodeUtil.setAttribute(m2, 'lspace', '0pt'); } break; } } mo.attributes.setInherited('form', (mo as MmlMo).getForms()[0]); } }; /** * Copies the specified explicit attributes from node2 to node1. * @param {string[]} attrs List of explicit attribute names. * @param {MmlNode} node1 The goal node. * @param {MmlNode} node2 The source node. */ let _copyExplicit = function(attrs: string[], node1: MmlNode, node2: MmlNode) { let attr1 = node1.attributes; let attr2 = node2.attributes; attrs.forEach(x => { let attr = attr2.getExplicit(x); if (attr != null) { // @test Infix Stretchy Right, Preset Lspace Rspace attr1.set(x, attr); } }); }; /** * Compares the explicit attributes of two nodes. Returns true if they * coincide, with the following exceptions: * - lspace attribute of node1 is ignored. * - rspace attribute of node2 is ignored. * - stretchy=false attributes are ignored. * @param {MmlNode} node1 The first node. * @param {MmlNode} node2 Its next sibling. */ let _compareExplicit = function(node1: MmlNode, node2: MmlNode) { let filter = (attr: Attributes, space: string): string[] => { let exp = attr.getExplicitNames(); return exp.filter(x => { return x !== space && (x !== 'stretchy' || attr.getExplicit('stretchy')); }); }; let attr1 = node1.attributes; let attr2 = node2.attributes; let exp1 = filter(attr1, 'lspace'); let exp2 = filter(attr2, 'rspace'); if (exp1.length !== exp2.length) { return false; } for (let name of exp1) { if (attr1.getExplicit(name) !== attr2.getExplicit(name)) { return false; } } return true; }; /** * Cleans msubsup and munderover elements. * @param {ParseOptions} options The parse options. * @param {string} low String representing the lower part of the expression. * @param {string} up String representing the upper part. */ let _cleanSubSup = function(options: ParseOptions, low: string, up: string) { for (let mml of options.getList('m' + low + up) as any[]) { const children = mml.childNodes; if (children[mml[low]] && children[mml[up]]) { continue; } const parent = mml.parent; let newNode = (children[mml[low]] ? options.nodeFactory.create('node', 'm' + low, [children[mml.base], children[mml[low]]]) : options.nodeFactory.create('node', 'm' + up, [children[mml.base], children[mml[up]]])); NodeUtil.copyAttributes(mml, newNode); if (parent) { parent.replaceChild(newNode, mml); } else { options.root = newNode; } } }; /** * Visitor that rewrites incomplete msubsup/munderover elements in the given * node into corresponding msub/sup/under/over nodes. * @param {MmlNode} node The node to rewrite. * @param {ParseOptions} options The parse options. */ export let cleanSubSup = function(arg: {math: any, data: ParseOptions}) { let options = arg.data; if (options.error) { return; } _cleanSubSup(options, 'sub', 'sup'); _cleanSubSup(options, 'under', 'over'); options.root.setInheritedAttributes({}, arg.math['display'], 0, false); }; } export default FilterUtil;