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:
parent
79a5687057
commit
f63af87f17
58
Lexer.js
58
Lexer.js
|
@ -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);
|
||||
|
|
37
Options.js
37
Options.js
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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
183
Parser.js
|
@ -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);
|
||||
|
||||
|
|
47
Style.js
47
Style.js
|
@ -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],
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
538
buildTree.js
538
buildTree.js
|
@ -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;
|
||||
|
|
167
delimiter.js
167
delimiter.js
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
60
domTree.js
60
domTree.js
|
@ -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
15
katex.js
15
katex.js
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
28
symbols.js
28
symbols.js
|
@ -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);
|
||||
|
|
20
utils.js
20
utils.js
|
@ -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, "");
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user