scribble-mathjax/unpacked/extensions/a11y/collapsible.js
2017-04-24 15:46:28 -04:00

742 lines
24 KiB
JavaScript

/*************************************************************
*
* [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.2",
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,
default: 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",
default: "f()"
},
fraction: "/",
sqrt: "\u221A",
root: "\u221A",
superscript: "\u25FD\u02D9",
subscript: "\u25FD.",
subsup:"\u25FD:",
vector: {
binomial: "(:)",
determinant: "|:|",
default: "\u27E8:\u27E9"
},
matrix: {
squarematrix: "[::]",
rowvector: "\u27E8\u22EF\u27E9",
columnvector: "\u27E8\u22EE\u27E9",
determinant: "|::|",
default: "(::)"
},
cases: "{:",
infixop: {
addition: "+",
subtraction: "\u2212",
multiplication: "\u22C5",
implicit: "\u22C5",
default: "+"
},
punctuated: {
text: "...",
default: ","
}
},
/*****************************************************************/
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 <math> 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({
data: mml.data,
complexity: mml.complexity,
attrNames: [], attr: {}
});
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.default;
if (mml.complexity > complexity) {
var marker = this.MARKER[type];
if (typeof(marker) !== "string") marker = marker[role] || marker.default;
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.default;
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");
});