/************************************************************* * * [Contrib]/a11y/collapsible.js * * A filter to add maction elements to the enriched MathML for parts that * can be collapsed. We determine this based on a "complexity" value and * collapse those terms that exceed a given complexity. * * The parameters controlling the complexity measure still need work. * * --------------------------------------------------------------------- * * Copyright (c) 2016-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. */ (function (HUB) { var MML; var SETTINGS = HUB.config.menuSettings; var COOKIE = {}; // replaced when menu is available var NOCOLLAPSE = 10000000; // really big complexity var COMPLEXATTR = "data-semantic-complexity"; // // Set up the a11y path,if it isn't already in place // var PATH = MathJax.Ajax.config.path; if (!PATH.a11y) PATH.a11y = HUB.config.root + "/extensions/a11y"; var Collapsible = MathJax.Extension.collapsible = { version: "1.2.3", config: HUB.CombineConfig("collapsible",{ disabled: false }), dependents: [], // the extensions that depend on this one COMPLEXATTR: COMPLEXATTR, // attribute name for the complexity value /*****************************************************************/ // // Complexity values to use for different structures // COMPLEXITY: { TEXT: .5, // each character of a token element adds this to complexity TOKEN: .5, // each toekn element gets this additional complexity CHILD: 1, // child nodes add this to their parent node's complexity SCRIPT: .8, // script elements reduce their complexity by this factor SQRT: 2, // sqrt adds this extra complexity SUBSUP: 2, // sub-sup adds this extra complexity UNDEROVER: 2, // under-over adds this extra complexity FRACTION: 2, // fractions add this extra complexity ACTION: 2, // maction adds this extra complexity PHANTOM: 0, // mphantom makes complexity 0? XML: 2, // Can't really measure complexity of annotation-xml, so punt GLYPH: 2 // Can't really measure complexity of mglyph, to punt }, // // These are the cut-off complexity values for when // the structure should collapse // COLLAPSE: { identifier: 3, number: 3, text: 10, infixop: 15, relseq: 15, multirel: 15, fenced: 18, bigop: 20, integral: 20, fraction: 12, sqrt: 9, root: 12, vector: 15, matrix: 15, cases: 15, superscript: 9, subscript: 9, subsup: 9, punctuated: { endpunct: NOCOLLAPSE, startpunct: NOCOLLAPSE, value: 12 } }, // // These are the characters to use for the various collapsed elements // (if an object, then semantic-role is used to get the character // from the object) // MARKER: { identifier: "x", number: "#", text: "...", appl: { "limit function": "lim", value: "f()" }, fraction: "/", sqrt: "\u221A", root: "\u221A", superscript: "\u25FD\u02D9", subscript: "\u25FD.", subsup:"\u25FD:", vector: { binomial: "(:)", determinant: "|:|", value: "\u27E8:\u27E9" }, matrix: { squarematrix: "[::]", rowvector: "\u27E8\u22EF\u27E9", columnvector: "\u27E8\u22EE\u27E9", determinant: "|::|", value: "(::)" }, cases: "{:", infixop: { addition: "+", subtraction: "\u2212", multiplication: "\u22C5", implicit: "\u22C5", value: "+" }, punctuated: { text: "...", value: "," } }, /*****************************************************************/ Enable: function (update,menu) { SETTINGS.collapsible = true; if (menu) COOKIE.collapsible = true; this.config.disabled = false; MathJax.Extension["semantic-enrich"].Enable(false,menu); if (update) HUB.Queue(["Reprocess",HUB]); }, Disable: function (update,menu) { SETTINGS.collapsible = false; if (menu) COOKIE.collapsible = false; this.config.disabled = true; for (var i = this.dependents.length-1; i >= 0; i--) { var dependent = this.dependents[i]; if (dependent.Disable) dependent.Disable(false,menu); } if (update) HUB.Queue(["Reprocess",HUB]); }, // // Register a dependent // Dependent: function (extension) { this.dependents.push(extension); }, Startup: function () { MML = MathJax.ElementJax.mml; // // Inform semantic-enrich extension that we are a dependent // var Enrich = MathJax.Extension["semantic-enrich"]; if (Enrich) Enrich.Dependent(this); // // Add the filter into the post-input hooks (priority 100, so other // hooks run first, in particular, the enrichment hook). // HUB.postInputHooks.Add(["Filter",Collapsible],100); }, // // The main filter: add mactions for collapsing the math. // Filter: function (jax,id,script) { if (!jax.enriched || this.config.disabled) return; jax.root = jax.root.Collapse(); jax.root.inputID = script.id; }, /*****************************************************************/ // // Create a marker from a given string of characters // (several possibilities are commented out) // // Marker: function (c) {return MML.mtext("\u25C3"+c+"\u25B9").With({mathcolor:"blue",attr:{},attrNames:[]})}, // Marker: function (c) {return MML.mtext("\u25B9"+c+"\u25C3").With({mathcolor:"blue",attr:{},attrNames:[]})}, Marker: function (c) {return MML.mtext("\u25C2"+c+"\u25B8").With({mathcolor:"blue",attr:{},attrNames:[]})}, // Marker: function (c) {return MML.mtext("\u25B8"+c+"\u25C2").With({mathcolor:"blue",attr:{},attrNames:[]})}, // Marker: function (c) {return MML.mtext("\u27EA"+c+"\u27EB").With({mathcolor:"blue",attr:{},attrNames:[]})}, /*****************************************************************/ // // Make a collapsible element using maction that contains // an appropriate marker, and the expanded MathML. // If the MathML is a node, make an mrow to use instead, // and move the semantic data to it (I guess it would have been // easier to have had that done initially, oh well). // MakeAction: function (collapse,mml) { var maction = MML.maction(collapse).With({ id:this.getActionID(), actiontype:"toggle", complexity:collapse.getComplexity(), collapsible:true, attrNames:["id","actiontype","selection",COMPLEXATTR], attr:{}, selection:2 }); maction.attr[COMPLEXATTR] = maction.complexity; if (mml.type === "math") { var mrow = MML.mrow().With({ complexity: mml.complexity, attrNames: [], attr: {} }); mrow.Append.apply(mrow,mml.data); for (var i = mml.attrNames.length-1, name; name = mml.attrNames[i]; i--) { if (name.substr(0,14) === "data-semantic-") { mrow.attr[name] = mml.attr[name]; mrow.attrNames.push(name); delete mml.attr[name]; mml.attrNames.splice(i,1); } } mrow.complexity = mml.complexity; maction.Append(mrow); mml.data = []; mml.Append(maction); mml.complexity = maction.complexity; maction = mml; } else { maction.Append(mml); } return maction; }, actionID: 1, getActionID: function () {return "MJX-Collapse-"+this.actionID++}, /*****************************************************************/ // // If there is a specific routine for the type, do that, otherwise // check if there is a complexity cut-off and marker for this type. // If so, check if the complexity exceeds the cut off, and // collapse using the appropriate marker for the type // Return the (possibly modified) MathML // Collapse: function (mml) { mml.getComplexity(); var type = (mml.attr||{})["data-semantic-type"]; if (type) { if (this["Collapse_"+type]) mml = (this["Collapse_"+type])(mml); else if (this.COLLAPSE[type] && this.MARKER[type]) { var role = mml.attr["data-semantic-role"]; var complexity = this.COLLAPSE[type]; if (typeof(complexity) !== "number") complexity = complexity[role] || complexity.value; if (mml.complexity > complexity) { var marker = this.MARKER[type]; if (typeof(marker) !== "string") marker = marker[role] || marker.value; mml = this.MakeAction(this.Marker(marker),mml); } } } return mml; }, // // If a parent is going to be collapsible, if can call this // to put back a collapsed child (rather than have too many // nested collapsings) // UncollapseChild: function (mml,n,m) { if (m == null) m = 1; if (this.SplitAttribute(mml,"children").length === m) { var child = (mml.data.length === 1 && mml.data[0].inferred ? mml.data[0] : mml); if (child && child.data[n] && child.data[n].collapsible) { child.SetData(n,child.data[n].data[1]); mml.complexity = child.complexity = null; mml.getComplexity(); return 1; } } return 0; }, // // Locate child node and return its text // FindChildText: function (mml,id) { var child = this.FindChild(mml,id); return (child ? (child.CoreMO()||child).data.join("") : "?"); }, FindChild: function (mml,id) { if (mml) { if (mml.attr && mml.attr["data-semantic-id"] === id) return mml; if (!mml.isToken) { for (var i = 0, m = mml.data.length; i < m; i++) { var child = this.FindChild(mml.data[i],id); if (child) return child; } } } return null; }, // // Split a data attribute at commas // SplitAttribute: function (mml,id) { return (mml.attr["data-semantic-"+id]||"").split(/,/); }, /*****************************************************************/ /* * These routines implement the collapsing of the various semantic types */ // // For fenced elements, if the contents are collapsed, // collapse the fence instead. // Collapse_fenced: function (mml) { this.UncollapseChild(mml,1); if (mml.complexity > this.COLLAPSE.fenced) { if (mml.attr["data-semantic-role"] === "leftright") { var marker = mml.data[0].data.join("") + mml.data[mml.data.length-1].data.join(""); mml = this.MakeAction(this.Marker(marker),mml); } } return mml; }, // // Collapse function applications if the argument is collapsed // (Handle role="limit function" a bit better?) // Collapse_appl: function (mml) { if (this.UncollapseChild(mml,2,2)) { var marker = this.MARKER.appl; marker = marker[mml.attr["data-semantic-role"]] || marker.value; mml = this.MakeAction(this.Marker(marker),mml); } return mml; }, // // For sqrt elements, if the contents are collapsed, // collapse the sqrt instead. // Collapse_sqrt: function (mml) { this.UncollapseChild(mml,0); if (mml.complexity > this.COLLAPSE.sqrt) mml = this.MakeAction(this.Marker(this.MARKER.sqrt),mml); return mml; }, Collapse_root: function (mml) { this.UncollapseChild(mml,0); if (mml.complexity > this.COLLAPSE.sqrt) mml = this.MakeAction(this.Marker(this.MARKER.sqrt),mml); return mml; }, // // For enclose, include enclosure in collapsed child, if any // Collapse_enclose: function (mml) { if (this.SplitAttribute(mml,"children").length === 1) { var child = (mml.data.length === 1 && mml.data[0].inferred ? mml.data[0] : mml); if (child.data[0] && child.data[0].collapsible) { // // Move menclose into the maction element // var maction = child.data[0]; child.SetData(0,maction.data[1]); maction.SetData(1,mml); mml = maction; } } return mml; }, // // For bigops, get the character to use from the largeop at its core. // Collapse_bigop: function (mml) { if (mml.complexity > this.COLLAPSE.bigop || mml.data[0].type !== "mo") { var id = this.SplitAttribute(mml,"content").pop(); var op = Collapsible.FindChildText(mml,id); mml = this.MakeAction(this.Marker(op),mml); } return mml; }, Collapse_integral: function (mml) { if (mml.complexity > this.COLLAPSE.integral || mml.data[0].type !== "mo") { var id = this.SplitAttribute(mml,"content")[0]; var op = Collapsible.FindChildText(mml,id); mml = this.MakeAction(this.Marker(op),mml); } return mml; }, // // For multirel and relseq, use proper symbol // Collapse_relseq: function (mml) { if (mml.complexity > this.COLLAPSE.relseq) { var content = this.SplitAttribute(mml,"content"); var marker = Collapsible.FindChildText(mml,content[0]); if (content.length > 1) marker += "\u22EF"; mml = this.MakeAction(this.Marker(marker),mml); } return mml; }, Collapse_multirel: function (mml) { if (mml.complexity > this.COLLAPSE.multirel) { var content = this.SplitAttribute(mml,"content"); var marker = Collapsible.FindChildText(mml,content[0]) + "\u22EF"; mml = this.MakeAction(this.Marker(marker),mml); } return mml; }, // // Include super- and subscripts into a collapsed base // Collapse_superscript: function (mml) { this.UncollapseChild(mml,0,2); if (mml.complexity > this.COLLAPSE.superscript) mml = this.MakeAction(this.Marker(this.MARKER.superscript),mml); return mml; }, Collapse_subscript: function (mml) { this.UncollapseChild(mml,0,2); if (mml.complexity > this.COLLAPSE.subscript) mml = this.MakeAction(this.Marker(this.MARKER.subscript),mml); return mml; }, Collapse_subsup: function (mml) { this.UncollapseChild(mml,0,3); if (mml.complexity > this.COLLAPSE.subsup) mml = this.MakeAction(this.Marker(this.MARKER.subsup),mml); return mml; } }; HUB.Register.StartupHook("End Extensions", function () { if (SETTINGS.collapsible == null) { SETTINGS.collapsible = !Collapsible.config.disabled; } else { Collapsible.config.disabled = !SETTINGS.collapsible; } HUB.Register.StartupHook("MathMenu Ready", function () { COOKIE = MathJax.Menu.cookie; var Switch = function(menu) { Collapsible[SETTINGS.collapsible ? "Enable" : "Disable"](true,true); MathJax.Menu.saveCookie(); }; var ITEM = MathJax.Menu.ITEM, MENU = MathJax.Menu.menu; var menu = ITEM.CHECKBOX( ['CollapsibleMath','Collapsible Math'], 'collapsible', {action: Switch} ); var submenu = (MENU.FindId('Accessibility')||{}).submenu, index; if (submenu) { index = submenu.IndexOfId('CollapsibleMath'); if (index !== null) { submenu.items[index] = menu; } else { submenu.items.push(ITEM.RULE(),menu); } } else { index = MENU.IndexOfId('About'); MENU.items.splice(index,0,menu,ITEM.RULE()); } },15); // before explorer extension },15); })(MathJax.Hub); /*****************************************************************/ /* * Add Collapse() and getComplexity() methods to the internal * MathML elements, and override these in the elements that need * special handling. */ MathJax.Ajax.Require("[a11y]/semantic-enrich.js"); MathJax.Hub.Register.StartupHook("Semantic Enrich Ready", function () { var MML = MathJax.ElementJax.mml, Collapsible = MathJax.Extension.collapsible, COMPLEXITY = Collapsible.COMPLEXITY, COMPLEXATTR = Collapsible.COMPLEXATTR; Collapsible.Startup(); // Initialize the collapsing process MML.mbase.Augment({ // // Just call the Collapse() method from the extension by default // (but can be overridden) // Collapse: function () {return Collapsible.Collapse(this)}, // // If we don't have a cached complexity value, // For token elements, just use the data length, // Otherwise // Add up the complexities of the (collapsed) children // and add the child complexity based on the number of children // Cache the complexity result // return the complexity // getComplexity: function () { if (this.complexity == null) { var complexity = 0; if (this.isToken) { complexity = COMPLEXITY.TEXT * this.data.join("").length + COMPLEXITY.TOKEN; } else { for (var i = 0, m = this.data.length; i < m; i++) { if (this.data[i]) { this.SetData(i,this.data[i].Collapse()); complexity += this.data[i].complexity; } } if (m > 1) complexity += m * COMPLEXITY.CHILD; } if (this.attrNames && !("complexity" in this)) this.attrNames.push(COMPLEXATTR); if (this.attr) this.attr[COMPLEXATTR] = complexity; this.complexity = complexity; } return this.complexity; }, reportComplexity: function () { if (this.attr && this.attrNames && !(COMPLEXATTR in this.attr)) { this.attrNames.push(COMPLEXATTR); this.attr[COMPLEXATTR] = this.complexity; } } }); // // For fractions, scale the complexity of the parts, and add // a complexity for fractions. // MML.mfrac.Augment({ getComplexity: function () { if (this.complexity == null) { this.SUPER(arguments).getComplexity.call(this); this.complexity *= COMPLEXITY.SCRIPT; this.complexity += COMPLEXITY.FRACTION; this.attr[COMPLEXATTR] = this.complexity; } return this.complexity; } }); // // Square roots add extra complexity // MML.msqrt.Augment({ getComplexity: function () { if (this.complexity == null) { this.SUPER(arguments).getComplexity.call(this); this.complexity += COMPLEXITY.SQRT; this.attr[COMPLEXATTR] = this.complexity; } return this.complexity; } }); MML.mroot.Augment({ getComplexity: function () { if (this.complexity == null) { this.SUPER(arguments).getComplexity.call(this); this.complexity -= (1-COMPLEXITY.SCRIPT) * this.data[1].getComplexity(); this.complexity += COMPLEXITY.SQRT; this.attr[COMPLEXATTR] = this.complexity; } return this.complexity; } }); // // For msubsup, use the script complexity factor, // take the maximum of the scripts, // and add the sub-sup complexity // MML.msubsup.Augment({ getComplexity: function () { if (this.complexity == null) { var C = 0; if (this.data[this.sub]) C = this.data[this.sub].getComplexity() + COMPLEXITY.CHILD; if (this.data[this.sup]) C = Math.max(this.data[this.sup].getComplexity(),C); C *= COMPLEXITY.SCRIPT; if (this.data[this.sub]) C += COMPLEXITY.CHILD; if (this.data[this.sup]) C += COMPLEXITY.CHILD; if (this.data[this.base]) C += this.data[this.base].getComplexity() + COMPLEXITY.CHILD; this.complexity = C + COMPLEXITY.SUBSUP; this.reportComplexity(); } return this.complexity; } }); // // For munderover, use the script complexity factor, // take the maximum of the scripts and the base, // and add the under-over complexity // MML.munderover.Augment({ getComplexity: function () { if (this.complexity == null) { var C = 0; if (this.data[this.sub]) C = this.data[this.sub].getComplexity() + COMPLEXITY.CHILD; if (this.data[this.sup]) C = Math.max(this.data[this.sup].getComplexity(),C); C *= COMPLEXITY.SCRIPT; if (this.data[this.base]) C = Math.max(this.data[this.base].getComplexity(),C); if (this.data[this.sub]) C += COMPLEXITY.CHILD; if (this.data[this.sup]) C += COMPLEXITY.CHILD; if (this.data[this.base]) C += COMPLEXITY.CHILD; this.complexity = C + COMPLEXITY.UNDEROVER; this.reportComplexity(); } return this.complexity; } }); // // For mphantom, complexity is 0? // MML.mphantom.Augment({ getComplexity: function () { this.complexity = COMPLEXITY.PHANTOM; this.reportComplexity(); return this.complexity; } }); // // For ms, add width of quotes. Don't cache the result, since // mstyle above it could affect the result. // MML.ms.Augment({ getComplexity: function () { this.SUPER(arguments).getComplexity.call(this); this.complexity += this.Get("lquote").length * COMPLEXITY.TEXT; this.complexity += this.Get("rquote").length * COMPLEXITY.TEXT; this.attr[COMPLEXATTR] = this.complexity; return this.complexity; } }); // ### FIXME: getComplexity special cases: // mtable, mfenced, mmultiscript // // For menclose, complexity goes up by a fixed amount // MML.menclose.Augment({ getComplexity: function () { if (this.complexity == null) { this.SUPER(arguments).getComplexity.call(this); this.complexity += COMPLEXITY.ACTION; this.attr[COMPLEXATTR] = this.complexity; } return this.complexity; } }); // // For maction, complexity is complexity of selected element // MML.maction.Augment({ getComplexity: function () { // // Don't cache it, since selection can change. // this.complexity = (this.collapsible ? this.data[0] : this.selected()).getComplexity(); this.reportComplexity(); return this.complexity; } }); // // For semantics, complexity is complexity of first child // MML.semantics.Augment({ getComplexity: function () { if (this.complexity == null) { this.complexity = (this.data[0] ? this.data[0].getComplexity() : 0); this.reportComplexity(); } return this.complexity; } }); // // Use fixed complexity, since we can't really measure it // MML["annotation-xml"].Augment({ getComplexity: function () { this.complexity = COMPLEXITY.XML; this.reportComplexity(); return this.complexity; } }); MML.annotation.Augment({ getComplexity: function () { this.complexity = COMPLEXITY.XML; this.reportComplexity(); return this.complexity; } }); // // Use fixed complexity, since we can't really measure it // MML.mglyph.Augment({ getComplexity: function () { this.complexity = COMPLEXITY.GLYPH; this.reportComplexity(); return this.complexity; } }); // // Signal that we are ready // MathJax.Hub.Startup.signal.Post("Collapsible Ready"); MathJax.Ajax.loadComplete("[a11y]/collapsible.js"); });