From f63af87f17fedfd0c4cab753a3d4f7a0a5290bc0 Mon Sep 17 00:00:00 2001 From: Emily Eisenberg Date: Sun, 14 Sep 2014 19:23:39 -0700 Subject: [PATCH] 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 --- Lexer.js | 58 +++++- Options.js | 37 ++++ ParseError.js | 9 + Parser.js | 183 ++++++++++------- Style.js | 47 ++++- buildCommon.js | 43 +++- buildTree.js | 538 ++++++++++++++++++++++++++++++++++--------------- delimiter.js | 167 ++++++++++++--- domTree.js | 60 +++++- fontMetrics.js | 38 +++- katex.js | 15 ++ parseTree.js | 9 +- symbols.js | 28 ++- utils.js | 20 +- 14 files changed, 955 insertions(+), 297 deletions(-) diff --git a/Lexer.js b/Lexer.js index 0093fbe56..bee9db0ce 100644 --- a/Lexer.js +++ b/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); diff --git a/Options.js b/Options.js index ca33a30bd..00dcf4f67 100644 --- a/Options.js +++ b/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; }; diff --git a/ParseError.js b/ParseError.js index 402b61c50..91cbb1e0b 100644 --- a/ParseError.js +++ b/ParseError.js @@ -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; diff --git a/Parser.js b/Parser.js index 0f18fe4bc..1874da515 100644 --- a/Parser.js +++ b/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.} + */ 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); diff --git a/Style.js b/Style.js index 6851e1af4..c331d1f17 100644 --- a/Style.js +++ b/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], diff --git a/buildCommon.js b/buildCommon.js index c61baf383..8d98656f0 100644 --- a/buildCommon.js +++ b/buildCommon.js @@ -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 }; diff --git a/buildTree.js b/buildTree.js index 47bf6c620..d8e286895 100644 --- a/buildTree.js +++ b/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; diff --git a/delimiter.js b/delimiter.js index 928ccb72d..d5179f402 100644 --- a/delimiter.js +++ b/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); }; diff --git a/domTree.js b/domTree.js index a86f20eab..87193f30c 100644 --- a/domTree.js +++ b/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 = "