diff --git a/README.md b/README.md index 6c08f73a..9e7bf3a4 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,25 @@ katex.render("c = \\pm\\sqrt{a^2 + b^2}\\in\\RR", element, { Math on the page can be automatically rendered using the auto-render extension. See [the Auto-render README](contrib/auto-render/README.md) for more information. +#### Font size and lengths + +By default, KaTeX math is rendered in a 1.21× larger font than the surrounding +context, which makes super- and subscripts easier to read. You can control +this using CSS, for example: + +```css +.katex { font-size: 1.1em; } +``` + +KaTeX supports all TeX units, including absolute units like `cm` and `in`. +Absolute units are currently scaled relative to the default TeX font size of +10pt, so that `\kern1cm` produces the same results as `\kern2.845275em`. +As a result, relative and absolute units are both uniformly scaled relative +to LaTeX with a 10pt font; for example, the rectangle `\rule{1cm}{1em}` has +the same aspect ratio in KaTeX as in LaTeX. However, because most browsers +default to a larger font size, this typically means that a 1cm kern in KaTeX +will appear larger than 1cm in browser units. + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/src/Parser.js b/src/Parser.js index 2a7a8656..63b34cbd 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -4,6 +4,7 @@ import environments from "./environments"; import MacroExpander from "./MacroExpander"; import symbols from "./symbols"; import utils from "./utils"; +import units from "./units"; import { cjkRegex } from "./unicodeRegexes"; import ParseNode from "./ParseNode"; import ParseError from "./ParseError"; @@ -798,11 +799,11 @@ class Parser { number: +(match[1] + match[2]), // sign + magnitude, cast to number unit: match[3], }; - if (data.unit !== "em" && data.unit !== "ex" && data.unit !== "mu") { + if (!units.validUnit(data)) { throw new ParseError("Invalid unit: '" + data.unit + "'", res); } return new ParseFuncOrArgument( - new ParseNode("color", data, this.mode), + new ParseNode("size", data, this.mode), false); } diff --git a/src/buildHTML.js b/src/buildHTML.js index 3ed59b6b..6c90d87f 100644 --- a/src/buildHTML.js +++ b/src/buildHTML.js @@ -12,6 +12,7 @@ import Style from "./Style"; import buildCommon, { makeSpan } from "./buildCommon"; import delimiter from "./delimiter"; import domTree from "./domTree"; +import units from "./units"; import utils from "./utils"; import stretchy from "./stretchy"; @@ -545,43 +546,6 @@ groupTypes.genfrac = function(group, options) { options); }; -/** - * Parse a `sizeValue`, as parsed by functions.js argType "size", into - * a CSS em value. `options` gives the current options. - */ -const calculateSize = function(sizeValue, options) { - let scale; - // `mu` units scale with scriptstyle/scriptscriptstyle. - // Other units always refer to the *textstyle* font in the current size. - if (sizeValue.unit === "mu") { - scale = options.fontMetrics().cssEmPerMu; - } else { - let unitOptions; - if (options.style.isTight()) { - // isTight() means current style is script/scriptscript. - unitOptions = options.havingStyle(options.style.text()); - } else { - unitOptions = options; - } - // TODO: In TeX these units are relative to the quad of the current - // *text* font, e.g. cmr10. KaTeX instead uses values from the - // comparably-sized *Computer Modern symbol* font. At 10pt, these - // match. At 7pt and 5pt, they differ: cmr7=1.138894, cmsy7=1.170641; - // cmr5=1.361133, cmsy5=1.472241. Consider $\scriptsize a\kern1emb$. - // TeX \showlists shows a kern of 1.13889 * fontsize; - // KaTeX shows a kern of 1.171 * fontsize. - if (sizeValue.unit === "ex") { - scale = unitOptions.fontMetrics().xHeight; - } else { - scale = unitOptions.fontMetrics().quad; - } - if (unitOptions !== options) { - scale *= unitOptions.sizeMultiplier / options.sizeMultiplier; - } - } - return sizeValue.number * scale; -}; - groupTypes.array = function(group, options) { let r; let c; @@ -629,7 +593,7 @@ groupTypes.array = function(group, options) { let gap = 0; if (group.value.rowGaps[r]) { - gap = calculateSize(group.value.rowGaps[r].value, options); + gap = units.calculateSize(group.value.rowGaps[r].value, options); if (gap > 0) { // \@argarraycr gap += arstrutDepth; if (depth < gap) { @@ -1330,11 +1294,11 @@ groupTypes.rule = function(group, options) { // Calculate the shift, width, and height of the rule, and account for units let shift = 0; if (group.value.shift) { - shift = calculateSize(group.value.shift, options); + shift = units.calculateSize(group.value.shift, options); } - const width = calculateSize(group.value.width, options); - const height = calculateSize(group.value.height, options); + const width = units.calculateSize(group.value.width, options); + const height = units.calculateSize(group.value.height, options); // Style the rule to the right size rule.style.borderRightWidth = width + "em"; @@ -1358,7 +1322,7 @@ groupTypes.kern = function(group, options) { const rule = makeSpan(["mord", "rule"], [], options); if (group.value.dimension) { - const dimension = calculateSize(group.value.dimension, options); + const dimension = units.calculateSize(group.value.dimension, options); rule.style.marginLeft = dimension + "em"; } diff --git a/src/macros.js b/src/macros.js index dd6c9bad..9d172e85 100644 --- a/src/macros.js +++ b/src/macros.js @@ -15,6 +15,10 @@ defineMacro("\\egroup", "}"); defineMacro("\\begingroup", "{"); defineMacro("\\endgroup", "}"); +// We don't distinguish between math and nonmath kerns. +// (In TeX, the mu unit works only with \mkern.) +defineMacro("\\mkern", "\\kern"); + ////////////////////////////////////////////////////////////////////// // amsmath.sty diff --git a/src/units.js b/src/units.js new file mode 100644 index 00000000..f60550e9 --- /dev/null +++ b/src/units.js @@ -0,0 +1,99 @@ +/* eslint no-console:0 */ + +/** + * This file does conversion between units. In particular, it provides + * calculateSize to convert other units into ems. + */ + +import ParseError from "./ParseError"; + +// This table gives the number of TeX pts in one of each *absolute* TeX unit. +// Thus, multiplying a length by this number converts the length from units +// into pts. Dividing the result by ptPerEm gives the number of ems +// *assuming* a font size of ptPerEm (normal size, normal style). +const ptPerUnit = { + // https://en.wikibooks.org/wiki/LaTeX/Lengths and + // https://tex.stackexchange.com/a/8263 + "pt": 1, // TeX point + "mm": 7227 / 2540, // millimeter + "cm": 7227 / 254, // centimeter + "in": 72.27, // inch + "bp": 803 / 800, // big (PostScript) points + "pc": 12, // pica + "dd": 1238 / 1157, // didot + "cc": 14856 / 1157, // cicero (12 didot) + "nd": 685 / 642, // new didot + "nc": 1370 / 107, // new cicero (12 new didot) + "sp": 1 / 65536, // scaled point (TeX's internal smallest unit) + // https://tex.stackexchange.com/a/41371 + "px": 803 / 800, // \pdfpxdimen defaults to 1 bp in pdfTeX and LuaTeX +}; + +// Dictionary of relative units, for fast validity testing. +const relativeUnit = { + "ex": true, + "em": true, + "mu": true, +}; + +/** + * Determine whether the specified unit (either a string defining the unit + * or a "size" parse node containing a unit field) is valid. + */ +const validUnit = function(unit) { + if (unit.unit) { + unit = unit.unit; + } + return (unit in ptPerUnit || unit in relativeUnit || unit === "ex"); +}; + +/* + * Convert a "size" parse node (with numeric "number" and string "unit" fields, + * as parsed by functions.js argType "size") into a CSS em value for the + * current style/scale. `options` gives the current options. + */ +const calculateSize = function(sizeValue, options) { + let scale; + if (sizeValue.unit in ptPerUnit) { + // Absolute units + scale = ptPerUnit[sizeValue.unit] // Convert unit to pt + / options.fontMetrics().ptPerEm // Convert pt to CSS em + / options.sizeMultiplier; // Unscale to make absolute units + } else if (sizeValue.unit === "mu") { + // `mu` units scale with scriptstyle/scriptscriptstyle. + scale = options.fontMetrics().cssEmPerMu; + } else { + // Other relative units always refer to the *textstyle* font + // in the current size. + let unitOptions; + if (options.style.isTight()) { + // isTight() means current style is script/scriptscript. + unitOptions = options.havingStyle(options.style.text()); + } else { + unitOptions = options; + } + // TODO: In TeX these units are relative to the quad of the current + // *text* font, e.g. cmr10. KaTeX instead uses values from the + // comparably-sized *Computer Modern symbol* font. At 10pt, these + // match. At 7pt and 5pt, they differ: cmr7=1.138894, cmsy7=1.170641; + // cmr5=1.361133, cmsy5=1.472241. Consider $\scriptsize a\kern1emb$. + // TeX \showlists shows a kern of 1.13889 * fontsize; + // KaTeX shows a kern of 1.171 * fontsize. + if (sizeValue.unit === "ex") { + scale = unitOptions.fontMetrics().xHeight; + } else if (sizeValue.unit === "em") { + scale = unitOptions.fontMetrics().quad; + } else { + throw new ParseError("Invalid unit: '" + sizeValue.unit + "'"); + } + if (unitOptions !== options) { + scale *= unitOptions.sizeMultiplier / options.sizeMultiplier; + } + } + return sizeValue.number * scale; +}; + +module.exports = { + validUnit: validUnit, + calculateSize: calculateSize, +}; diff --git a/test/katex-spec.js b/test/katex-spec.js index 7b2238d2..f44a808a 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -931,7 +931,7 @@ describe("An overline parser", function() { describe("A rule parser", function() { const emRule = "\\rule{1em}{2em}"; const exRule = "\\rule{1ex}{2em}"; - const badUnitRule = "\\rule{1px}{2em}"; + const badUnitRule = "\\rule{1au}{2em}"; const noNumberRule = "\\rule{1em}{em}"; const incompleteRule = "\\rule{1em}"; const hardNumberRule = "\\rule{ 01.24ex}{2.450 em }"; @@ -988,7 +988,7 @@ describe("A kern parser", function() { const exKern = "\\kern{1ex}"; const muKern = "\\kern{1mu}"; const abKern = "a\\kern{1em}b"; - const badUnitRule = "\\kern{1px}"; + const badUnitRule = "\\kern{1au}"; const noNumberRule = "\\kern{em}"; it("should list the correct units", function() { @@ -1026,7 +1026,7 @@ describe("A non-braced kern parser", function() { const abKern1 = "a\\mkern1mub"; const abKern2 = "a\\kern-1mub"; const abKern3 = "a\\kern-1mu b"; - const badUnitRule = "\\kern1px"; + const badUnitRule = "\\kern1au"; const noNumberRule = "\\kern em"; it("should list the correct units", function() { diff --git a/test/screenshotter/images/Units-chrome.png b/test/screenshotter/images/Units-chrome.png new file mode 100644 index 00000000..e3ae8606 Binary files /dev/null and b/test/screenshotter/images/Units-chrome.png differ diff --git a/test/screenshotter/images/Units-firefox.png b/test/screenshotter/images/Units-firefox.png new file mode 100644 index 00000000..718fefab Binary files /dev/null and b/test/screenshotter/images/Units-firefox.png differ diff --git a/test/screenshotter/ss_data.yaml b/test/screenshotter/ss_data.yaml index 348d9849..bb2b195e 100644 --- a/test/screenshotter/ss_data.yaml +++ b/test/screenshotter/ss_data.yaml @@ -223,6 +223,21 @@ Symbols1: | Text: \frac{a}{b}\text{c~ {ab} \ e}+fg TextWithMath: \text{for $a < b$ and $ c < d $}. Unicode: \begin{matrix}\text{ÀàÇçÉéÏïÖöÛû} \\ \text{БГДЖЗЙЛФЦШЫЮЯ} \\ \text{여보세요} \\ \text{私はバナナです} \end{matrix} +Units: | + \begin{array}{ll} + \mathrm H\kern 1em\mathrm H \text{\tiny (1em)} + & \mathrm H\kern 1ex\mathrm H \text{\tiny (1ex)} \\ + \mathrm H{\scriptstyle \kern 1em}\mathrm H \text{\tiny (ss 1em)} + & \mathrm H{\scriptstyle \kern 1ex}\mathrm H \text{\tiny (ss 1ex)} \\ + \mathrm H{\small \kern 1em}\mathrm H \text{\tiny (sm 1em)} + & \mathrm H{\small \kern 1ex}\mathrm H \text{\tiny (sm 1ex)} \\ + \mathrm H\mkern 18mu\mathrm H \text{\tiny (18mu)} + & \mathrm H\kern 1cm\mathrm H \text{\tiny (1cm)} \\ + \mathrm H{\scriptstyle \mkern 18mu}\mathrm H \text{\tiny (ss 18mu)} + & \mathrm H{\scriptstyle \kern 1cm}\mathrm H \text{\tiny (ss 1cm)} \\ + \mathrm H{\small \mkern 18mu}\mathrm H \text{\tiny (sm 18mu)} + & \mathrm H{\small \kern 1cm}\mathrm H \text{\tiny (sm 1cm)} + \end{array} UnsupportedCmds: tex: \err\,\frac\fracerr3\,2^\superr_\suberr\,\sqrt\sqrterr noThrow: 1