Add looots of comments

Summary:
Add comments everywhere! Also fix some small bugs like using Style.id
instead of Style.size, and rename some variables to be more descriptive.

Fixes #22

Test Plan:
 - Make sure the huxley screenshots didn't change
 - Make sure the tests still pass

Reviewers: alpert

Reviewed By: alpert

Differential Revision: http://phabricator.khanacademy.org/D13158
This commit is contained in:
Emily Eisenberg 2014-09-14 19:23:39 -07:00
parent 79a5687057
commit f63af87f17
14 changed files with 955 additions and 297 deletions

View File

@ -1,3 +1,16 @@
/**
* The Lexer class handles tokenizing the input in various ways. Since our
* parser expects us to be able to backtrack, the lexer allows lexing from any
* given starting point.
*
* Its main exposed function is the `lex` function, which takes a position to
* lex from and a type of token to lex. It defers to the appropriate `_innerLex`
* function.
*
* The various `_innerLex` functions perform the actual lexing of different
* kinds.
*/
var ParseError = require("./ParseError");
// The main lexer class
@ -5,14 +18,15 @@ function Lexer(input) {
this._input = input;
};
// The result of a single lex
// The resulting token returned from `lex`.
function LexResult(type, text, position) {
this.type = type;
this.text = text;
this.position = position;
}
// "normal" types of tokens
// "normal" types of tokens. These are tokens which can be matched by a simple
// regex, and have a type which is listed.
var mathNormals = [
[/^[/|@."`0-9]/, "textord"],
[/^[a-zA-Z]/, "mathord"],
@ -29,6 +43,8 @@ var mathNormals = [
[/^~/, "spacing"]
];
// These are "normal" tokens like above, but should instead be parsed in text
// mode.
var textNormals = [
[/^[a-zA-Z0-9`!@*()-=+\[\]'";:?\/.,]/, "textord"],
[/^{/, "{"],
@ -36,22 +52,29 @@ var textNormals = [
[/^~/, "spacing"]
];
// Regexes for matching whitespace
var whitespaceRegex = /^\s*/;
var whitespaceConcatRegex = /^( +|\\ +)/;
// Build a regex to easily parse the functions
// This regex matches any other TeX function, which is a backslash followed by a
// word or a single symbol
var anyFunc = /^\\(?:[a-zA-Z]+|.)/;
/**
* This function lexes a single normal token. It takes a position, a list of
* "normal" tokens to try, and whether it should completely ignore whitespace or
* not.
*/
Lexer.prototype._innerLex = function(pos, normals, ignoreWhitespace) {
var input = this._input.slice(pos);
// Get rid of whitespace
if (ignoreWhitespace) {
// Get rid of whitespace.
var whitespace = input.match(whitespaceRegex)[0];
pos += whitespace.length;
input = input.slice(whitespace.length);
} else {
// Do the funky concatenation of whitespace
// Do the funky concatenation of whitespace that happens in text mode.
var whitespace = input.match(whitespaceConcatRegex);
if (whitespace !== null) {
return new LexResult(" ", " ", pos + whitespace[0].length);
@ -65,7 +88,7 @@ Lexer.prototype._innerLex = function(pos, normals, ignoreWhitespace) {
var match;
if ((match = input.match(anyFunc))) {
// If we match one of the tokens, extract the type
// If we match a function token, return it
return new LexResult(match[0], match[0], pos + match[0].length);
} else {
// Otherwise, we look through the normal token regexes and see if it's
@ -81,7 +104,6 @@ Lexer.prototype._innerLex = function(pos, normals, ignoreWhitespace) {
}
}
// We didn't match any of the tokens, so throw an error.
throw new ParseError("Unexpected character: '" + input[0] +
"'", this, pos);
}
@ -89,6 +111,9 @@ Lexer.prototype._innerLex = function(pos, normals, ignoreWhitespace) {
// A regex to match a CSS color (like #ffffff or BlueViolet)
var cssColor = /^(#[a-z0-9]+|[a-z]+)/i;
/**
* This function lexes a CSS color.
*/
Lexer.prototype._innerLexColor = function(pos) {
var input = this._input.slice(pos);
@ -101,14 +126,18 @@ Lexer.prototype._innerLexColor = function(pos) {
if ((match = input.match(cssColor))) {
// If we look like a color, return a color
return new LexResult("color", match[0], pos + match[0].length);
} else {
throw new ParseError("Invalid color", this, pos);
}
// We didn't match a color, so throw an error.
throw new ParseError("Invalid color", this, pos);
};
// A regex to match a dimension. Dimensions look like
// "1.2em" or ".4pt" or "1 ex"
var sizeRegex = /^(\d+(?:\.\d*)?|\.\d+)\s*([a-z]{2})/;
/**
* This function lexes a dimension.
*/
Lexer.prototype._innerLexSize = function(pos) {
var input = this._input.slice(pos);
@ -120,6 +149,7 @@ Lexer.prototype._innerLexSize = function(pos) {
var match;
if ((match = input.match(sizeRegex))) {
var unit = match[2];
// We only currently handle "em" and "ex" units
if (unit !== "em" && unit !== "ex") {
throw new ParseError("Invalid unit: '" + unit + "'", this, pos);
}
@ -132,6 +162,9 @@ Lexer.prototype._innerLexSize = function(pos) {
throw new ParseError("Invalid size", this, pos);
};
/**
* This function lexes a string of whitespace.
*/
Lexer.prototype._innerLexWhitespace = function(pos) {
var input = this._input.slice(pos);
@ -141,7 +174,10 @@ Lexer.prototype._innerLexWhitespace = function(pos) {
return new LexResult("whitespace", whitespace, pos);
};
// Lex a single token
/**
* This function lexes a single token starting at `pos` and of the given mode.
* Based on the mode, we defer to one of the `_innerLex` functions.
*/
Lexer.prototype.lex = function(pos, mode) {
if (mode === "math") {
return this._innerLex(pos, mathNormals, true);

View File

@ -1,3 +1,19 @@
/**
* This file contains information about the options that the Parser carries
* around with it while parsing. Data is held in an `Options` object, and when
* recursing, a new `Options` object can be created with the `.with*` and
* `.reset` functions.
*/
/**
* This is the main options class. It contains the style, size, and color of the
* current parse level. It also contains the style and size of the parent parse
* level, so size changes can be handled efficiently.
*
* Each of the `.with*` and `.reset` functions passes its current style and size
* as the parentStyle and parentSize of the new options class, so parent
* handling is taken care of automatically.
*/
function Options(style, size, color, parentStyle, parentSize) {
this.style = style;
this.color = color;
@ -14,23 +30,40 @@ function Options(style, size, color, parentStyle, parentSize) {
this.parentSize = parentSize;
}
/**
* Create a new options object with the given style.
*/
Options.prototype.withStyle = function(style) {
return new Options(style, this.size, this.color, this.style, this.size);
};
/**
* Create a new options object with the given size.
*/
Options.prototype.withSize = function(size) {
return new Options(this.style, size, this.color, this.style, this.size);
};
/**
* Create a new options object with the given color.
*/
Options.prototype.withColor = function(color) {
return new Options(this.style, this.size, color, this.style, this.size);
};
/**
* Create a new options object with the same style, size, and color. This is
* used so that parent style and size changes are handled correctly.
*/
Options.prototype.reset = function() {
return new Options(
this.style, this.size, this.color, this.style, this.size);
};
/**
* A map of color names to CSS colors.
* TODO(emily): Remove this when we have real macros
*/
var colorMap = {
"katex-blue": "#6495ed",
"katex-orange": "#ffa500",
@ -41,6 +74,10 @@ var colorMap = {
"katex-purple": "#9d38bd"
};
/**
* Gets the CSS color of the current options object, accounting for the
* `colorMap`.
*/
Options.prototype.getColor = function() {
return colorMap[this.color] || this.color;
};

View File

@ -1,8 +1,14 @@
/**
* This is the ParseError class, which is the main error thrown by KaTeX
* functions when something has gone wrong. This is used to distinguish internal
* errors from errors in the expression that the user provided.
*/
function ParseError(message, lexer, position) {
var error = "KaTeX parse error: " + message;
if (lexer !== undefined && position !== undefined) {
// If we have the input and a position, make the error a bit fancier
// Prepend some information
error += " at position " + position + ": ";
@ -18,12 +24,15 @@ function ParseError(message, lexer, position) {
error += input.slice(begin, end);
}
// Some hackery to make ParseError a prototype of Error
// See http://stackoverflow.com/a/8460753
var self = new Error(error);
self.name = "ParseError";
self.__proto__ = ParseError.prototype;
return self;
}
// More hackery
ParseError.prototype.__proto__ = Error.prototype;
module.exports = ParseError;

183
Parser.js
View File

@ -5,60 +5,70 @@ var utils = require("./utils");
var ParseError = require("./ParseError");
// This file contains the parser used to parse out a TeX expression from the
// input. Since TeX isn't context-free, standard parsers don't work particularly
// well.
/**
* This file contains the parser used to parse out a TeX expression from the
* input. Since TeX isn't context-free, standard parsers don't work particularly
* well.
*
* The strategy of this parser is as such:
*
* The main functions (the `.parse...` ones) take a position in the current
* parse string to parse tokens from. The lexer (found in Lexer.js, stored at
* this.lexer) also supports pulling out tokens at arbitrary places. When
* individual tokens are needed at a position, the lexer is called to pull out a
* token, which is then used.
*
* The main functions also take a mode that the parser is currently in
* (currently "math" or "text"), which denotes whether the current environment
* is a math-y one or a text-y one (e.g. inside \text). Currently, this serves
* to limit the functions which can be used in text mode.
*
* The main functions then return an object which contains the useful data that
* was parsed at its given point, and a new position at the end of the parsed
* data. The main functions can call each other and continue the parsing by
* using the returned position as a new starting point.
*
* There are also extra `.handle...` functions, which pull out some reused
* functionality into self-contained functions.
*
* The earlier functions return `ParseResult`s, which contain a ParseNode and a
* new position.
*
* The later functions (which are called deeper in the parse) sometimes return
* ParseFuncOrArgument, which contain a ParseResult as well as some data about
* whether the parsed object is a function which is missing some arguments, or a
* standalone object which can be used as an argument to another function.
*/
// The strategy of this parser is as such:
//
// The main functions (the `.parse...` ones) take a position in the current
// parse string to parse tokens from. The lexer (found in Lexer.js, stored at
// this.lexer) also supports pulling out tokens at arbitrary places. When
// individual tokens are needed at a position, the lexer is called to pull out a
// token, which is then used.
//
// The main functions also take a mode that the parser is currently in
// (currently "math" or "text"), which denotes whether the current environment
// is a math-y one or a text-y one (e.g. inside \text). Currently, this serves
// to limit the functions which can be used in text mode.
//
// The main functions then return an object which contains the useful data that
// was parsed at its given point, and a new position at the end of the parsed
// data. The main functions can call each other and continue the parsing by
// using the returned position as a new starting point.
//
// There are also extra `.handle...` functions, which pull out some reused
// functionality into self-contained functions.
//
// The earlier functions return `ParseResult`s, which contain a ParseNode and a
// new position.
//
// The later functions (which are called deeper in the parse) sometimes return
// ParseFuncOrArgument, which contain a ParseResult as well as some data about
// whether the parsed object is a function which is missing some arguments, or a
// standalone object which can be used as an argument to another function.
// Main Parser class
/**
* Main Parser class
*/
function Parser(input) {
// Make a new lexer
this.lexer = new Lexer(input);
};
// The resulting parse tree nodes of the parse tree.
/**
* The resulting parse tree nodes of the parse tree.
*/
function ParseNode(type, value, mode) {
this.type = type;
this.value = value;
this.mode = mode;
}
// A result and final position returned by the `.parse...` functions.
/**
* A result and final position returned by the `.parse...` functions.
*/
function ParseResult(result, newPosition) {
this.result = result;
this.position = newPosition;
}
// An initial function (without its arguments), or an argument to a function.
// The `result` argument should be a ParseResult.
/**
* An initial function (without its arguments), or an argument to a function.
* The `result` argument should be a ParseResult.
*/
function ParseFuncOrArgument(result, isFunction, allowedInText, numArgs, argTypes) {
this.result = result;
// Is this a function (i.e. is it something defined in functions.js)?
@ -71,8 +81,10 @@ function ParseFuncOrArgument(result, isFunction, allowedInText, numArgs, argType
this.argTypes = argTypes;
}
// Checks a result to make sure it has the right type, and throws an
// appropriate error otherwise.
/**
* Checks a result to make sure it has the right type, and throws an
* appropriate error otherwise.
*/
Parser.prototype.expect = function(result, type) {
if (result.type !== type) {
throw new ParseError(
@ -82,15 +94,20 @@ Parser.prototype.expect = function(result, type) {
}
};
// Main parsing function, which parses an entire input. Returns either a list
// of parseNodes or null if the parse fails.
/**
* Main parsing function, which parses an entire input.
*
* @return {?Array.<ParseNode>}
*/
Parser.prototype.parse = function(input) {
// Try to parse the input
var parse = this.parseInput(0, "math");
return parse.result;
};
// Parses an entire input tree
/**
* Parses an entire input tree.
*/
Parser.prototype.parseInput = function(pos, mode) {
// Parse an expression
var expression = this.parseExpression(pos, mode);
@ -100,7 +117,9 @@ Parser.prototype.parseInput = function(pos, mode) {
return expression;
};
// Handles a body of an expression
/**
* Handles a body of an expression.
*/
Parser.prototype.handleExpressionBody = function(pos, mode) {
var body = [];
var atom;
@ -116,9 +135,11 @@ Parser.prototype.handleExpressionBody = function(pos, mode) {
};
};
// Parses an "expression", which is a list of atoms
//
// Returns ParseResult
/**
* Parses an "expression", which is a list of atoms.
*
* @return {ParseResult}
*/
Parser.prototype.parseExpression = function(pos, mode) {
var body = this.handleExpressionBody(pos, mode);
return new ParseResult(body.body, body.position);
@ -127,7 +148,9 @@ Parser.prototype.parseExpression = function(pos, mode) {
// The greediness of a superscript or subscript
var SUPSUB_GREEDINESS = 1;
// Handle a subscript or superscript with nice errors
/**
* Handle a subscript or superscript with nice errors.
*/
Parser.prototype.handleSupSubscript = function(pos, mode, symbol, name) {
var group = this.parseGroup(pos, mode);
@ -151,9 +174,11 @@ Parser.prototype.handleSupSubscript = function(pos, mode, symbol, name) {
}
};
// Parses a group with optional super/subscripts
//
// Returns ParseResult or null
/**
* Parses a group with optional super/subscripts.
*
* @return {?ParseResult}
*/
Parser.prototype.parseAtom = function(pos, mode) {
// The body of an atom is an implicit group, so that things like
// \left(x\right)^2 work correctly.
@ -247,15 +272,17 @@ var styleFuncs = [
"\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle"
];
// Parses an implicit group, which is a group that starts at the end of a
// specified, and ends right before a higher explicit group ends, or at EOL. It
// is used for functions that appear to affect the current style, like \Large or
// \textrm, where instead of keeping a style we just pretend that there is an
// implicit grouping after it until the end of the group. E.g.
// small text {\Large large text} small text again
// It is also used for \left and \right to get the correct grouping.
//
// Returns ParseResult or null
/**
* Parses an implicit group, which is a group that starts at the end of a
* specified, and ends right before a higher explicit group ends, or at EOL. It
* is used for functions that appear to affect the current style, like \Large or
* \textrm, where instead of keeping a style we just pretend that there is an
* implicit grouping after it until the end of the group. E.g.
* small text {\Large large text} small text again
* It is also used for \left and \right to get the correct grouping.
*
* @return {?ParseResult}
*/
Parser.prototype.parseImplicitGroup = function(pos, mode) {
var start = this.parseSymbol(pos, mode);
@ -320,9 +347,11 @@ Parser.prototype.parseImplicitGroup = function(pos, mode) {
}
};
// Parses an entire function, including its base and all of its arguments
//
// Returns ParseResult or null
/**
* Parses an entire function, including its base and all of its arguments
*
* @return {?ParseResult}
*/
Parser.prototype.parseFunction = function(pos, mode) {
var baseGroup = this.parseGroup(pos, mode);
@ -392,10 +421,12 @@ Parser.prototype.parseFunction = function(pos, mode) {
}
};
// Parses a group when the mode is changing. Takes a position, a new mode, and
// an outer mode that is used to parse the outside.
//
// Returns a ParseFuncOrArgument or null
/**
* Parses a group when the mode is changing. Takes a position, a new mode, and
* an outer mode that is used to parse the outside.
*
* @return {?ParseFuncOrArgument}
*/
Parser.prototype.parseSpecialGroup = function(pos, mode, outerMode) {
if (mode === "color" || mode === "size") {
// color and size modes are special because they should have braces and
@ -420,10 +451,12 @@ Parser.prototype.parseSpecialGroup = function(pos, mode, outerMode) {
}
};
// Parses a group, which is either a single nucleus (like "x") or an expression
// in braces (like "{x+y}")
//
// Returns a ParseFuncOrArgument or null
/**
* Parses a group, which is either a single nucleus (like "x") or an expression
* in braces (like "{x+y}")
*
* @return {?ParseFuncOrArgument}
*/
Parser.prototype.parseGroup = function(pos, mode) {
var start = this.lexer.lex(pos, mode);
// Try to parse an open brace
@ -444,10 +477,12 @@ Parser.prototype.parseGroup = function(pos, mode) {
}
};
// Parse a single symbol out of the string. Here, we handle both the functions
// we have defined, as well as the single character symbols
//
// Returns a ParseFuncOrArgument or null
/**
* Parse a single symbol out of the string. Here, we handle both the functions
* we have defined, as well as the single character symbols
*
* @return {?ParseFuncOrArgument}
*/
Parser.prototype.parseSymbol = function(pos, mode) {
var nucleus = this.lexer.lex(pos, mode);

View File

@ -1,3 +1,17 @@
/**
* This file contains information and classes for the various kinds of styles
* used in TeX. It provides a generic `Style` class, which holds information
* about a specific style. It then provides instances of all the different kinds
* of styles possible, and provides functions to move between them and get
* information about them.
*/
/**
* The main style class. Contains a unique id for the style, a size (which is
* the same for cramped and uncramped version of a style), a cramped flag, and a
* size multiplier, which gives the size difference between a style and
* textstyle.
*/
function Style(id, size, multiplier, cramped) {
this.id = id;
this.size = size;
@ -5,36 +19,59 @@ function Style(id, size, multiplier, cramped) {
this.sizeMultiplier = multiplier;
}
/**
* Get the style of a superscript given a base in the current style.
*/
Style.prototype.sup = function() {
return styles[sup[this.id]];
};
/**
* Get the style of a subscript given a base in the current style.
*/
Style.prototype.sub = function() {
return styles[sub[this.id]];
};
/**
* Get the style of a fraction numerator given the fraction in the current
* style.
*/
Style.prototype.fracNum = function() {
return styles[fracNum[this.id]];
};
/**
* Get the style of a fraction denominator given the fraction in the current
* style.
*/
Style.prototype.fracDen = function() {
return styles[fracDen[this.id]];
};
/**
* Get the cramped version of a style (in particular, cramping a cramped style
* doesn't change the style).
*/
Style.prototype.cramp = function() {
return styles[cramp[this.id]];
};
// HTML class name, like "displaystyle cramped"
/**
* HTML class name, like "displaystyle cramped"
*/
Style.prototype.cls = function() {
return sizeNames[this.size] + (this.cramped ? " cramped" : " uncramped");
};
// HTML Reset class name, like "reset-textstyle"
/**
* HTML Reset class name, like "reset-textstyle"
*/
Style.prototype.reset = function() {
return resetNames[this.size];
};
// IDs of the different styles
var D = 0;
var Dc = 1;
var T = 2;
@ -44,6 +81,7 @@ var Sc = 5;
var SS = 6;
var SSc = 7;
// String names for the different sizes
var sizeNames = [
"displaystyle textstyle",
"textstyle",
@ -51,6 +89,7 @@ var sizeNames = [
"scriptscriptstyle"
];
// Reset names for the different sizes
var resetNames = [
"reset-textstyle",
"reset-textstyle",
@ -58,6 +97,7 @@ var resetNames = [
"reset-scriptscriptstyle",
];
// Instances of the different styles
var styles = [
new Style(D, 0, 1.0, false),
new Style(Dc, 0, 1.0, true),
@ -69,12 +109,15 @@ var styles = [
new Style(SSc, 3, 0.5, true)
];
// Lookup tables for switching from one style to another
var sup = [S, Sc, S, Sc, SS, SSc, SS, SSc];
var sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc];
var fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc];
var fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc];
var cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc];
// We only export some of the styles. Also, we don't export the `Style` class so
// no more styles can be generated.
module.exports = {
DISPLAY: styles[D],
TEXT: styles[T],

View File

@ -1,8 +1,19 @@
/**
* This module contains general functions that can be used for building
* different kinds of domTree nodes in a consistent manner.
*/
var domTree = require("./domTree");
var fontMetrics = require("./fontMetrics");
var symbols = require("./symbols");
/**
* Makes a symbolNode after translation via the list of symbols in symbols.js.
* Correctly pulls out metrics for the character, and optionally takes a list of
* classes to be attached to the node.
*/
var makeSymbol = function(value, style, mode, color, classes) {
// Replace the value with its replaced value from symbol.js
if (symbols[mode][value] && symbols[mode][value].replace) {
value = symbols[mode][value].replace;
}
@ -15,8 +26,10 @@ var makeSymbol = function(value, style, mode, color, classes) {
value, metrics.height, metrics.depth, metrics.italic, metrics.skew,
classes);
} else {
console && console.warn("No character metrics for '" + value +
"' in style '" + style + "'");
// TODO(emily): Figure out a good way to only print this in development
typeof console !== "undefined" && console.warn(
"No character metrics for '" + value + "' in style '" +
style + "'");
symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes);
}
@ -27,12 +40,20 @@ var makeSymbol = function(value, style, mode, color, classes) {
return symbolNode;
};
/**
* Makes a symbol in the italic math font.
*/
var mathit = function(value, mode, color, classes) {
return makeSymbol(
value, "Math-Italic", mode, color, classes.concat(["mathit"]));
};
/**
* Makes a symbol in the upright roman font.
*/
var mathrm = function(value, mode, color, classes) {
// Decide what font to render the symbol in by its entry in the symbols
// table.
if (symbols[mode][value].font === "main") {
return makeSymbol(value, "Main-Regular", mode, color, classes);
} else {
@ -41,6 +62,10 @@ var mathrm = function(value, mode, color, classes) {
}
};
/**
* Calculate the height, depth, and maxFontSize of an element based on its
* children.
*/
var sizeElementFromChildren = function(elem) {
var height = 0;
var depth = 0;
@ -65,6 +90,9 @@ var sizeElementFromChildren = function(elem) {
elem.maxFontSize = maxFontSize;
};
/**
* Makes a span with the given list of classes, list of children, and color.
*/
var makeSpan = function(classes, children, color) {
var span = new domTree.span(classes, children);
@ -77,6 +105,9 @@ var makeSpan = function(classes, children, color) {
return span;
};
/**
* Makes a document fragment with the given list of children.
*/
var makeFragment = function(children) {
var fragment = new domTree.documentFragment(children);
@ -85,6 +116,11 @@ var makeFragment = function(children) {
return fragment;
};
/**
* Makes an element placed in each of the vlist elements to ensure that each
* element has the same max font size. To do this, we create a zero-width space
* with the correct font size.
*/
var makeFontSizer = function(options, fontSize) {
var fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]);
fontSizeInner.style.fontSize = (fontSize / options.style.sizeMultiplier) + "em";
@ -96,7 +132,7 @@ var makeFontSizer = function(options, fontSize) {
return fontSizer;
};
/*
/**
* Makes a vertical list by stacking elements and kerns on top of each other.
* Allows for many different ways of specifying the positioning method.
*
@ -229,6 +265,5 @@ module.exports = {
mathrm: mathrm,
makeSpan: makeSpan,
makeFragment: makeFragment,
makeFontSizer: makeFontSizer,
makeVList: makeVList
};

View File

@ -1,3 +1,10 @@
/**
* This file does the main work of building a domTree sturcture from a parse
* tree. The entry point is the `buildTree` function, which takes a parse tree.
* Then, the buildExpression, buildGroup, and various groupTypes functions are
* called, to produce a final tree.
*/
var Options = require("./Options");
var ParseError = require("./ParseError");
var Style = require("./Style");
@ -12,6 +19,11 @@ var utils = require("./utils");
var makeSpan = buildCommon.makeSpan;
/**
* Take a list of nodes, build them in order, and return a list of the built
* nodes. This function handles the `prev` node correctly, and passes the
* previous element from the list as the prev of the next element.
*/
var buildExpression = function(expression, options, prev) {
var groups = [];
for (var i = 0; i < expression.length; i++) {
@ -22,6 +34,7 @@ var buildExpression = function(expression, options, prev) {
return groups;
};
// List of types used by getTypeOfGroup
var groupToType = {
mathord: "mord",
textord: "mord",
@ -44,6 +57,20 @@ var groupToType = {
accent: "mord"
};
/**
* Gets the final math type of an expression, given its group type. This type is
* used to determine spacing between elements, and affects bin elements by
* causing them to change depending on what types are around them. This type
* must be attached to the outermost node of an element as a CSS class so that
* spacing with its surrounding elements works correctly.
*
* Some elements can be mapped one-to-one from group type to math type, and
* those are listed in the `groupToType` table.
*
* Others (usually elements that wrap around other elements) often have
* recursive definitions, and thus call `getTypeOfGroup` on their inner
* elements.
*/
var getTypeOfGroup = function(group) {
if (group == null) {
// Like when typesetting $^3$
@ -65,11 +92,19 @@ var getTypeOfGroup = function(group) {
}
};
/**
* Sometimes, groups perform special rules when they have superscripts or
* subscripts attached to them. This function lets the `supsub` group know that
* its inner element should handle the superscripts and subscripts instead of
* handling them itself.
*/
var shouldHandleSupSub = function(group, options) {
if (group == null) {
return false;
} else if (group.type === "op") {
return group.value.limits && options.style.id === Style.DISPLAY.id;
// Operators handle supsubs differently when they have limits
// (e.g. `\displaystyle\sum_2^3`)
return group.value.limits && options.style.size === Style.DISPLAY.size;
} else if (group.type === "accent") {
return isCharacterBox(group.value.base);
} else {
@ -77,6 +112,11 @@ var shouldHandleSupSub = function(group, options) {
}
};
/**
* Sometimes we want to pull out the innermost element of a group. In most
* cases, this will just be the group itself, but when ordgroups and colors have
* a single element, we want to pull that out.
*/
var getBaseElem = function(group) {
if (group == null) {
return false;
@ -97,18 +137,29 @@ var getBaseElem = function(group) {
}
};
/**
* TeXbook algorithms often reference "character boxes", which are simply groups
* with a single character in them. To decide if something is a character box,
* we find its innermost group, and see if it is a single character.
*/
var isCharacterBox = function(group) {
var baseElem = getBaseElem(group);
// These are all they types of groups which hold single characters
return baseElem.type === "mathord" ||
baseElem.type === "textord" ||
baseElem.type === "bin" ||
baseElem.type === "rel" ||
baseElem.type === "inner" ||
baseElem.type === "open" ||
baseElem.type === "close" ||
baseElem.type === "punct";
};
/**
* This is a map of group types to the function used to handle that type.
* Simpler types come at the beginning, while complicated types come afterwards.
*/
var groupTypes = {
mathord: function(group, options, prev) {
return buildCommon.mathit(
@ -122,11 +173,17 @@ var groupTypes = {
bin: function(group, options, prev) {
var className = "mbin";
// Pull out the most recent element. Do some special handling to find
// things at the end of a \color group. Note that we don't use the same
// logic for ordgroups (which count as ords).
var prevAtom = prev;
while (prevAtom && prevAtom.type == "color") {
var atoms = prevAtom.value.value;
prevAtom = atoms[atoms.length - 1];
}
// See TeXbook pg. 442-446, Rules 5 and 6, and the text before Rule 19.
// Here, we determine whether the bin should turn into an ord. We
// currently only apply Rule 5.
if (!prev || utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"],
getTypeOfGroup(prevAtom))) {
group.type = "textord";
@ -142,14 +199,59 @@ var groupTypes = {
group.value, group.mode, options.getColor(), ["mrel"]);
},
open: function(group, options, prev) {
return buildCommon.mathrm(
group.value, group.mode, options.getColor(), ["mopen"]);
},
close: function(group, options, prev) {
return buildCommon.mathrm(
group.value, group.mode, options.getColor(), ["mclose"]);
},
inner: function(group, options, prev) {
return buildCommon.mathrm(
group.value, group.mode, options.getColor(), ["minner"]);
},
punct: function(group, options, prev) {
return buildCommon.mathrm(
group.value, group.mode, options.getColor(), ["mpunct"]);
},
ordgroup: function(group, options, prev) {
return makeSpan(
["mord", options.style.cls()],
buildExpression(group.value, options.reset())
);
},
text: function(group, options, prev) {
return makeSpan(["text", "mord", options.style.cls()],
buildExpression(group.value.body, options.reset()));
},
color: function(group, options, prev) {
var elements = buildExpression(
group.value.value,
options.withColor(group.value.color),
prev
);
// \color isn't supposed to affect the type of the elements it contains.
// To accomplish this, we wrap the results in a fragment, so the inner
// elements will be able to directly interact with their neighbors. For
// example, `\color{red}{2 +} 3` has the same spacing as `2 + 3`
return new buildCommon.makeFragment(elements);
},
supsub: function(group, options, prev) {
// Superscript and subscripts are handled in the TeXbook on page
// 445-446, rules 18(a-f).
var baseGroup = group.value.base;
// Here is where we defer to the inner group if it should handle
// superscripts and subscripts itself.
if (shouldHandleSupSub(group.value.base, options)) {
return groupTypes[group.value.base.type](group, options, prev);
}
@ -170,77 +272,90 @@ var groupTypes = {
[options.style.reset(), options.style.sub().cls()], [sub]);
}
var u, v;
// Rule 18a
var supShift, subShift;
if (isCharacterBox(group.value.base)) {
u = 0;
v = 0;
supShift = 0;
subShift = 0;
} else {
u = base.height - fontMetrics.metrics.supDrop;
v = base.depth + fontMetrics.metrics.subDrop;
supShift = base.height - fontMetrics.metrics.supDrop;
subShift = base.depth + fontMetrics.metrics.subDrop;
}
var p;
// Rule 18c
var minSupShift;
if (options.style === Style.DISPLAY) {
p = fontMetrics.metrics.sup1;
minSupShift = fontMetrics.metrics.sup1;
} else if (options.style.cramped) {
p = fontMetrics.metrics.sup3;
minSupShift = fontMetrics.metrics.sup3;
} else {
p = fontMetrics.metrics.sup2;
minSupShift = fontMetrics.metrics.sup2;
}
// scriptspace is a font-size-independent size, so scale it
// appropriately
var multiplier = Style.TEXT.sizeMultiplier *
options.style.sizeMultiplier;
var scriptspace =
(0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em";
var supsub;
if (!group.value.sup) {
v = Math.max(v, fontMetrics.metrics.sub1,
// Rule 18b
subShift = Math.max(
subShift, fontMetrics.metrics.sub1,
sub.height - 0.8 * fontMetrics.metrics.xHeight);
supsub = buildCommon.makeVList([
{type: "elem", elem: submid}
], "shift", v, options);
], "shift", subShift, options);
supsub.children[0].style.marginRight = scriptspace;
// Subscripts shouldn't be shifted by the base's italic correction.
// Account for that by shifting the subscript back the appropriate
// amount. Note we only do this when the base is a single symbol.
if (base instanceof domTree.symbolNode) {
supsub.children[0].style.marginLeft = -base.italic + "em";
}
} else if (!group.value.sub) {
u = Math.max(u, p,
// Rule 18c, d
supShift = Math.max(supShift, minSupShift,
sup.depth + 0.25 * fontMetrics.metrics.xHeight);
supsub = buildCommon.makeVList([
{type: "elem", elem: supmid}
], "shift", -u, options);
], "shift", -supShift, options);
supsub.children[0].style.marginRight = scriptspace;
} else {
u = Math.max(u, p,
supShift = Math.max(
supShift, minSupShift,
sup.depth + 0.25 * fontMetrics.metrics.xHeight);
v = Math.max(v, fontMetrics.metrics.sub2);
subShift = Math.max(subShift, fontMetrics.metrics.sub2);
var theta = fontMetrics.metrics.defaultRuleThickness;
var ruleWidth = fontMetrics.metrics.defaultRuleThickness;
if ((u - sup.depth) - (sub.height - v) < 4 * theta) {
v = 4 * theta - (u - sup.depth) + sub.height;
var psi = 0.8 * fontMetrics.metrics.xHeight - (u - sup.depth);
// Rule 18e
if ((supShift - sup.depth) - (sub.height - subShift) <
4 * ruleWidth) {
subShift = 4 * ruleWidth - (supShift - sup.depth) + sub.height;
var psi = 0.8 * fontMetrics.metrics.xHeight -
(supShift - sup.depth);
if (psi > 0) {
u += psi;
v -= psi;
supShift += psi;
subShift -= psi;
}
}
supsub = buildCommon.makeVList([
{type: "elem", elem: submid, shift: v},
{type: "elem", elem: supmid, shift: -u}
{type: "elem", elem: submid, shift: subShift},
{type: "elem", elem: supmid, shift: -supShift}
], "individualShift", null, options);
// See comment above about subscripts not being shifted
if (base instanceof domTree.symbolNode) {
supsub.children[1].style.marginLeft = base.italic + "em";
base.italic = 0;
supsub.children[0].style.marginLeft = -base.italic + "em";
}
supsub.children[0].style.marginRight = scriptspace;
@ -251,22 +366,10 @@ var groupTypes = {
[base, supsub]);
},
open: function(group, options, prev) {
return buildCommon.mathrm(
group.value, group.mode, options.getColor(), ["mopen"]);
},
close: function(group, options, prev) {
return buildCommon.mathrm(
group.value, group.mode, options.getColor(), ["mclose"]);
},
inner: function(group, options, prev) {
return buildCommon.mathrm(
group.value, group.mode, options.getColor(), ["minner"]);
},
frac: function(group, options, prev) {
// Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e).
// Figure out what style this fraction should be in based on the
// function used
var fstyle = options.style;
if (group.value.size === "dfrac") {
fstyle = Style.DISPLAY;
@ -283,40 +386,54 @@ var groupTypes = {
var denom = buildGroup(group.value.denom, options.withStyle(dstyle));
var denomreset = makeSpan([fstyle.reset(), dstyle.cls()], [denom])
var theta = fontMetrics.metrics.defaultRuleThickness / options.style.sizeMultiplier;
var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
options.style.sizeMultiplier;
var mid = makeSpan([options.style.reset(), Style.TEXT.cls(), "frac-line"]);
mid.height = theta;
var mid = makeSpan(
[options.style.reset(), Style.TEXT.cls(), "frac-line"]);
// Manually set the height of the line because its height is created in
// CSS
mid.height = ruleWidth;
var u, v, phi;
// Rule 15b, 15d
var numShift, denomShift, clearance;
if (fstyle.size === Style.DISPLAY.size) {
u = fontMetrics.metrics.num1;
v = fontMetrics.metrics.denom1;
phi = 3 * theta;
numShift = fontMetrics.metrics.num1;
denomShift = fontMetrics.metrics.denom1;
clearance = 3 * ruleWidth;
} else {
u = fontMetrics.metrics.num2;
v = fontMetrics.metrics.denom2;
phi = theta;
numShift = fontMetrics.metrics.num2;
denomShift = fontMetrics.metrics.denom2;
clearance = ruleWidth;
}
var a = fontMetrics.metrics.axisHeight;
var axisHeight = fontMetrics.metrics.axisHeight;
if ((u - numer.depth) - (a + 0.5 * theta) < phi) {
u += phi - ((u - numer.depth) - (a + 0.5 * theta));
// Rule 15d
if ((numShift - numer.depth) - (axisHeight + 0.5 * ruleWidth)
< clearance) {
numShift +=
clearance - ((numShift - numer.depth) -
(axisHeight + 0.5 * ruleWidth));
}
if ((a - 0.5 * theta) - (denom.height - v) < phi) {
v += phi - ((a - 0.5 * theta) - (denom.height - v));
if ((axisHeight - 0.5 * ruleWidth) - (denom.height - denomShift)
< clearance) {
denomShift +=
clearance - ((axisHeight - 0.5 * ruleWidth) -
(denom.height - denomShift));
}
var midShift = -(a - 0.5 * theta);
var midShift = -(axisHeight - 0.5 * ruleWidth);
var frac = buildCommon.makeVList([
{type: "elem", elem: denomreset, shift: v},
{type: "elem", elem: denomreset, shift: denomShift},
{type: "elem", elem: mid, shift: midShift},
{type: "elem", elem: numerreset, shift: -u}
{type: "elem", elem: numerreset, shift: -numShift}
], "individualShift", null, options);
// Since we manually change the style sometimes (with \dfrac or \tfrac),
// account for the possible size change here.
frac.height *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
frac.depth *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
@ -325,24 +442,19 @@ var groupTypes = {
[frac], options.getColor());
},
color: function(group, options, prev) {
var elements = buildExpression(
group.value.value,
options.withColor(group.value.color),
prev
);
return new buildCommon.makeFragment(elements);
},
spacing: function(group, options, prev) {
if (group.value === "\\ " || group.value === "\\space" ||
group.value === " " || group.value === "~") {
// Spaces are generated by adding an actual space. Each of these
// things has an entry in the symbols table, so these will be turned
// into appropriate outputs.
return makeSpan(
["mord", "mspace"],
[buildCommon.mathrm(group.value, group.mode)]
);
} else {
// Other kinds of spaces are of arbitrary width. We use CSS to
// generate these.
var spacingClassMap = {
"\\qquad": "qquad",
"\\quad": "quad",
@ -374,23 +486,15 @@ var groupTypes = {
["rlap", options.style.cls()], [inner, fix]);
},
punct: function(group, options, prev) {
return buildCommon.mathrm(
group.value, group.mode, options.getColor(), ["mpunct"]);
},
ordgroup: function(group, options, prev) {
return makeSpan(
["mord", options.style.cls()],
buildExpression(group.value, options.reset())
);
},
op: function(group, options, prev) {
// Operators are handled in the TeXbook pg. 443-444, rule 13(a).
var supGroup;
var subGroup;
var hasLimits = false;
if (group.type === "supsub" ) {
// If we have limits, supsub will pass us its group to handle. Pull
// out the superscript and subscript and set the group to the op in
// its base.
supGroup = group.value.sup;
subGroup = group.value.sub;
group = group.value.base;
@ -403,29 +507,40 @@ var groupTypes = {
];
var large = false;
if (options.style.id === Style.DISPLAY.id &&
if (options.style.size === Style.DISPLAY.size &&
group.value.symbol &&
!utils.contains(noSuccessor, group.value.body)) {
// Make symbols larger in displaystyle, except for smallint
// Most symbol operators get larger in displaystyle (rule 13)
large = true;
}
var base;
var baseShift = 0;
var delta = 0;
var slant = 0;
if (group.value.symbol) {
// If this is a symbol, create the symbol.
var style = large ? "Size2-Regular" : "Size1-Regular";
base = buildCommon.makeSymbol(
group.value.body, style, "math", options.getColor(),
["op-symbol", large ? "large-op" : "small-op", "mop"]);
// Shift the symbol so its center lies on the axis (rule 13). It
// appears that our fonts have the centers of the symbols already
// almost on the axis, so these numbers are very small. Note we
// don't actually apply this here, but instead it is used either in
// the vlist creation or separately when there are no limits.
baseShift = (base.height - base.depth) / 2 -
fontMetrics.metrics.axisHeight *
options.style.sizeMultiplier;
delta = base.italic;
// The slant of the symbol is just its italic correction.
slant = base.italic;
} else {
// Otherwise, this is a text operator. Build the text from the
// operator's name.
// TODO(emily): Add a space in the middle of some of these
// operators, like \limsup
var output = [];
for (var i = 1; i < group.value.body.length; i++) {
output.push(buildCommon.mathrm(group.value.body[i], group.mode));
@ -438,28 +553,34 @@ var groupTypes = {
// in a new span so it is an inline, and works.
var base = makeSpan([], [base]);
var supmid, supKern, submid, subKern;
// We manually have to handle the superscripts and subscripts. This,
// aside from the kern calculations, is copied from supsub.
if (supGroup) {
var sup = buildGroup(supGroup,
options.withStyle(options.style.sup()));
var supmid = makeSpan(
var sup = buildGroup(
supGroup, options.withStyle(options.style.sup()));
supmid = makeSpan(
[options.style.reset(), options.style.sup().cls()], [sup]);
var supKern = Math.max(
supKern = Math.max(
fontMetrics.metrics.bigOpSpacing1,
fontMetrics.metrics.bigOpSpacing3 - sup.depth);
}
if (subGroup) {
var sub = buildGroup(subGroup,
options.withStyle(options.style.sub()));
var submid = makeSpan(
[options.style.reset(), options.style.sub().cls()], [sub]);
var sub = buildGroup(
subGroup, options.withStyle(options.style.sub()));
submid = makeSpan(
[options.style.reset(), options.style.sub().cls()],
[sub]);
var subKern = Math.max(
subKern = Math.max(
fontMetrics.metrics.bigOpSpacing2,
fontMetrics.metrics.bigOpSpacing4 - sub.height);
}
// Build the final group as a vlist of the possible subscript, base,
// and possible superscript.
var finalGroup;
if (!supGroup) {
var top = base.height - baseShift;
@ -471,7 +592,11 @@ var groupTypes = {
{type: "elem", elem: base}
], "top", top, options);
finalGroup.children[0].style.marginLeft = -delta + "em";
// Here, we shift the limits by the slant of the symbol. Note
// that we are supposed to shift the limits by 1/2 of the slant,
// but since we are centering the limits adding a full slant of
// margin will shift by 1/2 that.
finalGroup.children[0].style.marginLeft = -slant + "em";
} else if (!subGroup) {
var bottom = base.depth + baseShift;
@ -482,8 +607,12 @@ var groupTypes = {
{type: "kern", size: fontMetrics.metrics.bigOpSpacing5}
], "bottom", bottom, options);
finalGroup.children[1].style.marginLeft = delta + "em";
// See comment above about slants
finalGroup.children[1].style.marginLeft = slant + "em";
} else if (!supGroup && !subGroup) {
// This case probably shouldn't occur (this would mean the
// supsub was sending us a group with no superscript or
// subscript) but be safe.
return base;
} else {
var bottom = fontMetrics.metrics.bigOpSpacing5 +
@ -501,8 +630,9 @@ var groupTypes = {
{type: "kern", size: fontMetrics.metrics.bigOpSpacing5}
], "bottom", bottom, options);
finalGroup.children[0].style.marginLeft = -delta + "em";
finalGroup.children[2].style.marginLeft = delta + "em";
// See comment above about slants
finalGroup.children[0].style.marginLeft = -slant + "em";
finalGroup.children[2].style.marginLeft = slant + "em";
}
return makeSpan(["mop", "op-limits"], [finalGroup]);
@ -516,6 +646,9 @@ var groupTypes = {
},
katex: function(group, options, prev) {
// The KaTeX logo. The offsets for the K and a were chosen to look
// good, but the offsets for the T, E, and X were taken from the
// definition of \TeX in TeX (see TeXbook pg. 356)
var k = makeSpan(
["k"], [buildCommon.mathrm("K", group.mode)]);
var a = makeSpan(
@ -539,42 +672,78 @@ var groupTypes = {
["katex-logo"], [k, a, t, e, x], options.getColor());
},
overline: function(group, options, prev) {
// Overlines are handled in the TeXbook pg 443, Rule 9.
// Build the inner group in the cramped style.
var innerGroup = buildGroup(group.value.body,
options.withStyle(options.style.cramp()));
var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
options.style.sizeMultiplier;
// Create the line above the body
var line = makeSpan(
[options.style.reset(), Style.TEXT.cls(), "overline-line"]);
line.height = ruleWidth;
line.maxFontSize = 1.0;
// Generate the vlist, with the appropriate kerns
var vlist = buildCommon.makeVList([
{type: "elem", elem: innerGroup},
{type: "kern", size: 3 * ruleWidth},
{type: "elem", elem: line},
{type: "kern", size: ruleWidth}
], "firstBaseline", null, options);
return makeSpan(["overline", "mord"], [vlist], options.getColor());
},
sqrt: function(group, options, prev) {
// Square roots are handled in the TeXbook pg. 443, Rule 11.
// First, we do the same steps as in overline to build the inner group
// and line
var inner = buildGroup(group.value.body,
options.withStyle(options.style.cramp()));
var theta = fontMetrics.metrics.defaultRuleThickness /
var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
options.style.sizeMultiplier;
var line = makeSpan(
[options.style.reset(), Style.TEXT.cls(), "sqrt-line"], [],
options.getColor());
line.height = theta;
line.height = ruleWidth;
line.maxFontSize = 1.0;
var phi = theta;
var phi = ruleWidth;
if (options.style.id < Style.TEXT.id) {
phi = fontMetrics.metrics.xHeight;
}
var psi = theta + phi / 4;
// Calculate the clearance between the body and line
var lineClearance = ruleWidth + phi / 4;
var innerHeight =
(inner.height + inner.depth) * options.style.sizeMultiplier;
var minDelimiterHeight = innerHeight + psi + theta;
var minDelimiterHeight = innerHeight + lineClearance + ruleWidth;
// Create a \surd delimiter of the required minimum size
var delim = makeSpan(["sqrt-sign"], [
delimiter.customSizedDelim("\\surd", minDelimiterHeight,
false, options, group.mode)],
options.getColor());
var delimDepth = (delim.height + delim.depth) - theta;
var delimDepth = (delim.height + delim.depth) - ruleWidth;
if (delimDepth > inner.height + inner.depth + psi) {
psi = (psi + delimDepth - inner.height - inner.depth) / 2;
// Adjust the clearance based on the delimiter size
if (delimDepth > inner.height + inner.depth + lineClearance) {
lineClearance =
(lineClearance + delimDepth - inner.height - inner.depth) / 2;
}
delimShift = -(inner.height + psi + theta) + delim.height;
// Shift the delimiter so that its top lines up with the top of the line
delimShift = -(inner.height + lineClearance + ruleWidth) + delim.height;
delim.style.top = delimShift + "em";
delim.height -= delimShift;
delim.depth += delimShift;
@ -590,38 +759,19 @@ var groupTypes = {
} else {
body = buildCommon.makeVList([
{type: "elem", elem: inner},
{type: "kern", size: psi},
{type: "kern", size: lineClearance},
{type: "elem", elem: line},
{type: "kern", size: theta}
{type: "kern", size: ruleWidth}
], "firstBaseline", null, options);
}
return makeSpan(["sqrt", "mord"], [delim, body]);
},
overline: function(group, options, prev) {
var innerGroup = buildGroup(group.value.body,
options.withStyle(options.style.cramp()));
var theta = fontMetrics.metrics.defaultRuleThickness /
options.style.sizeMultiplier;
var line = makeSpan(
[options.style.reset(), Style.TEXT.cls(), "overline-line"]);
line.height = theta;
line.maxFontSize = 1.0;
var vlist = buildCommon.makeVList([
{type: "elem", elem: innerGroup},
{type: "kern", size: 3 * theta},
{type: "elem", elem: line},
{type: "kern", size: theta}
], "firstBaseline", null, options);
return makeSpan(["overline", "mord"], [vlist], options.getColor());
},
sizing: function(group, options, prev) {
// Handle sizing operators like \Huge. Real TeX doesn't actually allow
// these functions inside of math expressions, so we do some special
// handling.
var inner = buildExpression(group.value.value,
options.withSize(group.value.size), prev);
@ -630,26 +780,17 @@ var groupTypes = {
options.style.cls()],
inner)]);
var sizeToFontSize = {
"size1": 0.5,
"size2": 0.7,
"size3": 0.8,
"size4": 0.9,
"size5": 1.0,
"size6": 1.2,
"size7": 1.44,
"size8": 1.73,
"size9": 2.07,
"size10": 2.49
};
var fontSize = sizeToFontSize[group.value.size];
// Calculate the correct maxFontSize manually
var fontSize = sizingMultiplier[group.value.size];
span.maxFontSize = fontSize * options.style.sizeMultiplier;
return span;
},
styling: function(group, options, prev) {
// Style changes are handled in the TeXbook on pg. 442, Rule 3.
// Figure out what style we're changing to.
var style = {
"display": Style.DISPLAY,
"text": Style.TEXT,
@ -659,6 +800,7 @@ var groupTypes = {
var newStyle = style[group.value.style];
// Build the inner expression in the new style.
var inner = buildExpression(
group.value.value, options.withStyle(newStyle), prev);
@ -669,9 +811,12 @@ var groupTypes = {
var delim = group.value.value;
if (delim === ".") {
// Empty delimiters still count as elements, even though they don't
// show anything.
return makeSpan([groupToType[group.value.delimType]]);
}
// Use delimiter.sizedDelim to generate the delimiter.
return makeSpan(
[groupToType[group.value.delimType]],
[delimiter.sizedDelim(
@ -679,30 +824,40 @@ var groupTypes = {
},
leftright: function(group, options, prev) {
// Build the inner expression
var inner = buildExpression(group.value.body, options.reset());
var innerHeight = 0;
var innerDepth = 0;
// Calculate its height and depth
for (var i = 0; i < inner.length; i++) {
innerHeight = Math.max(inner[i].height, innerHeight);
innerDepth = Math.max(inner[i].depth, innerDepth);
}
// The size of delimiters is the same, regardless of what style we are
// in. Thus, to correctly calculate the size of delimiter we need around
// a group, we scale down the inner size based on the size.
innerHeight *= options.style.sizeMultiplier;
innerDepth *= options.style.sizeMultiplier;
var leftDelim;
if (group.value.left === ".") {
// Empty delimiters in \left and \right make null delimiter spaces.
leftDelim = makeSpan(["nulldelimiter"]);
} else {
// Otherwise, use leftRightDelim to generate the correct sized
// delimiter.
leftDelim = delimiter.leftRightDelim(
group.value.left, innerHeight, innerDepth, options,
group.mode);
}
// Add it to the beginning of the expression
inner.unshift(leftDelim);
var rightDelim;
// Same for the right delimiter
if (group.value.right === ".") {
rightDelim = makeSpan(["nulldelimiter"]);
} else {
@ -710,6 +865,7 @@ var groupTypes = {
group.value.right, innerHeight, innerDepth, options,
group.mode);
}
// Add it to the end of the expression.
inner.push(rightDelim);
return makeSpan(
@ -720,6 +876,7 @@ var groupTypes = {
// Make an empty span for the rule
var rule = makeSpan(["mord", "rule"], [], options.getColor());
// Calculate the width and height of the rule, and account for units
var width = group.value.width.number;
if (group.value.width.unit === "ex") {
width *= fontMetrics.metrics.xHeight;
@ -730,6 +887,8 @@ var groupTypes = {
height *= fontMetrics.metrics.xHeight;
}
// The sizes of rules are absolute, so make it larger if we are in a
// smaller style.
width /= options.style.sizeMultiplier;
height /= options.style.sizeMultiplier;
@ -745,36 +904,69 @@ var groupTypes = {
},
accent: function(group, options, prev) {
// Accents are handled in the TeXbook pg. 443, rule 12.
var base = group.value.base;
var supsubGroup;
if (group.type === "supsub") {
// If our base is a character box, and we have superscripts and
// subscripts, the supsub will defer to us. In particular, we want
// to attach the superscripts and subscripts to the inner body (so
// that the position of the superscripts and subscripts won't be
// affected by the height of the accent). We accomplish this by
// sticking the base of the accent into the base of the supsub, and
// rendering that, while keeping track of where the accent is.
// The supsub group is the group that was passed in
var supsub = group;
group = group.value.base;
// The real accent group is the base of the supsub group
group = supsub.value.base;
// The character box is the base of the accent group
base = group.value.base;
// Stick the character box into the base of the supsub group
supsub.value.base = base;
// Rerender the supsub group with its new base, and store that
// result.
supsubGroup = buildGroup(
supsub, options.reset());
supsub, options.reset(), prev);
}
// Build the base group
var body = buildGroup(
base, options.withStyle(options.style.cramp()));
var s;
if (isCharacterBox(group.value.base)) {
var baseChar = getBaseElem(group.value.base);
// Calculate the skew of the accent. This is based on the line "If the
// nucleus is not a single character, let s = 0; otherwise set s to the
// kern amount for the nucleus followed by the \skewchar of its font."
// Note that our skew metrics are just the kern between each character
// and the skewchar.
var skew;
if (isCharacterBox(base)) {
// If the base is a character box, then we want the skew of the
// innermost character. To do that, we find the innermost character:
var baseChar = getBaseElem(base);
// Then, we render its group to get the symbol inside it
var baseGroup = buildGroup(
baseChar, options.withStyle(options.style.cramp()));
s = baseGroup.skew;
// Finally, we pull the skew off of the symbol.
skew = baseGroup.skew;
// Note that we now throw away baseGroup, because the layers we
// removed with getBaseElem might contain things like \color which
// we can't get rid of.
// TODO(emily): Find a better way to get the skew
} else {
s = 0;
skew = 0;
}
var delta = Math.min(body.height, fontMetrics.metrics.xHeight);
// calculate the amount of space between the body and the accent
var clearance = Math.min(body.height, fontMetrics.metrics.xHeight);
// Build the accent
var accent = buildCommon.makeSymbol(
group.value.accent, "Main-Regular", "math", options.getColor());
// Remove the italic correction of the accent, because it only serves to
// shift the accent over to a place we don't want.
accent.italic = 0;
// The \vec character that the fonts use is a combining character, and
@ -788,11 +980,14 @@ var groupTypes = {
var accentBody = buildCommon.makeVList([
{type: "elem", elem: body},
{type: "kern", size: -delta},
{type: "kern", size: -clearance},
{type: "elem", elem: accentBody}
], "firstBaseline", null, options);
accentBody.children[1].style.marginLeft = 2 * s + "em";
// Shift the accent over by the skew. Note we shift by twice the skew
// because we are centering the accent, so by adding 2*skew to the left,
// we shift it to the right by 1*skew.
accentBody.children[1].style.marginLeft = 2 * skew + "em";
var accentWrap = makeSpan(["mord", "accent"], [accentBody]);
@ -828,14 +1023,22 @@ var sizingMultiplier = {
size10: 2.49
};
/**
* buildGroup is the function that takes a group and calls the correct groupType
* function for it. It also handles the interaction of size and style changes
* between parents and children.
*/
var buildGroup = function(group, options, prev) {
if (!group) {
return makeSpan();
}
if (groupTypes[group.type]) {
// Call the groupTypes function
var groupNode = groupTypes[group.type](group, options, prev);
// If the style changed between the parent and the current group,
// account for the size difference
if (options.style !== options.parentStyle) {
var multiplier = options.style.sizeMultiplier /
options.parentStyle.sizeMultiplier;
@ -844,6 +1047,8 @@ var buildGroup = function(group, options, prev) {
groupNode.depth *= multiplier;
}
// If the size changed between the parent and the current group, account
// for that size difference.
if (options.size !== options.parentSize) {
var multiplier = sizingMultiplier[options.size] /
sizingMultiplier[options.parentSize];
@ -859,24 +1064,33 @@ var buildGroup = function(group, options, prev) {
}
};
/**
* Take an entire parse tree, and build it into an appropriate set of nodes.
*/
var buildTree = function(tree) {
// Setup the default options
var options = new Options(Style.TEXT, "size5", "");
// Build the expression contained in the tree
var expression = buildExpression(tree, options);
var span = makeSpan(["base", options.style.cls()], expression);
var body = makeSpan(["base", options.style.cls()], expression);
// Add struts, which ensure that the top of the HTML element falls at the
// height of the expression, and the bottom of the HTML element falls at the
// depth of the expression.
var topStrut = makeSpan(["strut"]);
var bottomStrut = makeSpan(["strut", "bottom"]);
topStrut.style.height = span.height + "em";
bottomStrut.style.height = (span.height + span.depth) + "em";
topStrut.style.height = body.height + "em";
bottomStrut.style.height = (body.height + body.depth) + "em";
// We'd like to use `vertical-align: top` but in IE 9 this lowers the
// baseline of the box to the bottom of this strut (instead staying in the
// normal place) so we use an absolute value for vertical-align instead
bottomStrut.style.verticalAlign = -span.depth + "em";
bottomStrut.style.verticalAlign = -body.depth + "em";
// Wrap the struts and body together
var katexNode = makeSpan(["katex"], [
makeSpan(["katex-inner"], [topStrut, bottomStrut, span])
makeSpan(["katex-inner"], [topStrut, bottomStrut, body])
]);
return katexNode;

View File

@ -1,17 +1,42 @@
/**
* This file deals with creating delimiters of various sizes. The TeXbook
* discusses these routines on page 441-442, in the "Another subroutine sets box
* x to a specified variable delimiter" paragraph.
*
* There are three main routines here. `makeSmallDelim` makes a delimiter in the
* normal font, but in either text, script, or scriptscript style.
* `makeLargeDelim` makes a delimiter in textstyle, but in one of the Size1,
* Size2, Size3, or Size4 fonts. `makeStackedDelim` makes a delimiter out of
* smaller pieces that are stacked on top of one another.
*
* The functions take a parameter `center`, which determines if the delimiter
* should be centered around the axis.
*
* Then, there are three exposed functions. `sizedDelim` makes a delimiter in
* one of the given sizes. This is used for things like `\bigl`.
* `customSizedDelim` makes a delimiter with a given total height+depth. It is
* called in places like `\sqrt`. `leftRightDelim` makes an appropriate
* delimiter which surrounds an expression of a given height an depth. It is
* used in `\left` and `\right`.
*/
var Options = require("./Options");
var ParseError = require("./ParseError");
var Style = require("./Style");
var buildCommon = require("./buildCommon");
var domTree = require("./domTree");
var fontMetrics = require("./fontMetrics");
var parseTree = require("./parseTree");
var utils = require("./utils");
var symbols = require("./symbols");
var buildCommon = require("./buildCommon");
var makeSpan = require("./buildCommon").makeSpan;
var utils = require("./utils");
// Get the metrics for a given symbol and font, after transformation (i.e.
// after following replacement from symbols.js)
var makeSpan = buildCommon.makeSpan;
/**
* Get the metrics for a given symbol and font, after transformation (i.e.
* after following replacement from symbols.js)
*/
var getMetrics = function(symbol, font) {
if (symbols["math"][symbol] && symbols["math"][symbol].replace) {
return fontMetrics.getCharacterMetrics(
@ -22,12 +47,20 @@ var getMetrics = function(symbol, font) {
}
};
/**
* Builds a symbol in the given font size (note size is an integer)
*/
var mathrmSize = function(value, size, mode) {
return buildCommon.makeSymbol(value, "Size" + size + "-Regular", mode);
};
/**
* Puts a delimiter span in a given style, and adds appropriate height, depth,
* and maxFontSizes.
*/
var styleWrap = function(delim, toStyle, options) {
var span = makeSpan(["style-wrap", options.style.reset(), toStyle.cls()], [delim]);
var span = makeSpan(
["style-wrap", options.style.reset(), toStyle.cls()], [delim]);
var multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier;
@ -38,6 +71,11 @@ var styleWrap = function(delim, toStyle, options) {
return span;
};
/**
* Makes a small delimiter. This is a delimiter that comes in the Main-Regular
* font, but is restyled to either be in textstyle, scriptstyle, or
* scriptscriptstyle.
*/
var makeSmallDelim = function(delim, style, center, options, mode) {
var text = buildCommon.makeSymbol(delim, "Main-Regular", mode);
@ -56,6 +94,10 @@ var makeSmallDelim = function(delim, style, center, options, mode) {
return span;
};
/**
* Makes a large delimiter. This is a delimiter that comes in the Size1, Size2,
* Size3, or Size4 fonts. It is always rendered in textstyle.
*/
var makeLargeDelim = function(delim, size, center, options, mode) {
var inner = mathrmSize(delim, size, mode);
@ -76,9 +118,13 @@ var makeLargeDelim = function(delim, size, center, options, mode) {
return span;
};
// Make an inner span with the given offset and in the given font
/**
* Make an inner span with the given offset and in the given font. This is used
* in `makeStackedDelim` to make the stacking pieces for the delimiter.
*/
var makeInner = function(symbol, font, mode) {
var sizeClass;
// Apply the correct CSS class to choose the right font.
if (font === "Size1-Regular") {
sizeClass = "delim-size1";
} else if (font === "Size4-Regular") {
@ -89,14 +135,22 @@ var makeInner = function(symbol, font, mode) {
["delimsizinginner", sizeClass],
[makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]);
// Since this will be passed into `makeVList` in the end, wrap the element
// in the appropriate tag that VList uses.
return {type: "elem", elem: inner};
};
/**
* Make a stacked delimiter out of a given delimiter, with the total height at
* least `heightTotal`. This routine is mentioned on page 442 of the TeXbook.
*/
var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
// There are four parts, the top, a middle, a repeated part, and a bottom.
// There are four parts, the top, an optional middle, a repeated part, and a
// bottom.
var top, middle, repeat, bottom;
top = repeat = bottom = delim;
middle = null;
// Also keep track of what font the delimiters are in
var font = "Size1-Regular";
// We set the parts and font based on the symbol. Note that we use
@ -175,7 +229,7 @@ var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
font = "Size4-Regular";
}
// Get the metrics of the three sections
// Get the metrics of the four sections
var topMetrics = getMetrics(top, font);
var topHeightTotal = topMetrics.height + topMetrics.depth;
var repeatMetrics = getMetrics(repeat, font);
@ -188,36 +242,49 @@ var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
middleHeightTotal = middleMetrics.height + middleMetrics.depth;
}
// Calcuate the real height that the delimiter will have. It is at least the
// size of the top, bottom, and optional middle combined.
var realHeightTotal = topHeightTotal + bottomHeightTotal;
if (middle !== null) {
realHeightTotal += middleHeightTotal;
}
// Then add repeated pieces until we reach the specified height.
while (realHeightTotal < heightTotal) {
realHeightTotal += repeatHeightTotal;
if (middle !== null) {
// If there is a middle section, we need an equal number of pieces
// on the top and bottom.
realHeightTotal += repeatHeightTotal;
}
}
// The center of the delimiter is placed at the center of the axis. Note
// that in this context, "center" means that the delimiter should be
// centered around the axis in the current style, while normally it is
// centered around the axis in textstyle.
var axisHeight = fontMetrics.metrics.axisHeight;
if (center) {
axisHeight *= options.style.sizeMultiplier;
}
// Calculate the height and depth
var height = realHeightTotal / 2 + axisHeight;
var depth = realHeightTotal / 2 - axisHeight;
// Keep a list of the inner spans
// Now, we start building the pieces that will go into the vlist
// Keep a list of the inner pieces
var inners = [];
// Add the bottom symbol
inners.push(makeInner(bottom, font, mode));
if (middle === null) {
// Calculate the number of repeated symbols we need
var repeatHeight = realHeightTotal - topHeightTotal - bottomHeightTotal;
var symbolCount = Math.ceil(repeatHeight / repeatHeightTotal);
// Add repeat symbols until there's only space for the bottom symbol
// Add that many symbols
for (var i = 0; i < symbolCount; i++) {
inners.push(makeInner(repeat, font, mode));
}
@ -250,8 +317,10 @@ var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
}
}
// Add the top symbol
inners.push(makeInner(top, font, mode));
// Finally, build the vlist
var inner = buildCommon.makeVList(inners, "bottom", depth, options);
return styleWrap(
@ -259,29 +328,37 @@ var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
Style.TEXT, options);
};
var normalDelimiters = [
// There are three kinds of delimiters, delimiters that stack when they become
// too large
var stackLargeDelimiters = [
"(", ")", "[", "\\lbrack", "]", "\\rbrack",
"\\{", "\\lbrace", "\\}", "\\rbrace",
"\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
"<", ">", "\\langle", "\\rangle", "/", "\\backslash",
"\\surd"
];
var stackDelimiters = [
// delimiters that always stack
var stackAlwaysDelimiters = [
"\\uparrow", "\\downarrow", "\\updownarrow",
"\\Uparrow", "\\Downarrow", "\\Updownarrow",
"|", "\\|", "\\vert", "\\Vert"
];
var onlyNormalDelimiters = [
// and delimiters that never stack
var stackNeverDelimiters = [
"<", ">", "\\langle", "\\rangle", "/", "\\backslash"
];
// Metrics of the different sizes. Found by looking at TeX's output of
// $\bigl| \Bigl| \biggl| \Biggl| \showlists$
// $\bigl| // \Bigl| \biggl| \Biggl| \showlists$
// Used to create stacked delimiters of appropriate sizes in makeSizedDelim.
var sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0];
/**
* Used to create a delimiter of a specific size, where `size` is 1, 2, 3, or 4.
*/
var makeSizedDelim = function(delim, size, options, mode) {
// < and > turn into \langle and \rangle in delimiters
if (delim === "<") {
delim = "\\langle";
} else if (delim === ">") {
@ -290,9 +367,11 @@ var makeSizedDelim = function(delim, size, options, mode) {
var retDelim;
if (utils.contains(normalDelimiters, delim)) {
// Sized delimiters are never centered.
if (utils.contains(stackLargeDelimiters, delim) ||
utils.contains(stackNeverDelimiters, delim)) {
return makeLargeDelim(delim, size, false, options, mode);
} else if (utils.contains(stackDelimiters, delim)) {
} else if (utils.contains(stackAlwaysDelimiters, delim)) {
return makeStackedDelim(
delim, sizeToMaxHeight[size], false, options, mode);
} else {
@ -300,7 +379,20 @@ var makeSizedDelim = function(delim, size, options, mode) {
}
};
var normalDelimiterSequence = [
/**
* There are three different sequences of delimiter sizes that the delimiters
* follow depending on the kind of delimiter. This is used when creating custom
* sized delimiters to decide whether to create a small, large, or stacked
* delimiter.
*
* In real TeX, these sequences aren't explicitly defined, but are instead
* defined inside the font metrics. Since there are only three sequences that
* are possible for the delimiters that TeX defines, it is easier to just encode
* them explicitly here.
*/
// Delimiters that never stack try small delimiters and large delimiters only
var stackNeverDelimiterSequence = [
{type: "small", style: Style.SCRIPTSCRIPT},
{type: "small", style: Style.SCRIPT},
{type: "small", style: Style.TEXT},
@ -310,6 +402,7 @@ var normalDelimiterSequence = [
{type: "large", size: 4}
];
// Delimiters that always stack try the small delimiters first, then stack
var stackAlwaysDelimiterSequence = [
{type: "small", style: Style.SCRIPTSCRIPT},
{type: "small", style: Style.SCRIPT},
@ -317,6 +410,8 @@ var stackAlwaysDelimiterSequence = [
{type: "stack"}
];
// Delimiters that stack when large try the small and then large delimiters, and
// stack afterwards
var stackLargeDelimiterSequence = [
{type: "small", style: Style.SCRIPTSCRIPT},
{type: "small", style: Style.SCRIPT},
@ -328,6 +423,9 @@ var stackLargeDelimiterSequence = [
{type: "stack"}
];
/**
* Get the font used in a delimiter based on what kind of delimiter it is.
*/
var delimTypeToFont = function(type) {
if (type.type === "small") {
return "Main-Regular";
@ -338,6 +436,10 @@ var delimTypeToFont = function(type) {
}
};
/**
* Traverse a sequence of types of delimiters to decide what kind of delimiter
* should be used to create a delimiter of the given height+depth.
*/
var traverseSequence = function(delim, height, sequence, options) {
// Here, we choose the index we should start at in the sequences. In smaller
// sizes (which correspond to larger numbers in style.size) we start earlier
@ -351,21 +453,29 @@ var traverseSequence = function(delim, height, sequence, options) {
}
var metrics = getMetrics(delim, delimTypeToFont(sequence[i]));
var heightDepth = metrics.height + metrics.depth;
// Small delimiters are scaled down versions of the same font, so we
// account for the style change size.
if (sequence[i].type === "small") {
heightDepth *= sequence[i].style.sizeMultiplier;
}
// Check if the delimiter at this size works for the given height.
if (heightDepth > height) {
return sequence[i];
}
}
// If we reached the end of the sequence, return the last sequence element.
return sequence[sequence.length - 1];
};
/**
* Make a delimiter of a given height+depth, with optional centering. Here, we
* traverse the sequences, and create a delimiter that the sequence tells us to.
*/
var makeCustomSizedDelim = function(delim, height, center, options, mode) {
if (delim === "<") {
delim = "\\langle";
@ -373,17 +483,21 @@ var makeCustomSizedDelim = function(delim, height, center, options, mode) {
delim = "\\rangle";
}
// Decide what sequence to use
var sequence;
if (utils.contains(onlyNormalDelimiters, delim)) {
sequence = normalDelimiterSequence;
} else if (utils.contains(normalDelimiters, delim)) {
if (utils.contains(stackNeverDelimiters, delim)) {
sequence = stackNeverDelimiterSequence;
} else if (utils.contains(stackLargeDelimiters, delim)) {
sequence = stackLargeDelimiterSequence;
} else {
sequence = stackAlwaysDelimiterSequence;
}
// Look through the sequence
var delimType = traverseSequence(delim, height, sequence, options);
// Depending on the sequence element we decided on, call the appropriate
// function.
if (delimType.type === "small") {
return makeSmallDelim(delim, delimType.style, center, options, mode);
} else if (delimType.type === "large") {
@ -393,7 +507,12 @@ var makeCustomSizedDelim = function(delim, height, center, options, mode) {
}
};
/**
* Make a delimiter for use with `\left` and `\right`, given a height and depth
* of an expression that the delimiters surround.
*/
var makeLeftRightDelim = function(delim, height, depth, options, mode) {
// We always center \left/\right delimiters, so the axis is always shifted
var axisHeight =
fontMetrics.metrics.axisHeight * options.style.sizeMultiplier;
@ -417,6 +536,8 @@ var makeLeftRightDelim = function(delim, height, depth, options, mode) {
maxDistFromAxis / 500 * delimiterFactor,
2 * maxDistFromAxis - delimiterExtend);
// Finally, we defer to `makeCustomSizedDelim` with our calculated total
// height
return makeCustomSizedDelim(delim, totalHeight, true, options, mode);
};

View File

@ -1,11 +1,17 @@
// These objects store the data about the DOM nodes we create, as well as some
// extra data. They can then be transformed into real DOM nodes with the toNode
// function or HTML markup using toMarkup. They are useful for both storing
// extra properties on the nodes, as well as providing a way to easily work
// with the DOM.
/**
* These objects store the data about the DOM nodes we create, as well as some
* extra data. They can then be transformed into real DOM nodes with the toNode
* function or HTML markup using toMarkup. They are useful for both storing
* extra properties on the nodes, as well as providing a way to easily work
* with the DOM.
*/
var utils = require("./utils");
/**
* Create an HTML className based on a list of classes. In addition to joining
* with spaces, we also remove null or empty classes.
*/
var createClass = function(classes) {
classes = classes.slice();
for (var i = classes.length - 1; i >= 0; i--) {
@ -17,6 +23,11 @@ var createClass = function(classes) {
return classes.join(" ");
};
/**
* This node represents a span node, with a className, a list of children, and
* an inline style. It also contains information about its height, depth, and
* maxFontSize.
*/
function span(classes, children, height, depth, maxFontSize, style) {
this.classes = classes || [];
this.children = children || [];
@ -26,17 +37,23 @@ function span(classes, children, height, depth, maxFontSize, style) {
this.style = style || {};
}
/**
* Convert the span into an HTML node
*/
span.prototype.toNode = function() {
var span = document.createElement("span");
// Apply the class
span.className = createClass(this.classes);
// Apply inline styles
for (var style in this.style) {
if (this.style.hasOwnProperty(style)) {
span.style[style] = this.style[style];
}
}
// Append the children, also as HTML nodes
for (var i = 0; i < this.children.length; i++) {
span.appendChild(this.children[i].toNode());
}
@ -44,9 +61,13 @@ span.prototype.toNode = function() {
return span;
};
/**
* Convert the span into an HTML markup string
*/
span.prototype.toMarkup = function() {
var markup = "<span";
// Add the class
if (this.classes.length) {
markup += " class=\"";
markup += utils.escape(createClass(this.classes));
@ -55,6 +76,7 @@ span.prototype.toMarkup = function() {
var styles = "";
// Add the styles, after hyphenation
for (var style in this.style) {
if (this.style.hasOwnProperty(style)) {
styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
@ -67,6 +89,7 @@ span.prototype.toMarkup = function() {
markup += ">";
// Add the markup of the children, also as markup
for (var i = 0; i < this.children.length; i++) {
markup += this.children[i].toMarkup();
}
@ -76,6 +99,12 @@ span.prototype.toMarkup = function() {
return markup;
};
/**
* This node represents a document fragment, which contains elements, but when
* placed into the DOM doesn't have any representation itself. Thus, it only
* contains children and doesn't have any HTML properties. It also keeps track
* of a height, depth, and maxFontSize.
*/
function documentFragment(children, height, depth, maxFontSize) {
this.children = children || [];
this.height = height || 0;
@ -83,9 +112,14 @@ function documentFragment(children, height, depth, maxFontSize) {
this.maxFontSize = maxFontSize || 0;
}
/**
* Convert the fragment into a node
*/
documentFragment.prototype.toNode = function() {
// Create a fragment
var frag = document.createDocumentFragment();
// Append the children
for (var i = 0; i < this.children.length; i++) {
frag.appendChild(this.children[i].toNode());
}
@ -93,9 +127,13 @@ documentFragment.prototype.toNode = function() {
return frag;
};
/**
* Convert the fragment into HTML markup
*/
documentFragment.prototype.toMarkup = function() {
var markup = "";
// Simply concatenate the markup for the children together
for (var i = 0; i < this.children.length; i++) {
markup += this.children[i].toMarkup();
}
@ -103,6 +141,11 @@ documentFragment.prototype.toMarkup = function() {
return markup;
};
/**
* A symbol node contains information about a single symbol. It either renders
* to a single text node, or a span with a single text node in it, depending on
* whether it has CSS classes, styles, or needs italic correction.
*/
function symbolNode(value, height, depth, italic, skew, classes, style) {
this.value = value || "";
this.height = height || 0;
@ -114,6 +157,10 @@ function symbolNode(value, height, depth, italic, skew, classes, style) {
this.maxFontSize = 0;
}
/**
* Creates a text node or span from a symbol node. Note that a span is only
* created if it is needed.
*/
symbolNode.prototype.toNode = function() {
var node = document.createTextNode(this.value);
var span = null;
@ -143,6 +190,9 @@ symbolNode.prototype.toNode = function() {
}
};
/**
* Creates markup for a symbol node.
*/
symbolNode.prototype.toMarkup = function() {
// TODO(alpert): More duplication than I'd like from
// span.prototype.toMarkup and symbolNode.prototype.toNode...

File diff suppressed because one or more lines are too long

View File

@ -1,9 +1,21 @@
/**
* This is the main entry point for KaTeX. Here, we expose functions for
* rendering expressions either to DOM nodes or to markup strings.
*
* We also expose the ParseError class to check if errors thrown from KaTeX are
* errors in the expression, or errors in javascript handling.
*/
var ParseError = require("./ParseError");
var buildTree = require("./buildTree");
var parseTree = require("./parseTree");
var utils = require("./utils");
/**
* Parse and build an expression, and place that expression in the DOM node
* given.
*/
var process = function(toParse, baseNode) {
utils.clearNode(baseNode);
@ -13,6 +25,9 @@ var process = function(toParse, baseNode) {
baseNode.appendChild(node);
};
/**
* Parse and build an expression, and return the markup for that.
*/
var renderToString = function(toParse) {
var tree = parseTree(toParse);
return buildTree(tree).toMarkup();

View File

@ -1,6 +1,13 @@
/**
* Provides a single function for parsing an expression using a Parser
* TODO(emily): Remove this
*/
var Parser = require("./Parser");
// Parses the expression using the parser
/**
* Parses an expression using a Parser, then returns the parsed result.
*/
var parseTree = function(toParse) {
var parser = new Parser(toParse);

View File

@ -1,15 +1,18 @@
/* This file holds a list of all no-argument functions and single-character
* symbols (like 'a' or ';'). For each of the symbols, there are three
* properties they can have:
* - font (required): the font to be used for this * symbol. Either "main" (the
normal font), or "ams" (the ams fonts)
/**
* This file holds a list of all no-argument functions and single-character
* symbols (like 'a' or ';').
*
* For each of the symbols, there are three properties they can have:
* - font (required): the font to be used for this symbol. Either "main" (the
normal font), or "ams" (the ams fonts).
* - group (required): the ParseNode group type the symbol should have (i.e.
"textord" or "mathord" or
* - replace (optiona): the character that this symbol or function should be
"textord", "mathord", etc).
* - replace (optional): the character that this symbol or function should be
* replaced with (i.e. "\phi" has a replace value of "\u03d5", the phi
* character in the main font)
* There outermost map in the table indicates what mode the symbols should be
* accepted in (e.g. "math" or "text")
* character in the main font).
*
* The outermost map in the table indicates what mode the symbols should be
* accepted in (e.g. "math" or "text").
*/
var symbols = {
@ -854,6 +857,9 @@ var symbols = {
}
};
// There are lots of symbols which are the same, so we add them in afterwards.
// All of these are textords in math mode
var mathTextSymbols = "0123456789/@.\"";
for (var i = 0; i < mathTextSymbols.length; i++) {
var ch = mathTextSymbols.charAt(i);
@ -863,6 +869,7 @@ for (var i = 0; i < mathTextSymbols.length; i++) {
};
}
// All of these are textords in text mode
var textSymbols = "0123456789`!@*()-=+[]'\";:?/.,";
for (var i = 0; i < textSymbols.length; i++) {
var ch = textSymbols.charAt(i);
@ -872,6 +879,7 @@ for (var i = 0; i < textSymbols.length; i++) {
};
}
// All of these are textords in text mode, and mathords in math mode
var letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
for (var i = 0; i < letters.length; i++) {
var ch = letters.charAt(i);

View File

@ -1,3 +1,12 @@
/**
* This file contains a list of utility functions which are useful in other
* files.
*/
/**
* Provide an `indexOf` function which works in IE8, but defers to native if
* possible.
*/
var nativeIndexOf = Array.prototype.indexOf;
var indexOf = function(list, elem) {
if (list == null) {
@ -15,6 +24,9 @@ var indexOf = function(list, elem) {
return -1;
};
/**
* Return whether an element is contained in a list
*/
var contains = function(list, elem) {
return indexOf(list, elem) !== -1;
};
@ -50,8 +62,11 @@ function escape(text) {
return ('' + text).replace(ESCAPE_REGEX, escaper);
}
/**
* A function to set the text content of a DOM element in all supported
* browsers. Note that we don't define this if there is no document.
*/
var setTextContent;
if (typeof document !== "undefined") {
var testNode = document.createElement("span");
if ("textContent" in testNode) {
@ -65,6 +80,9 @@ if (typeof document !== "undefined") {
}
}
/**
* A function to clear a node.
*/
function clearNode(node) {
setTextContent(node, "");
}