diff --git a/Parser.js b/Parser.js index 6a3d09776..f24c2d289 100644 --- a/Parser.js +++ b/Parser.js @@ -535,6 +535,19 @@ Parser.prototype.parseNucleus = function(pos, mode) { this.lexer, nucleus.position ); } + } else if (mode === "math" && nucleus.type === "\\sqrt") { + // If this is a square root, parse its argument and return + var group = this.parseGroup(nucleus.position, mode); + if (group) { + return new ParseResult( + new ParseNode("sqrt", group, mode), + group.position); + } else { + throw new ParseError("Expected group after '" + + nucleus.type + "'", + this.lexer, nucleus.position + ); + } } else if (mode === "math" && nucleus.type === "\\rule") { // Parse the width of the rule var widthGroup = this.parseSizeGroup(nucleus.position, mode); diff --git a/buildTree.js b/buildTree.js index a523548ed..4d7a6cc44 100644 --- a/buildTree.js +++ b/buildTree.js @@ -38,7 +38,8 @@ var groupToType = { katex: "mord", overline: "mord", rule: "mord", - leftright: "minner" + leftright: "minner", + sqrt: "mord" }; var getTypeOfGroup = function(group) { @@ -429,6 +430,68 @@ var groupTypes = { ["katex-logo"], [k, a, t, e, x], options.getColor()); }, + sqrt: function(group, options, prev) { + var innerGroup = buildGroup(group.value.result, + options.withStyle(options.style.cramp())); + + var fontSizer = buildCommon.makeFontSizer( + options, Math.max(innerGroup.maxFontSize, 1.0)); + + // The theta variable in the TeXbook + var lineWidth = fontMetrics.metrics.defaultRuleThickness; + + var lineInner = + makeSpan([options.style.reset(), Style.TEXT.cls(), "line"]); + lineInner.maxFontSize = 1.0; + var line = makeSpan(["sqrt-line"], [fontSizer, lineInner]); + + var inner = makeSpan(["sqrt-inner"], [fontSizer, innerGroup]); + var fixIE = makeSpan( + ["fix-ie"], [fontSizer, new domTree.textNode("\u00a0")]); + + var theta = fontMetrics.metrics.defaultRuleThickness / + options.style.sizeMultiplier; + var phi = theta; + if (options.style.id < Style.TEXT.id) { + phi = fontMetrics.metrics.xHeight; + } + + var psi = theta + phi / 4; + + var innerHeight = + (inner.height + inner.depth) * options.style.sizeMultiplier; + var minDelimiterHeight = innerHeight + psi + theta; + + var delim = makeSpan(["sqrt-sign"], [ + delimiter.customSizedDelim("\\surd", minDelimiterHeight, + false, options, group.mode)]); + + var delimDepth = delim.height + delim.depth; + + if (delimDepth > inner.height + inner.depth + psi) { + psi = (psi + delimDepth - inner.height - inner.depth) / 2; + } + + delim.style.top = (-inner.height - psi + delim.height - theta) + "em"; + + line.style.top = (-inner.height - psi) + "em"; + line.height = inner.height + psi + 2 * theta; + + // We add a special case here, because even when `inner` is empty, we + // still get a line. So, we use a simple heuristic to decide if we + // should omit the body entirely. (note this doesn't work for something + // like `\sqrt{\rlap{x}}`, but if someone is doing that they deserve for + // it not to work. + var body; + if (inner.height === 0 && inner.depth === 0) { + body = makeSpan(); + } else { + body = makeSpan(["sqrt-body"], [line, inner, fixIE]); + } + + return makeSpan(["sqrt", "mord"], [delim, body]); + }, + overline: function(group, options, prev) { var innerGroup = buildGroup(group.value.result, options.withStyle(options.style.cramp())); diff --git a/delimiter.js b/delimiter.js index de5dda085..b5f786eaa 100644 --- a/delimiter.js +++ b/delimiter.js @@ -191,6 +191,12 @@ var makeStackedDelim = function(delim, heightTotal, center, options, mode) { bottom = "\u23ad"; repeat = "\u23aa"; font = "Size4-Regular"; + } else if (delim === "\\surd") { + top = "\ue001"; + bottom = "\u23b7"; + repeat = "\ue000"; + font = "Size4-Regular"; + overlap = true; } // Get the metrics of the three sections @@ -312,7 +318,8 @@ var normalDelimiters = [ "(", ")", "[", "\\lbrack", "]", "\\rbrack", "\\{", "\\lbrace", "\\}", "\\rbrace", "\\lfloor", "\\rfloor", "\\lceil", "\\rceil", - "<", ">", "\\langle", "\\rangle", "/", "\\backslash" + "<", ">", "\\langle", "\\rangle", "/", "\\backslash", + "\\surd" ]; var stackDelimiters = [ diff --git a/static/katex.less b/static/katex.less index df8bc8b37..3ad5e52ab 100644 --- a/static/katex.less +++ b/static/katex.less @@ -350,6 +350,46 @@ big parens } } + .sqrt { + > .sqrt-sign { + position: relative; + } + + > .sqrt-body { + .baseline-align-hack-outer; + + > .sqrt-line, + > .sqrt-inner, + > .fix-ie { + .baseline-align-hack-middle; + position: relative; + + > span { + .baseline-align-hack-inner; + } + } + + > .sqrt-line > .line { + width: 100%; + + &:before { + border-bottom-style: solid; + border-bottom-width: 1px; + content: ""; + display: block; + } + + &:after { + border-bottom-style: solid; + border-bottom-width: 0.04em; + content: ""; + display: block; + margin-top: -1px; + } + } + } + } + .sizing, .fontsize-ensurer { display: inline-block; diff --git a/symbols.js b/symbols.js index 41631e73f..377dafa5e 100644 --- a/symbols.js +++ b/symbols.js @@ -298,6 +298,11 @@ var symbols = { group: "bin", replace: "\u00d7" }, + "\\surd": { + font: "main", + group: "textord", + replace: "\u221a" + }, "(": { font: "main", group: "open" diff --git a/test/huxley/Huxleyfile.json b/test/huxley/Huxleyfile.json index 0fe460402..18b1ca780 100644 --- a/test/huxley/Huxleyfile.json +++ b/test/huxley/Huxleyfile.json @@ -153,5 +153,11 @@ "name": "NullDelimiterInteraction", "screenSize": [1024, 768], "url": "http://localhost:7936/test/huxley/test.html?m=a \\bigl. + 2 \\quad \\left. + a \\right)" + }, + + { + "name": "Sqrt", + "screenSize": [1024, 768], + "url": "http://localhost:7936/test/huxley/test.html?m=\\sqrt{\\sqrt{\\sqrt{x}}}_{\\sqrt{\\sqrt{x}}}^{\\sqrt{\\sqrt{\\sqrt{x}}}^{\\sqrt{\\sqrt{\\sqrt{x}}}}}" } ] diff --git a/test/huxley/Sqrt.hux/firefox-1.png b/test/huxley/Sqrt.hux/firefox-1.png new file mode 100644 index 000000000..d22c3229b Binary files /dev/null and b/test/huxley/Sqrt.hux/firefox-1.png differ diff --git a/test/huxley/Sqrt.hux/record.json b/test/huxley/Sqrt.hux/record.json new file mode 100644 index 000000000..3cae6ac65 --- /dev/null +++ b/test/huxley/Sqrt.hux/record.json @@ -0,0 +1,5 @@ +[ + { + "action": "screenshot" + } +] diff --git a/test/katex-tests.js b/test/katex-tests.js index 92bbf9911..a804f80fe 100644 --- a/test/katex-tests.js +++ b/test/katex-tests.js @@ -798,3 +798,26 @@ describe("A left/right parser", function() { }).not.toThrow(); }); }); + +describe("A sqrt parser", function() { + var sqrt = "\\sqrt{x}"; + var missingGroup = "\\sqrt"; + + it("should parse square roots", function() { + expect(function() { + parseTree(sqrt); + }).not.toThrow(); + }); + + it("should error when there is no group", function() { + expect(function() { + parseTree(missingGroup); + }).toThrow(); + }); + + it("should produce sqrts", function() { + var parse = parseTree(sqrt)[0]; + + expect(parse.type).toMatch("sqrt"); + }); +});