From 42cc5e507e9300ad03969a50299c0458b7862056 Mon Sep 17 00:00:00 2001 From: Ron Kok Date: Wed, 12 Aug 2020 21:08:36 -0700 Subject: [PATCH] feat: Support \phase (#2406) * Support \phase * Update screenshots * Update documentation * Add a11y test * Edit MathML notation to phasorangle * Move code to enclose.js Co-authored-by: Kevin Barabash --- .../render-a11y-string/render-a11y-string.js | 7 +++ .../test/render-a11y-string-spec.js | 9 ++++ docs/support_table.md | 2 +- docs/supported.md | 1 + src/domTree.js | 2 +- src/functions/enclose.js | 44 ++++++++++++++++-- src/svgGeometry.js | 5 ++ test/katex-spec.js | 9 ++++ test/screenshotter/images/Phase-chrome.png | Bin 0 -> 11842 bytes test/screenshotter/images/Phase-firefox.png | Bin 0 -> 11808 bytes test/screenshotter/ss_data.yaml | 1 + 11 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 test/screenshotter/images/Phase-chrome.png create mode 100644 test/screenshotter/images/Phase-firefox.png diff --git a/contrib/render-a11y-string/render-a11y-string.js b/contrib/render-a11y-string/render-a11y-string.js index dd9591c6..b04c4eaf 100644 --- a/contrib/render-a11y-string/render-a11y-string.js +++ b/contrib/render-a11y-string/render-a11y-string.js @@ -534,6 +534,13 @@ const handleObject = ( regionStrings.push("end strikeout"); }); break; + } else if (/phase/.test(tree.label)) { + buildRegion(a11yStrings, function(regionStrings) { + regionStrings.push("start phase angle"); + buildA11yStrings(tree.body, regionStrings, atomType); + regionStrings.push("end phase angle"); + }); + break; } throw new Error( `KaTeX-a11y: enclose node with ${tree.label} not supported yet`); diff --git a/contrib/render-a11y-string/test/render-a11y-string-spec.js b/contrib/render-a11y-string/test/render-a11y-string-spec.js index 0929e6fc..7b547441 100644 --- a/contrib/render-a11y-string/test/render-a11y-string-spec.js +++ b/contrib/render-a11y-string/test/render-a11y-string-spec.js @@ -151,6 +151,15 @@ describe("renderA11yString", () => { }); }); + describe("phase", () => { + test("\\phase", () => { + const result = renderA11yString("\\phase{a}"); + expect(result).toMatchInlineSnapshot( + `"start phase angle, a, end phase angle"`, + ); + }); + }); + describe("exponents", () => { test("simple exponent", () => { const result = renderA11yString("e^x"); diff --git a/docs/support_table.md b/docs/support_table.md index 6a7f137b..b5636b31 100644 --- a/docs/support_table.md +++ b/docs/support_table.md @@ -816,7 +816,7 @@ use `\ce` instead| |\partial|$\partial$|| |\perp|$\perp$|| |\phantom|$\Gamma^{\phantom{i}j}_{i\phantom{j}k}$|`\Gamma^{\phantom{i}j}_{i\phantom{j}k}`| -|\phase|Not supported|| +|\phase|$\phase{-78^\circ}$|`\phase{-78^\circ}`| |\Phi|$\Phi$|| |\phi|$\phi$|| |\Pi|$\Pi$|| diff --git a/docs/supported.md b/docs/supported.md index e5bdc145..0a4b96bb 100644 --- a/docs/supported.md +++ b/docs/supported.md @@ -212,6 +212,7 @@ For Persian composite characters, a user-suppliedĀ [plug-in](https://github.com/ |$\xcancel{ABC}$ `\xcancel{ABC}`|$\not =$ `\not =` |$\sout{abc}$ `\sout{abc}`|$\boxed{\pi=\frac c d}$ `\boxed{\pi=\frac c d}` |$a_{\angl n}$ `$a_{\angl n}`|$a_\angln$ `a_\angln` +|$\phase{-78^\circ}$`\phase{-78^\circ}` | `\tag{hi} x+y^{2x}` $$\tag{hi} x+y^{2x}$$ diff --git a/src/domTree.js b/src/domTree.js index 580cf1a7..9439bb14 100644 --- a/src/domTree.js +++ b/src/domTree.js @@ -533,7 +533,7 @@ export class PathNode implements VirtualNode { constructor(pathName: string, alternate?: string) { this.pathName = pathName; - this.alternate = alternate; // Used only for \sqrt + this.alternate = alternate; // Used only for \sqrt and \phase } toNode(): Node { diff --git a/src/functions/enclose.js b/src/functions/enclose.js index 1f70fa4f..2eb8ecd5 100644 --- a/src/functions/enclose.js +++ b/src/functions/enclose.js @@ -4,6 +4,9 @@ import buildCommon from "../buildCommon"; import mathMLTree from "../mathMLTree"; import utils from "../utils"; import stretchy from "../stretchy"; +import {phasePath} from "../svgGeometry"; +import {PathNode, SvgNode} from "../domTree"; +import {calculateSize} from "../units"; import {assertNodeType} from "../parseNode"; import * as html from "../buildHTML"; @@ -11,14 +14,14 @@ import * as mml from "../buildMathML"; const htmlBuilder = (group, options) => { - // \cancel, \bcancel, \xcancel, \sout, \fbox, \colorbox, \fcolorbox + // \cancel, \bcancel, \xcancel, \sout, \fbox, \colorbox, \fcolorbox, \phase // Some groups can return document fragments. Handle those by wrapping // them in a span. const inner = buildCommon.wrapFragment( html.buildGroup(group.body, options), options); const label = group.label.substr(1); - const scale = options.sizeMultiplier; + let scale = options.sizeMultiplier; let img; let imgShift = 0; @@ -34,6 +37,33 @@ const htmlBuilder = (group, options) => { img.height = options.fontMetrics().defaultRuleThickness / scale; imgShift = -0.5 * options.fontMetrics().xHeight; + } else if (label === "phase") { + // Set a couple of dimensions from the steinmetz package. + const lineWeight = calculateSize({number: 0.6, unit: "pt"}, options); + const clearance = calculateSize({number: 0.35, unit: "ex"}, options); + + // Prevent size changes like \Huge from affecting line thickness + const newOptions = options.havingBaseSizing(); + scale = scale / newOptions.sizeMultiplier; + + const angleHeight = inner.height + inner.depth + lineWeight + clearance; + // Reserve a left pad for the angle. + inner.style.paddingLeft = (angleHeight / 2 + lineWeight) + "em"; + + // Create an SVG + const viewBoxHeight = Math.floor(1000 * angleHeight * scale); + const path = phasePath(viewBoxHeight); + const svgNode = new SvgNode([new PathNode("phase", path)], { + "width": "400em", + "height": `${viewBoxHeight / 1000}em`, + "viewBox": `0 0 400000 ${viewBoxHeight}`, + "preserveAspectRatio": "xMinYMin slice", + }); + // Wrap it in a span with overflow: hidden. + img = buildCommon.makeSvgSpan(["hide-tail"], [svgNode], options); + img.style.height = angleHeight + "em"; + imgShift = inner.depth + lineWeight + clearance; + } else { // Add horizontal padding if (/cancel/.test(label)) { @@ -100,6 +130,7 @@ const htmlBuilder = (group, options) => { ], }, options); } else { + const classes = /cancel|phase/.test(label) ? ["svg-align"] : []; vlist = buildCommon.makeVList({ positionType: "individualShift", children: [ @@ -113,7 +144,7 @@ const htmlBuilder = (group, options) => { type: "elem", elem: img, shift: imgShift, - wrapperClasses: /cancel/.test(label) ? ["svg-align"] : [], + wrapperClasses: classes, }, ], }, options); @@ -147,6 +178,9 @@ const mathmlBuilder = (group, options) => { case "\\bcancel": node.setAttribute("notation", "downdiagonalstrike"); break; + case "\\phase": + node.setAttribute("notation", "phasorangle"); + break; case "\\sout": node.setAttribute("notation", "horizontalstrike"); break; @@ -255,11 +289,11 @@ defineFunction({ defineFunction({ type: "enclose", - names: ["\\cancel", "\\bcancel", "\\xcancel", "\\sout"], + names: ["\\cancel", "\\bcancel", "\\xcancel", "\\sout", "\\phase"], props: { numArgs: 1, }, - handler({parser, funcName}, args, optArgs) { + handler({parser, funcName}, args) { const body = args[0]; return { type: "enclose", diff --git a/src/svgGeometry.js b/src/svgGeometry.js index 65fed9bb..00018e2b 100644 --- a/src/svgGeometry.js +++ b/src/svgGeometry.js @@ -98,6 +98,11 @@ s76,-153,76,-153s77,-151,77,-151c0.7,0.7,35.7,202,105,604c67.3,400.7,102,602.7,1 606zM${1001 + extraViniculum} ${hLinePad}h400000v${40 + extraViniculum}H1017.7z`; }; +export const phasePath = function(y: number): string { + const x = y / 2; // x coordinate at top of angle + return `M400000 ${y} H0 L${x} 0 l65 45 L145 ${y - 80} H400000z`; +}; + const sqrtTall = function( extraViniculum: number, hLinePad: number, diff --git a/test/katex-spec.js b/test/katex-spec.js index 451df860..f986cb38 100644 --- a/test/katex-spec.js +++ b/test/katex-spec.js @@ -2479,6 +2479,15 @@ describe("A actuarial angle builder", function() { }); }); +describe("\\phase", function() { + it("should fail in text mode", function() { + expect`\text{\phase{-78.2^\circ}}`.not.toParse(); + }); + it("should not fail in math mode", function() { + expect`\phase{-78.2^\circ}`.toBuild(); + }); +}); + describe("A phantom parser", function() { it("should not fail", function() { expect`\phantom{x}`.toParse(); diff --git a/test/screenshotter/images/Phase-chrome.png b/test/screenshotter/images/Phase-chrome.png new file mode 100644 index 0000000000000000000000000000000000000000..6ea60033e110ae014f7c34eda414fbc99aa1c3a3 GIT binary patch literal 11842 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 IbxsLQ00%;s;s5{u literal 0 HcmV?d00001 diff --git a/test/screenshotter/images/Phase-firefox.png b/test/screenshotter/images/Phase-firefox.png new file mode 100644 index 0000000000000000000000000000000000000000..79e77fe72a504d8d7d204fd42dc18acef0b2989c GIT binary patch literal 11808 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