mathjax
Version:
Beautiful math in all browsers. MathJax is an open-source JavaScript display engine for LaTeX, MathML, and AsciiMath notation that works in all browsers.
1,203 lines (1,139 loc) • 142 kB
JavaScript
/* -*- Mode: Javascript; indent-tabs-mode:nil; js-indent-level: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/*************************************************************
*
* MathJax/jax/output/HTML-CSS/jax.js
*
* Implements the HTML-CSS OutputJax that displays mathematics
* using HTML and CSS to position the characters from math fonts
* in their proper locations.
*
* ---------------------------------------------------------------------
*
* Copyright (c) 2009-2019 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 (AJAX,HUB,HTMLCSS) {
var MML, isMobile = HUB.Browser.isMobile;
var isArray = MathJax.Object.isArray;
var MESSAGE = function () {
var data = [].slice.call(arguments,0);
data[0][0] = ["HTML-CSS",data[0][0]];
return MathJax.Message.Set.apply(MathJax.Message,data);
};
var FONTTEST = MathJax.Object.Subclass({
timeout: (isMobile? 15:8)*1000, // timeout for loading web fonts
comparisonFont: ["sans-serif","monospace","script","Times","Courier","Arial","Helvetica"],
testSize: ["40px","50px","60px","30px","20px"],
//
// Fedora aliases STIXSizeOneSym to STIX Word, so MathJax thinks STIX is
// available, but the fonts aren't actually correct. This is to test if
// STIXSizeOneSym has letters in it (so is actually STIX Word).
//
FedoraSTIXcheck: {family:"STIXSizeOneSym", testString:"abcABC", noStyleChar:true},
Init: function () {
//
// Wrap the Font_Test DIV in a 0x0 DIV so that it takes no room
//
this.div = MathJax.HTML.addElement(document.body,"div",{style: {
position:"absolute", width:0, height:0, overflow:"hidden",
padding:0, border:0, margin:0
}},[["div",{
id: "MathJax_Font_Test",
style: {position:"absolute", visibility:"hidden", top:0, left:0, width: "auto",
"min-width": 0, "max-width": "none",
padding:0, border:0, margin:0, whiteSpace:"nowrap",
textAlign:"left", textIndent:0, textTransform:"none",
lineHeight:"normal", letterSpacing:"normal", wordSpacing:"normal",
fontSize:this.testSize[0], fontWeight:"normal", fontStyle:"normal",
fontSizeAdjust:"none"}
},[""]]]
).firstChild;
this.text = this.div.firstChild;
},
findFont: function (fonts,pref) {
var found = null;
if (pref && this.testCollection(pref)) {
found = pref;
} else {
for (var i = 0, m = fonts.length; i < m; i++) {
if (fonts[i] === pref) continue;
if (this.testCollection(fonts[i])) {found = fonts[i]; break}
}
}
if (found === "STIX" && this.testFont(this.FedoraSTIXcheck)) {found = null}
return found;
},
testCollection: function (name) {
var font = {testString: "() {} []"};
font.family = {TeX:"MathJax_Size1", STIX:"STIXSizeOneSym"}[name] ||
name.replace(/-(Math)?/,"")+"MathJax_Size1";
if (name === "STIX") {font.noStyleChar = true}
return this.testFont(font);
},
testFont: function (font) {
if (font.isWebFont && HTMLCSS.FontFaceBug) {
this.div.style.fontWeight = this.div.style.fontStyle = "normal";
} else {
this.div.style.fontWeight = (font.weight||"normal");
this.div.style.fontStyle = (font.style||"normal");
}
//
// Hack: Fix up web font names for local access.
// (The names for Windows and Mac are different, unlike in the STIX and
// TeX fonts, so we have to work out a list of names here.)
//
// This should be removed when the web fonts are fixed. FIXME
//
var family = font.familyFixed || font.family;
if (!font.isWebFont && !family.match(/^(STIX|MathJax)|'/)) {
family = family.replace(/_/g," ").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/ Jax/,"Jax")
+ "','" + family + "','" + family + "-";
if (font.weight) {family += "Bold"}; if (font.style) {family += "Italic"}
if (!font.weight && !font.style) {family += "Regular"}
font.familyFixed = family = "'"+family+"'"
}
var W = this.getComparisonWidths(font.testString,font.noStyleChar);
var found = null;
if (W) {
this.div.style.fontFamily = family+","+this.comparisonFont[0];
if (this.div.offsetWidth == W[0]) {
this.div.style.fontFamily = family+","+this.comparisonFont[W[2]];
if (this.div.offsetWidth == W[1]) {found = false}
}
if (found === null && (this.div.offsetWidth != W[3] || this.div.offsetHeight != W[4])) {
if (!font.noStyleChar && HTMLCSS.FONTDATA && HTMLCSS.FONTDATA.hasStyleChar) {
for (var i = 0, m = this.testSize.length; i < m; i++)
{if (this.testStyleChar(font,this.testSize[i])) {found = true; m = 0}}
} else {found = true}
}
}
if (HTMLCSS.safariTextNodeBug) {this.div.innerHTML = ""} else {this.text.nodeValue = ""}
return found;
},
styleChar: "\uEFFD", // width encodes style
versionChar: "\uEFFE", // width encodes version
compChar: "\uEFFF", // "standard" width to compare to
testStyleChar: function (font,size) {
var n = 3 + (font.weight ? 2 : 0) + (font.style ? 4 : 0);
var extra = "", dw = 0;
var SIZE = this.div.style.fontSize; this.div.style.fontSize = size;
if (HTMLCSS.msieItalicWidthBug && font.style === "italic") {
this.text.nodeValue = extra = this.compChar;
dw = this.div.offsetWidth;
}
if (HTMLCSS.safariTextNodeBug) {this.div.innerHTML = this.compChar+extra}
else {this.text.nodeValue = this.compChar+extra}
var W = this.div.offsetWidth-dw;
if (HTMLCSS.safariTextNodeBug) {this.div.innerHTML = this.styleChar+extra}
else {this.text.nodeValue = this.styleChar+extra}
var N = Math.floor((this.div.offsetWidth-dw)/W+.5);
if (N === n) {
if (HTMLCSS.safariTextNodeBug) {this.div.innerHTML = this.versionChar+extra}
else {this.text.nodeValue = this.versionChar+extra}
font.version = Math.floor((this.div.offsetWidth-dw)/W+1.5)/2;
}
this.div.style.fontSize = SIZE;
return (N === n);
},
getComparisonWidths: function (string,noStyleChar) {
if (HTMLCSS.FONTDATA && HTMLCSS.FONTDATA.hasStyleChar && !noStyleChar)
{string += this.styleChar + " " + this.compChar}
if (HTMLCSS.safariTextNodeBug) {this.div.innerHTML = string}
else {this.text.nodeValue = string}
this.div.style.fontFamily = this.comparisonFont[0];
var W = this.div.offsetWidth;
this.div.style.fontFamily = HTMLCSS.webFontDefault;
var sW = this.div.offsetWidth, sH = this.div.offsetHeight;
for (var i = 1, m = this.comparisonFont.length; i < m; i++) {
this.div.style.fontFamily = this.comparisonFont[i];
if (this.div.offsetWidth != W) {return [W,this.div.offsetWidth,i,sW,sH]}
}
return null;
},
loadWebFont: function (font) {
HUB.Startup.signal.Post("HTML-CSS Jax - Web-Font "+HTMLCSS.fontInUse+"/"+font.directory);
var n = MESSAGE(["LoadWebFont","Loading web-font %1",HTMLCSS.fontInUse+"/"+font.directory]);
var done = MathJax.Callback({}); // called when font is loaded
var callback = MathJax.Callback(["loadComplete",this,font,n,done]);
AJAX.timer.start(AJAX,[this.checkWebFont,font,callback],0,this.timeout);
return done;
},
loadComplete: function (font,n,done,status) {
MathJax.Message.Clear(n);
if (status === AJAX.STATUS.OK) {this.webFontLoaded = true; done(); return}
this.loadError(font);
if (HUB.Browser.isFirefox && HTMLCSS.allowWebFonts) {
var host = document.location.protocol + "//" + document.location.hostname;
if (document.location.port != "") {host += ":" + document.location.port}
host += "/";
if (AJAX.fileURL(HTMLCSS.webfontDir).substr(0,host.length) !== host)
{this.firefoxFontError(font)}
}
if (!this.webFontLoaded) {HTMLCSS.loadWebFontError(font,done)} else {done()}
},
loadError: function (font) {
MESSAGE(["CantLoadWebFont","Can't load web font %1",HTMLCSS.fontInUse+"/"+font.directory],null,2000);
HUB.Startup.signal.Post("HTML-CSS Jax - web font error for " + HTMLCSS.fontInUse+"/"+font.directory);
},
firefoxFontError: function (font) {
MESSAGE(["FirefoxCantLoadWebFont","Firefox can't load web fonts from a remote host"],null,3000);
HUB.Startup.signal.Post("HTML-CSS Jax - Firefox web fonts on remote host error");
},
checkWebFont: function (check,font,callback) {
if (check.time(callback)) return;
if (HTMLCSS.Font.testFont(font)) {callback(check.STATUS.OK)}
else {setTimeout(check,check.delay)}
},
fontFace: function (name) {
var type = HTMLCSS.allowWebFonts;
var FONT = HTMLCSS.FONTDATA.FONTS[name];
if (HTMLCSS.msieFontCSSBug && !FONT.family.match(/-Web$/)) {FONT.family += "-Web"}
if (FONT.isWebFont) delete FONT.familyFixed;
var webfonts = HTMLCSS.webfontDir+"/"+type;
var dir = AJAX.fileURL(webfonts);
var fullname = name.replace(/-b/,"-B").replace(/-i/,"-I").replace(/-Bold-/,"-Bold");
if (!fullname.match(/-/)) {fullname += "-Regular"}
if (type === "svg") {fullname += ".svg#"+fullname} else {fullname += "."+type}
var rev = AJAX.fileRev(webfonts+"/"+fullname.replace(/#.*/,""));
var def = {
"font-family": FONT.family,
src: "url('"+dir+"/"+fullname+rev+"')"
};
if (type === "otf") {
fullname = fullname.replace(/otf$/,"woff");
rev = AJAX.fileRev(webfonts+"/"+fullname);
def.src += " format('opentype')";
dir = AJAX.fileURL(HTMLCSS.webfontDir+"/woff"); // add woff fonts as well
def.src = "url('"+dir+"/"+fullname+rev+"') format('woff'), "+def.src;
} else if (type !== "eot") {def.src += " format('"+type+"')"}
if (!(HTMLCSS.FontFaceBug && FONT.isWebFont)) {
if (name.match(/-bold/)) {def["font-weight"] = "bold"}
if (name.match(/-italic/)) {def["font-style"] = "italic"}
}
return def;
}
});
var EVENT, TOUCH, HOVER; // filled in later
var oldIE = MathJax.Hub.Browser.isMSIE && (document.documentMode||0) < 8;
HTMLCSS.Augment({
config: {
styles: {
".MathJax": {
"display": "inline",
"font-style": "normal",
"font-weight": "normal",
"line-height": "normal",
"font-size": "100%",
"font-size-adjust":"none",
"text-indent": 0,
"text-align": "left",
"text-transform": "none",
"letter-spacing": "normal",
"word-spacing": "normal",
"word-wrap": "normal",
"white-space": "nowrap",
"float": "none",
"direction": "ltr",
"max-width": "none", "max-height": "none",
"min-width": 0, "min-height": 0,
border: 0, padding: 0, margin: 0
},
// Focus elements for keyboard tabbing.
".MathJax:focus, body :focus .MathJax": {
display:"inline-table" // see issues #1282 and #1338
},
".MathJax_Display": {
position: "relative",
display: "block!important",
"text-indent": 0,
"max-width": "none", "max-height": "none",
"min-width": 0, "min-height": 0,
width: "100%"
},
".MathJax.MathJax_FullWidth": {
"text-align": "center",
display: (oldIE ? "block" : "table-cell") + "!important",
width: (oldIE ? "100%" : "10000em") + "!important"
},
".MathJax img, .MathJax nobr, .MathJax a": {
border: 0, padding: 0, margin: 0,
"max-width": "none", "max-height": "none",
"min-width": 0, "min-height": 0,
"vertical-align": 0, "line-height": "normal",
"text-decoration": "none"
},
"img.MathJax_strut": {
border:"0!important", padding:"0!important", margin:"0!important",
"vertical-align": "0!important"
},
".MathJax span": {
display: "inline", position: "static",
border: 0, padding: 0, margin: 0,
"vertical-align": 0, "line-height": "normal",
"text-decoration": "none",
"box-sizing": "content-box"
},
".MathJax nobr": {
"white-space": "nowrap!important"
},
".MathJax img": {
display: "inline!important",
"float": "none!important"
},
".MathJax *": {
transition: "none",
"-webkit-transition": "none",
"-moz-transition": "none",
"-ms-transition": "none",
"-o-transition": "none"
},
".MathJax_Processing": {
visibility: "hidden", position:"fixed",
width: 0, height: 0, overflow:"hidden"
},
".MathJax_Processed": {display:"none!important"},
".MathJax_test": {
"font-style": "normal",
"font-weight": "normal",
"font-size": "100%",
"font-size-adjust":"none",
"text-indent": 0,
"text-transform": "none",
"letter-spacing": "normal",
"word-spacing": "normal",
overflow: "hidden",
height: "1px"
},
".MathJax_test.mjx-test-display": {
display: (oldIE ? "block" : "table") + "!important"
},
".MathJax_test.mjx-test-inline": {
display: "inline!important",
"margin-right": "-1px"
},
".MathJax_test.mjx-test-default": {
display: "block!important",
clear: "both"
},
".MathJax_ex_box": {
display: "inline-block!important",
position: "absolute",
overflow: "hidden",
"min-height": 0, "max-height":"none",
padding:0, border: 0, margin: 0,
width:"1px", height:"60ex"
},
".MathJax_em_box": {
display: "inline-block!important",
position: "absolute",
overflow: "hidden",
"min-height": 0, "max-height":"none",
padding:0, border: 0, margin: 0,
width:"1px", height:"60em"
},
".mjx-test-inline .MathJax_left_box": {
display: "inline-block",
width: 0,
"float":"left"
},
".mjx-test-inline .MathJax_right_box": {
display: "inline-block",
width: 0,
"float":"right"
},
".mjx-test-display .MathJax_right_box": {
display: (oldIE ? "block" : "table-cell") + "!important",
width: (oldIE ? "100%" : "10000em") + "!important",
"min-width":0, "max-width":"none",
padding:0, border:0, margin:0
},
".MathJax .MathJax_HitBox": {
cursor: "text",
background: "white",
opacity:0, filter:"alpha(opacity=0)"
},
".MathJax .MathJax_HitBox *": {
filter: "none", opacity:1, background:"transparent" // for IE
},
"#MathJax_Tooltip": {
position: "absolute", left: 0, top: 0,
width: "auto", height: "auto",
display: "none"
},
"#MathJax_Tooltip *": {
filter: "none", opacity:1, background:"transparent" // for IE
},
//
// Used for testing web fonts against the default font used while
// web fonts are loading
//
"@font-face": {
"font-family": "MathJax_Blank",
"src": "url('about:blank')"
}
}
},
settings: HUB.config.menuSettings,
Font: null, // created by Config() below
webFontDefault: "MathJax_Blank",
allowWebFonts: "otf", // assume browser can use OTF web fonts
maxStretchyParts: 1000, // limit the number of parts allowed for
// stretchy operators. See issue 366.
fontName: {
TeXLocal: "TeX",
TeXWeb: ["","TeX"],
TeXImage: ["",""],
STIXLocal: ["STIX","STIX-Web"],
STIXWeb: "STIX-Web",
AsanaMathWeb: "Asana-Math",
GyrePagellaWeb: "Gyre-Pagella",
GyreTermesWeb: "Gyre-Termes",
LatinModernWeb: "Latin-Modern",
NeoEulerWeb: "Neo-Euler"
},
fontInUse: "generic",
FONTDATA: {
TeX_factor: 1, baselineskip: 1.2, lineH: .8, lineD: .2, ffLineH: .8,
FONTS: {},
VARIANT: {"normal": {fonts:[]}, "-generic-variant": {}, "-largeOp": {}, "-smallOp": {}},
RANGES: [], DELIMITERS: {}, RULECHAR: 0x2D, REMAP: {}
},
Config: function () {
if (!this.require) {this.require = []}
this.Font = FONTTEST(); this.SUPER(arguments).Config.call(this);
var settings = this.settings, config = this.config, font = settings.font;
if (this.adjustAvailableFonts) {this.adjustAvailableFonts(config.availableFonts)}
if (settings.scale) {config.scale = settings.scale}
if (font && font !== "Auto" && this.fontName[font]) {
config.availableFonts = []; delete config.fonts;
if (isArray(this.fontName[font])) {
config.preferredFont = this.fontName[font][0];
config.webFont = this.fontName[font][1];
} else {
config.preferredFont = config.webFont = this.fontName[font];
}
if (config.preferredFont) {config.availableFonts[0] = config.preferredFont}
}
if (config.fonts) {
config.availableFonts = config.fonts;
config.preferredFont = config.webFont = config.fonts[0];
if (config.webFont === "STIX") {config.webFont += "-Web"}
}
font = this.Font.findFont(config.availableFonts,config.preferredFont);
if (!font && this.allowWebFonts) {font = config.webFont; if (font) {this.webFonts = true}}
if (!font && this.config.imageFont) {font = config.imageFont; this.imgFonts = true}
if (font) {
this.fontInUse = font; this.fontDir += "/" + font; this.webfontDir += "/" + font;
this.require.push(this.fontDir+"/fontdata.js");
if (this.imgFonts) {
this.require.push(this.directory+"/imageFonts.js");
HUB.Startup.signal.Post("HTML-CSS Jax - using image fonts");
}
} else {
MESSAGE(["CantFindFontUsing","Can't find a valid font using %1",
"["+this.config.availableFonts.join(", ")+"]"],null,3000);
HUB.Startup.signal.Post("HTML-CSS Jax - no valid font");
}
this.require.push(MathJax.OutputJax.extensionDir+"/MathEvents.js");
},
Startup: function () {
// Set up event handling
EVENT = MathJax.Extension.MathEvents.Event;
TOUCH = MathJax.Extension.MathEvents.Touch;
HOVER = MathJax.Extension.MathEvents.Hover;
this.ContextMenu = EVENT.ContextMenu;
this.Mousedown = EVENT.AltContextMenu;
this.Mouseover = HOVER.Mouseover;
this.Mouseout = HOVER.Mouseout;
this.Mousemove = HOVER.Mousemove;
// Make hidden div for when math is in a display:none block
this.hiddenDiv = this.Element("div",{
style:{visibility:"hidden", overflow:"hidden", position:"absolute", top:0,
height:"1px", width: "auto", padding:0, border:0, margin:0,
textAlign:"left", textIndent:0, textTransform:"none",
lineHeight:"normal", letterSpacing:"normal", wordSpacing:"normal"}
});
if (!document.body.firstChild) {document.body.appendChild(this.hiddenDiv)}
else {document.body.insertBefore(this.hiddenDiv,document.body.firstChild)}
this.hiddenDiv = this.addElement(this.hiddenDiv,"div",{id:"MathJax_Hidden"});
// Determine pixels per inch
var div = this.addElement(this.hiddenDiv,"div",{style:{width:"5in"}});
this.pxPerInch = div.offsetWidth/5; this.hiddenDiv.removeChild(div);
// Markers used by getW
this.startMarker = this.createStrut(this.Element("span"),10,true);
this.endMarker = this.addText(this.Element("span"),"x").parentNode;
// Used in getHD
this.HDspan = this.Element("span");
if (this.operaHeightBug) {this.createStrut(this.HDspan,0)}
if (this.msieInlineBlockAlignBug) {
this.HDimg = this.addElement(this.HDspan,"img",{style:{height:"0px", width:"1px"}});
try {this.HDimg.src = "about:blank"} catch(err) {}
} else {
this.HDimg = this.createStrut(this.HDspan,0);
}
// Used in preTranslate to get scaling factors
this.TestSpan = this.Element("span",
{className:"MathJax_test"},
[
["span",{className:"MathJax_left_box"}],
["span",{className:"MathJax_ex_box"}],
["span",{className:"MathJax_em_box"}],
["span",{className:"MathJax_right_box"}]
]
);
// Set up styles and preload web fonts
return AJAX.Styles(this.config.styles,["InitializeHTML",this]);
},
removeSTIXfonts: function (fonts) {
//
// Opera doesn't display large chunks of the STIX fonts, and
// Safari/Windows doesn't display Plane1,
// so disable STIX for these browsers.
//
// ### FIXME ### Do we need to disable the other web fonts for these?
//
for (var i = 0, m = fonts.length; i < m; i++)
{if (fonts[i] === "STIX") {fonts.splice(i,1); m--; i--;}}
if (this.config.preferredFont === "STIX") {this.config.preferredFont = fonts[0]}
},
PreloadWebFonts: function () {
if (!HTMLCSS.allowWebFonts || !HTMLCSS.config.preloadWebFonts) return;
for (var i = 0, m = HTMLCSS.config.preloadWebFonts.length; i < m; i++) {
var FONT = HTMLCSS.FONTDATA.FONTS[HTMLCSS.config.preloadWebFonts[i]];
if (!FONT.available) {HTMLCSS.Font.testFont(FONT)}
}
},
//
// Handle initialization that requires styles to be set up
//
InitializeHTML: function () {
this.PreloadWebFonts();
this.getDefaultExEm();
//
// If the defaultEm size is zero, it might be that a web font hasn't
// arrived yet, so try to wait for it, but don't wait too long.
//
if (this.defaultEm) return;
var ready = MathJax.Callback();
AJAX.timer.start(AJAX,function (check) {
if (check.time(ready)) {HUB.signal.Post(["HTML-CSS Jax - no default em size"]); return}
HTMLCSS.getDefaultExEm();
if (HTMLCSS.defaultEm) {ready()} else {setTimeout(check,check.delay)}
},this.defaultEmDelay,this.defaultEmTimeout);
return ready;
},
defaultEmDelay: 100, // initial delay when checking for defaultEm
defaultEmTimeout: 1000, // when to stop looking for defaultEm
getDefaultExEm: function () {
//
// Get the default sizes (need styles in place to do this)
//
var test = document.body.appendChild(this.TestSpan.cloneNode(true));
test.className += " mjx-test-inline mjx-test-default";
this.defaultEx = test.childNodes[1].offsetHeight/60;
this.defaultEm = test.childNodes[2].offsetHeight/60;
this.defaultWidth = Math.max(0,test.lastChild.offsetLeft-test.firstChild.offsetLeft-2);
document.body.removeChild(test);
},
preTranslate: function (state) {
var scripts = state.jax[this.id], i, m = scripts.length, n,
script, prev, span, div, test, jax, ex, em, scale, maxwidth, relwidth = false, cwidth,
linebreak = this.config.linebreaks.automatic, width = this.config.linebreaks.width;
if (linebreak) {
relwidth = (width.match(/^\s*(\d+(\.\d*)?%\s*)?container\s*$/) != null);
if (relwidth) {width = width.replace(/\s*container\s*/,"")}
else {maxwidth = this.defaultWidth}
if (width === "") {width = "100%"}
} else {maxwidth = 100000} // a big width, so no implicit line breaks
//
// Loop through the scripts
//
for (i = 0; i < m; i++) {
script = scripts[i]; if (!script.parentNode) continue;
//
// Remove any existing output
//
prev = script.previousSibling;
if (prev && String(prev.className).match(/^MathJax(_Display)?( MathJax_Process(ing|ed))?$/))
{prev.parentNode.removeChild(prev)}
if (script.MathJax.preview) script.MathJax.preview.style.display = "none";
//
// Add the span, and a div if in display mode,
// then mark it as being processed
//
jax = script.MathJax.elementJax; if (!jax) continue;
jax.HTMLCSS = {display: (jax.root.Get("display") === "block")}
span = div = this.Element("span",{
className:"MathJax", id:jax.inputID+"-Frame", isMathJax:true, jaxID:this.id,
oncontextmenu:EVENT.Menu, onmousedown: EVENT.Mousedown,
onmouseover:EVENT.Mouseover, onmouseout:EVENT.Mouseout,
onmousemove:EVENT.Mousemove, onclick:EVENT.Click,
ondblclick:EVENT.DblClick,
// Added for keyboard accessible menu.
onkeydown: EVENT.Keydown, tabIndex: HUB.getTabOrder(jax)
});
if (HUB.Browser.noContextMenu) {
span.ontouchstart = TOUCH.start;
span.ontouchend = TOUCH.end;
}
if (jax.HTMLCSS.display) {
div = this.Element("div",{className:"MathJax_Display"});
div.appendChild(span);
} else if (this.msieDisappearingBug) {span.style.display = "inline-block"}
div.className += " MathJax_Processing";
script.parentNode.insertBefore(div,script);
jax.HTMLCSS.span = span; jax.HTMLCSS.div = div; // save for use in Translate()
//
// Add the test span for determining scales and linebreak widths
//
test = this.TestSpan.cloneNode(true);
test.className += " mjx-test-" + (jax.HTMLCSS.display ? "display" : "inline");
script.parentNode.insertBefore(test,script);
}
//
// Determine the scaling factors for each script
// (this only requires one reflow rather than a reflow for each equation)
// Record any that need to be hidden (don't move them now, since that
// would cause reflows).
//
var hidden = [];
for (i = 0; i < m; i++) {
script = scripts[i]; if (!script.parentNode) continue;
test = script.previousSibling; div = test.previousSibling;
jax = script.MathJax.elementJax; if (!jax) continue;
ex = test.childNodes[1].offsetHeight/60;
em = test.childNodes[2].offsetHeight/60;
cwidth = Math.max(0, jax.HTMLCSS.display ? test.lastChild.offsetWidth - 1:
test.lastChild.offsetLeft - test.firstChild.offsetLeft - 2);
if (ex === 0 || ex === "NaN") {
// can't read width, so move to hidden div for processing
hidden.push(div);
jax.HTMLCSS.isHidden = true;
ex = this.defaultEx; em = this.defaultEm; cwidth = this.defaultWidth;
}
if (cwidth === 0 && !jax.HTMLCSS.display) cwidth = this.defaultWidth;
if (relwidth) {maxwidth = cwidth}
scale = (this.config.matchFontHeight ? ex/this.TeX.x_height/em : 1);
scale = Math.floor(Math.max(this.config.minScaleAdjust/100,scale)*this.config.scale);
jax.HTMLCSS.scale = scale/100; jax.HTMLCSS.fontSize = scale+"%";
jax.HTMLCSS.em = jax.HTMLCSS.outerEm = em; this.em = em * scale/100; jax.HTMLCSS.ex = ex;
jax.HTMLCSS.cwidth = cwidth/this.em;
jax.HTMLCSS.lineWidth = (linebreak ? this.length2em(width,1,maxwidth/this.em) : 1000000);
}
for (i = 0, n = hidden.length; i < n; i++) {
this.hiddenDiv.appendChild(hidden[i]);
this.addElement(this.hiddenDiv,"br");
}
//
// Remove the test spans used for determining scales and linebreak widths
//
for (i = 0; i < m; i++) {
script = scripts[i]; if (!script.parentNode) continue;
jax = scripts[i].MathJax.elementJax; if (!jax) continue;
script.parentNode.removeChild(script.previousSibling);
if (script.MathJax.preview) script.MathJax.preview.style.display = "";
}
//
// Set state variables used for displaying equations in chunks
//
state.HTMLCSSeqn = state.HTMLCSSlast = 0; state.HTMLCSSi = -1;
state.HTMLCSSchunk = this.config.EqnChunk;
state.HTMLCSSdelay = false;
},
PHASE: {I: 1, II: 2, III: 3}, // processing phases
Translate: function (script,state) {
if (!script.parentNode) return;
//
// If we are supposed to do a chunk delay, do it
//
if (state.HTMLCSSdelay) {
state.HTMLCSSdelay = false;
HUB.RestartAfter(MathJax.Callback.Delay(this.config.EqnChunkDelay));
}
//
// Get the data about the math
//
var jax = script.MathJax.elementJax, math = jax.root,
div = jax.HTMLCSS.div, span = jax.HTMLCSS.span;
if (!document.getElementById(span.id)) return;
//
// Set the font metrics
//
this.getMetrics(jax);
if (this.scale !== 1) {span.style.fontSize = jax.HTMLCSS.fontSize}
//
// Typeset the math
//
this.initImg(span);
this.initHTML(math,span);
this.savePreview(script);
try {
math.setTeXclass();
math.toHTML(span,div,this.PHASE.I);
} catch (err) {
if (err.restart) {while (span.firstChild) {span.removeChild(span.firstChild)}}
this.restorePreview(script);
throw err;
}
this.restorePreview(script);
//
// Remove the processing marker, and signal the new math pending
//
div.className = div.className.split(/ /)[0] + " MathJax_Processed";
HUB.signal.Post(["New Math Pending",jax.inputID]); // FIXME: wait for this? (i.e., restart if returns uncalled callback)
//
// Check if we should show this chunk of equations
//
state.HTMLCSSeqn += (state.i - state.HTMLCSSi); state.HTMLCSSi = state.i;
if (state.HTMLCSSeqn >= state.HTMLCSSlast + state.HTMLCSSchunk) {
this.postTranslate(state,true);
state.HTMLCSSchunk = Math.floor(state.HTMLCSSchunk*this.config.EqnChunkFactor);
state.HTMLCSSdelay = true; // delay if there are more scripts
}
return false;
},
//
// MathML previews can contain the same ID's as the HTML output,
// which confuses HTMLspanElement(), so remove the preview temporarily
// and restore it after typesetting the math.
//
savePreview: function (script) {
var preview = script.MathJax.preview;
if (preview) {
script.MathJax.tmpPreview = document.createElement("span");
preview.parentNode.replaceChild(script.MathJax.tmpPreview,preview);
}
},
restorePreview: function (script) {
var tmpPreview = script.MathJax.tmpPreview;
if (tmpPreview) {
tmpPreview.parentNode.replaceChild(script.MathJax.preview,tmpPreview);
delete script.MathJax.tmpPreview;
}
},
//
// Get the jax metric information
//
getMetrics: function(jax) {
var data = jax.HTMLCSS;
this.em = MML.mbase.prototype.em = data.em * data.scale;
this.outerEm = data.em;
this.scale = data.scale;
this.cwidth = data.cwidth;
this.linebreakWidth = data.lineWidth;
},
postTranslate: function (state,partial) {
var scripts = state.jax[this.id], script, jax, i, m;
//
// Remove the processed markers so that measuring can occur,
// and remove the preview, if any, since the math will now be visible.
//
for (i = state.HTMLCSSlast, m = state.HTMLCSSeqn; i < m; i++) {
script = scripts[i];
if (script && script.parentNode && script.MathJax.elementJax) {
var div = (script.MathJax.elementJax.HTMLCSS||{}).div;
if (div) {div.className = div.className.split(/ /)[0]}
if (script.MathJax.preview) script.MathJax.preview.innerHTML = "";
}
}
//
// Measure the math in this chunk (toHTML phase II)
//
for (i = state.HTMLCSSlast, m = state.HTMLCSSeqn; i < m; i++) {
script = scripts[i];
if (script && script.parentNode && script.MathJax.elementJax) {
jax = script.MathJax.elementJax; this.getMetrics(jax);
if (jax.HTMLCSS.span && jax.HTMLCSS.div)
jax.root.toHTML(jax.HTMLCSS.span,jax.HTMLCSS.div,this.PHASE.II);
}
}
//
// Reveal this chunk of math
//
for (i = state.HTMLCSSlast, m = state.HTMLCSSeqn; i < m; i++) {
script = scripts[i];
if (script && script.parentNode && script.MathJax.elementJax) {
//
// Finish the math with its measured size (toHTML phase III)
//
jax = script.MathJax.elementJax; this.getMetrics(jax);
if (jax.HTMLCSS.span && jax.HTMLCSS.div) {
jax.root.toHTML(jax.HTMLCSS.span,jax.HTMLCSS.div,this.PHASE.III);
if (jax.HTMLCSS.isHidden) script.parentNode.insertBefore(jax.HTMLCSS.div,script);
delete jax.HTMLCSS.span; delete jax.HTMLCSS.div;
//
// The math is now fully processed
//
script.MathJax.state = jax.STATE.PROCESSED;
HUB.signal.Post(["New Math",script.MathJax.elementJax.inputID]); // FIXME: wait for this? (i.e., restart if returns uncalled callback)
}
}
}
if (this.forceReflow) {
// WebKit can misplace some elements that should wrap to the next line
// but gets them right on a reflow, so force reflow by toggling a stylesheet
var sheet = (document.styleSheets||[])[0]||{};
sheet.disabled = true; sheet.disabled = false;
}
//
// Save our place so we know what is revealed
//
state.HTMLCSSlast = state.HTMLCSSeqn;
},
getJaxFromMath: function (math) {
if (math.parentNode.className.match(/MathJax_Display/)) {math = math.parentNode}
do {math = math.nextSibling} while (math && math.nodeName.toLowerCase() !== "script");
return HUB.getJaxFor(math);
},
getHoverSpan: function (jax,math) {return jax.root.HTMLspanElement()},
getHoverBBox: function (jax,span,math) {
var bbox = span.bbox, em = jax.HTMLCSS.outerEm;
var BBOX = {w:bbox.w*em, h:bbox.h*em, d:bbox.d*em};
if (bbox.width) {BBOX.width = bbox.width}
return BBOX;
},
Zoom: function (jax,span,math,Mw,Mh) {
//
// Re-render at larger size
//
span.className = "MathJax";
span.style.fontSize = jax.HTMLCSS.fontSize;
//
// get em sizes (taken from HTMLCSS.preTranslate)
//
var emex = span.appendChild(this.TestSpan.cloneNode(true));
var em = emex.childNodes[2].offsetHeight/60;
this.em = MML.mbase.prototype.em = em;
this.outerEm = em / jax.HTMLCSS.scale;
emex.parentNode.removeChild(emex);
this.scale = jax.HTMLCSS.scale;
this.linebreakWidth = jax.HTMLCSS.lineWidth;
this.cwidth = jax.HTMLCSS.cwidth;
this.zoomScale = parseInt(HUB.config.menuSettings.zscale) / 100;
this.idPostfix = "-zoom"; jax.root.toHTML(span,span); this.idPostfix = "";
this.zoomScale = 1;
var bbox = jax.root.HTMLspanElement().bbox, width = bbox.width;
if (width) {
// Handle full-width displayed equations
if (bbox.tw) {Mw = bbox.tw*em}
if (bbox.w*em < Mw) {Mw = bbox.w*em}
span.style.width = Math.floor(Mw-1.5*HTMLCSS.em)+"px"; span.style.display="inline-block";
var id = (jax.root.id||"MathJax-Span-"+jax.root.spanID)+"-zoom";
var child = document.getElementById(id).firstChild;
while (child && child.style.width !== width) {child = child.nextSibling}
if (child) {
var cwidth = child.offsetWidth; child.style.width = "100%";
if (cwidth > Mw) {span.style.width = (cwidth+100)+"px"}
}
}
//
// Adjust margins to prevent overlaps at the edges
//
child = span.firstChild.firstChild.style;
if (bbox.H != null && bbox.H > bbox.h)
{child.marginTop = HTMLCSS.Em(bbox.H-Math.max(bbox.h,HTMLCSS.FONTDATA.lineH))}
if (bbox.D != null && bbox.D > bbox.d)
{child.marginBottom = HTMLCSS.Em(bbox.D-Math.max(bbox.d,HTMLCSS.FONTDATA.lineD))}
if (bbox.lw < 0) {child.paddingLeft = HTMLCSS.Em(-bbox.lw)}
if (bbox.rw > bbox.w) {child.marginRight = HTMLCSS.Em(bbox.rw-bbox.w)}
//
// Get height and width of zoomed math and original math
//
span.style.position = "absolute";
if (!width) {math.style.position = "absolute"}
var zW = span.offsetWidth, zH = span.offsetHeight,
mH = math.offsetHeight, mW = math.offsetWidth;
span.style.position = math.style.position = "";
//
return {Y:-EVENT.getBBox(span).h, mW:mW, mH:mH, zW:zW, zH:zH};
},
initImg: function (span) {},
initHTML: function (math,span) {},
initFont: function (name) {
var FONTS = HTMLCSS.FONTDATA.FONTS, AVAIL = HTMLCSS.config.availableFonts;
if (AVAIL && AVAIL.length && HTMLCSS.Font.testFont(FONTS[name])) {
FONTS[name].available = true;
if (FONTS[name].familyFixed) {
FONTS[name].family = FONTS[name].familyFixed;
delete FONTS[name].familyFixed;
}
return null;
}
if (!this.allowWebFonts) {return null}
FONTS[name].isWebFont = true;
if (HTMLCSS.FontFaceBug) {
FONTS[name].family = name;
if (HTMLCSS.msieFontCSSBug) {FONTS[name].family += "-Web"}
}
return AJAX.Styles({"@font-face":this.Font.fontFace(name)});
},
Remove: function (jax) {
var span = document.getElementById(jax.inputID+"-Frame");
if (span) {
if (jax.HTMLCSS.display) {span = span.parentNode}
span.parentNode.removeChild(span);
}
delete jax.HTMLCSS;
},
getHD: function (span,force) {
if (span.bbox && this.config.noReflows && !force) {return {h:span.bbox.h, d:span.bbox.d}}
var position = span.style.position;
span.style.position = "absolute";
this.HDimg.style.height = "0px";
span.appendChild(this.HDspan);
var HD = {h:span.offsetHeight};
this.HDimg.style.height = HD.h+"px";
HD.d = span.offsetHeight - HD.h; HD.h -= HD.d;
HD.h /= this.em; HD.d /= this.em;
span.removeChild(this.HDspan);
span.style.position = position;
return HD;
},
getW: function (span) {
var W, H, w = (span.bbox||{}).w, start = span;
if (span.bbox && this.config.noReflows && span.bbox.exactW !== false) {
if (!span.bbox.exactW) {
if (span.style.paddingLeft) w += this.unEm(span.style.paddingLeft)*(span.scale||1);
if (span.style.paddingRight) w += this.unEm(span.style.paddingRight)*(span.scale||1);
}
return w;
}
if (span.bbox && span.bbox.exactW) {return w}
if ((span.bbox && w >= 0 && !this.initialSkipBug && !this.msieItalicWidthBug) ||
this.negativeBBoxes || !span.firstChild) {
W = span.offsetWidth; H = span.parentNode.offsetHeight;
} else if (span.bbox && w < 0 && this.msieNegativeBBoxBug) {
W = -span.offsetWidth, H = span.parentNode.offsetHeight;
} else {
// IE can't deal with a space at the beginning, so put something else first
var position = span.style.position; span.style.position = "absolute";
start = this.startMarker; span.insertBefore(start,span.firstChild)
span.appendChild(this.endMarker);
W = this.endMarker.offsetLeft - start.offsetLeft;
span.removeChild(this.endMarker);
span.removeChild(start); span.style.position = position
}
if (H != null) {span.parentNode.HH = H/this.em}
return W/this.em;
},
Measured: function (span,parent) {
var bbox = span.bbox;
if (bbox.width == null && bbox.w && !bbox.isMultiline) {
var w = this.getW(span);
bbox.rw += w - bbox.w;
bbox.w = w; bbox.exactW = true;
}
if (!parent) {parent = span.parentNode}
if (!parent.bbox) {parent.bbox = bbox}
return span;
},
Remeasured: function (span,parent) {
parent.bbox = this.Measured(span,parent).bbox;
},
MeasureSpans: function (SPANS) {
var spans = [], span, i, m, bbox, start, end, W, parent;
//
// Insert the needed markers
//
for (i = 0, m = SPANS.length; i < m; i++) {
span = SPANS[i]; if (!span) continue;
bbox = span.bbox; parent = this.parentNode(span);
if (bbox.exactW || bbox.width || bbox.w === 0 || bbox.isMultiline ||
(this.config.noReflows && bbox.exactW !== false)) {
if (!parent.bbox) {parent.bbox = bbox}
continue;
}
if (this.negativeBBoxes || !span.firstChild || (bbox.w >= 0 && !this.initialSkipBug) ||
(bbox.w < 0 && this.msieNegativeBBoxBug)) {
spans.push([span]);
} else if (this.initialSkipBug) {
start = this.startMarker.cloneNode(true); end = this.endMarker.cloneNode(true);
span.insertBefore(start,span.firstChild); span.appendChild(end);
spans.push([span,start,end,span.style.position]); span.style.position = "absolute";
} else {
end = this.endMarker.cloneNode(true);
span.appendChild(end); spans.push([span,null,end]);
}
}
//
// Read the widths and heights
//
for (i = 0, m = spans.length; i < m; i++) {
span = spans[i][0]; bbox = span.bbox; parent = this.parentNode(span);
if ((bbox.w >= 0 && !this.initialSkipBug) || this.negativeBBoxes || !span.firstChild) {
W = span.offsetWidth; parent.HH = parent.offsetHeight/this.em;
} else if (bbox.w < 0 && this.msieNegativeBBoxBug) {
W = -span.offsetWidth, parent.HH = parent.offsetHeight/this.em;
} else {
W = spans[i][2].offsetLeft - ((spans[i][1]||{}).offsetLeft||0);
}
W /= this.em;
bbox.rw += W - bbox.w;
bbox.w = W; bbox.exactW = true;
if (!parent.bbox) {parent.bbox = bbox}
}
//
// Remove markers
//
for (i = 0, m = spans.length; i < m; i++) {
span = spans[i];
if (span[1]) {span[1].parentNode.removeChild(span[1]), span[0].style.position = span[3]}
if (span[2]) {span[2].parentNode.removeChild(span[2])}
}
},
Em: function (m) {
if (Math.abs(m) < .0006) {return "0em"}
return m.toFixed(3).replace(/\.?0+$/,"") + "em";
},
EmRounded: function (m) {
if (Math.abs(m) < .0006) {return "0em"}
m = (Math.round(m*HTMLCSS.em)+.05)/HTMLCSS.em;
return m.toFixed(3).replace(/\.?0+$/,"") + "em";
},
unEm: function (m) {
return parseFloat(m);
},
Px: function (m) {
m *= this.em; var s = (m < 0? "-" : "");
return s+Math.abs(m).toFixed(1).replace(/\.?0+$/,"") + "px";
},
unPx: function (m) {
return parseFloat(m)/this.em;
},
Percent: function (m) {
return (100*m).toFixed(1).replace(/\.?0+$/,"") + "%";
},
length2em: function (length,mu,size) {
if (typeof(length) !== "string") {length = length.toString()}
if (length === "") {return ""}
if (length === MML.SIZE.NORMAL) {return 1}
if (length === MML.SIZE.BIG) {return 2}
if (length === MML.SIZE.SMALL) {return .71}
if (length === "infinity") {return HTMLCSS.BIGDIMEN}
var factor = this.FONTDATA.TeX_factor, emFactor = (HTMLCSS.zoomScale||1) / HTMLCSS.em;
if (length.match(/mathspace$/)) {return HTMLCSS.MATHSPACE[length]*factor}
var match = length.match(/^\s*([-+]?(?:\.\d+|\d+(?:\.\d*)?))?(pt|em|ex|mu|px|pc|in|mm|cm|%)?/);
var m = parseFloat(match[1]||"1"), unit = match[2];
if (size == null) {size = 1}; if (mu == null) {mu = 1}
if (unit === "em") {return m * factor}
if (unit === "ex") {return m * HTMLCSS.TeX.x_height * factor}
if (unit === "%") {return m / 100 * size}
if (unit === "px") {return m * emFactor}
if (unit === "pt") {return m / 10 * factor} // 10 pt to an em
if (unit === "pc") {return m * 1.2 * factor} // 12 pt to a pc
if (unit === "in") {return m * this.pxPerInch * emFactor}
if (unit === "cm") {return m * this.pxPerInch * emFactor / 2.54} // 2.54 cm to an inch
if (unit === "mm") {return m * this.pxPerInch * emFactor / 25.4} // 10 mm to a cm
if (unit === "mu") {return m / 18 * factor * mu} // 18mu to an em for the scriptlevel
return m*size; // relative to given size (or 1em as default)
},
thickness2em: function (length,mu) {
var thick = HTMLCSS.TeX.rule_thickness;
if (length === MML.LINETHICKNESS.MEDIUM) {return thick}
if (length === MML.LINETHICKNESS.THIN) {return .67*thick}
if (length === MML.LINETHICKNESS.THICK) {return 1.67*thick}
return this.length2em(length,mu,thick);
},
border2em: function (length,mu) {
if (length === MML.LINETHICKNESS.THIN) {length = "1px"}
if (length === MML.LINETHICKNESS.MEDIUM) {length = "3px"}
if (length === MML.LINETHICKNESS.THICK) {length = "5px"}
return this.length2em(length,mu);
},
getPadding: function (span) {
var padding = {top:0, right:0, bottom:0, left:0}, has = false;
for (var id in padding) {if (padding.hasOwnProperty(id)) {
var pad = span.style["padding"+id.charAt(0).toUpperCase()+id.substr(1)];
if (pad) {padding[id] = this.length2em(pad); has = true;}
}}
return (has ? padding : false);
},
getBorders: function (span) {
var border = {top:0, right:0, bottom:0, left:0}, css = {}, has = false;
for (var id in border) {if (border.hasOwnProperty(id)) {
var ID = "border"+id.charAt(0).toUpperCase()+id.substr(1);
var style = span.style[ID+"Style"];
if (style) {
has = true;
border[id] = this.border2em(span.style[ID+"Width"] || MML.LINETHICKNESS.MEDIUM);
css[ID] = [span.style[ID+"Width"],span.style[ID+"Style"],span.style[ID+"Color"]].join(" ");
}
}}
border.css = css;
return (has ? border : false);
},
setBorders: function (span,borders) {
if (borders) {
for (var id in borders.css) {if (borders.css.hasOwnProperty(id)) {
span.style[id] = borders.css[id];
}}
}
},
createStrut: function (span,h,before) {
var strut = this.Element("span",{
isMathJax: true,
style:{display:"inline-block", overflow:"hidden", height:h+"px",
width:"1px", marginRight:"-1px"}
});
if (before) {span.insertBefore(strut,span.firstChild)} else {span.appendChild(strut)}
return strut;
},
createBlank: function (span,w,before) {
var blank = this.Element("span",{
isMathJax: true,
style: {display:"inline-block", overflow:"hidden", height:"1px", width:this.Em(w)}
});
if (w < 0) {blank.style.marginRight = blank.style.width; blank.style.width = 0}
if (before) {span.insertBefore(blank,span.firstChild)} else {span.appendChild(blank)}
return blank;
},
createShift: function (span,w,before) {
var space = this.Element("span",{style:{marginLeft:this.Em(w)}, isMathJax:true});
if (before) {span.insertBefore(space,span.firstChild)} else {span.appendChild(space)}
return space;
},
createSpace: function (span,h,d,w,color,isSpace) {
if (h < -d) {d = -h} // make sure h is above d
var H = this.Em(h+d), D = this.Em(-d);
if (this.msieInlineBlockAlignBug) {D = this.Em(HTMLCSS.getHD(span.parentNode,true).d-d)}
if (span.isBox || isSpace) {
var scale = (span.scale == null ? 1 : span.scale);
span.bbox = {exactW: true, h: h*scale, d: d*scale, w: w*scale, rw: w*scale, lw: 0};
span.style.height = H; span.style.verticalAlign = D;
span.HH = (h+d)*scale;
} else {
span = this.addElement(span,"span",{style: {height:H, verticalAlign:D}, isMathJax:true});
}
if (w >= 0) {
span.style.width = this.Em(w);
span.style.display = "inline-block";
span.style.overflow = "hidden"; // for IE in quirks mode
} else {
if (this.msieNegativeSpaceBug) {span.style.height = ""}
span.style.marginLeft = this.Em(w);
if (HTMLCSS.safariNegativeSpaceBug && span.parentNode.firstChild == span)
{this.createBlank(span,0,true)}
}
if (color && color !== MML.COLOR.TRANSPARENT) {
span.style.backgroundColor = color;
span.style.position = "relative"; // make sure it covers earlier items
}
return span;
},
createRule: function (span,h,d,w,color) {
if (h < -d) {d = -h} // make sure h is above d
var min = HTMLCSS.TeX.min_rule_thickness, f = 1;
// If rule is very thin, make it at least min_rule_thickness so it doesn't disappear
if (w > 0 && w*this.