UNPKG

9.65 kBJavaScriptView Raw
1// @flow
2import defineFunction from "../defineFunction";
3import buildCommon from "../buildCommon";
4import mathMLTree from "../mathMLTree";
5import utils from "../utils";
6import stretchy from "../stretchy";
7import {assertNodeType, checkNodeType} from "../parseNode";
8import {assertSpan, assertSymbolDomNode} from "../domTree";
9
10import * as html from "../buildHTML";
11import * as mml from "../buildMathML";
12
13import type {ParseNode, AnyParseNode} from "../parseNode";
14import type {HtmlBuilderSupSub, MathMLBuilder} from "../defineFunction";
15
16// NOTE: Unlike most `htmlBuilder`s, this one handles not only "accent", but
17// also "supsub" since an accent can affect super/subscripting.
18export const htmlBuilder: HtmlBuilderSupSub<"accent"> = (grp, options) => {
19 // Accents are handled in the TeXbook pg. 443, rule 12.
20 let base: AnyParseNode;
21 let group: ParseNode<"accent">;
22
23 const supSub: ?ParseNode<"supsub"> = checkNodeType(grp, "supsub");
24 let supSubGroup;
25 if (supSub) {
26 // If our base is a character box, and we have superscripts and
27 // subscripts, the supsub will defer to us. In particular, we want
28 // to attach the superscripts and subscripts to the inner body (so
29 // that the position of the superscripts and subscripts won't be
30 // affected by the height of the accent). We accomplish this by
31 // sticking the base of the accent into the base of the supsub, and
32 // rendering that, while keeping track of where the accent is.
33
34 // The real accent group is the base of the supsub group
35 group = assertNodeType(supSub.base, "accent");
36 // The character box is the base of the accent group
37 base = group.base;
38 // Stick the character box into the base of the supsub group
39 supSub.base = base;
40
41 // Rerender the supsub group with its new base, and store that
42 // result.
43 supSubGroup = assertSpan(html.buildGroup(supSub, options));
44
45 // reset original base
46 supSub.base = group;
47 } else {
48 group = assertNodeType(grp, "accent");
49 base = group.base;
50 }
51
52 // Build the base group
53 const body = html.buildGroup(base, options.havingCrampedStyle());
54
55 // Does the accent need to shift for the skew of a character?
56 const mustShift = group.isShifty && utils.isCharacterBox(base);
57
58 // Calculate the skew of the accent. This is based on the line "If the
59 // nucleus is not a single character, let s = 0; otherwise set s to the
60 // kern amount for the nucleus followed by the \skewchar of its font."
61 // Note that our skew metrics are just the kern between each character
62 // and the skewchar.
63 let skew = 0;
64 if (mustShift) {
65 // If the base is a character box, then we want the skew of the
66 // innermost character. To do that, we find the innermost character:
67 const baseChar = utils.getBaseElem(base);
68 // Then, we render its group to get the symbol inside it
69 const baseGroup = html.buildGroup(baseChar, options.havingCrampedStyle());
70 // Finally, we pull the skew off of the symbol.
71 skew = assertSymbolDomNode(baseGroup).skew;
72 // Note that we now throw away baseGroup, because the layers we
73 // removed with getBaseElem might contain things like \color which
74 // we can't get rid of.
75 // TODO(emily): Find a better way to get the skew
76 }
77
78 // calculate the amount of space between the body and the accent
79 let clearance = Math.min(
80 body.height,
81 options.fontMetrics().xHeight);
82
83 // Build the accent
84 let accentBody;
85 if (!group.isStretchy) {
86 let accent;
87 let width: number;
88 if (group.label === "\\vec") {
89 // Before version 0.9, \vec used the combining font glyph U+20D7.
90 // But browsers, especially Safari, are not consistent in how they
91 // render combining characters when not preceded by a character.
92 // So now we use an SVG.
93 // If Safari reforms, we should consider reverting to the glyph.
94 accent = buildCommon.staticSvg("vec", options);
95 width = buildCommon.svgData.vec[1];
96 } else {
97 accent = buildCommon.makeSymbol(
98 group.label, "Main-Regular", group.mode, options);
99 // Remove the italic correction of the accent, because it only serves to
100 // shift the accent over to a place we don't want.
101 accent.italic = 0;
102 width = accent.width;
103 }
104
105 accentBody = buildCommon.makeSpan(["accent-body"], [accent]);
106
107 // "Full" accents expand the width of the resulting symbol to be
108 // at least the width of the accent, and overlap directly onto the
109 // character without any vertical offset.
110 const accentFull = (group.label === "\\textcircled");
111 if (accentFull) {
112 accentBody.classes.push('accent-full');
113 clearance = body.height;
114 }
115
116 // Shift the accent over by the skew.
117 let left = skew;
118
119 // CSS defines `.katex .accent .accent-body:not(.accent-full) { width: 0 }`
120 // so that the accent doesn't contribute to the bounding box.
121 // We need to shift the character by its width (effectively half
122 // its width) to compensate.
123 if (!accentFull) {
124 left -= width / 2;
125 }
126
127 accentBody.style.left = left + "em";
128
129 // \textcircled uses the \bigcirc glyph, so it needs some
130 // vertical adjustment to match LaTeX.
131 if (group.label === "\\textcircled") {
132 accentBody.style.top = ".2em";
133 }
134
135 accentBody = buildCommon.makeVList({
136 positionType: "firstBaseline",
137 children: [
138 {type: "elem", elem: body},
139 {type: "kern", size: -clearance},
140 {type: "elem", elem: accentBody},
141 ],
142 }, options);
143
144 } else {
145 accentBody = stretchy.svgSpan(group, options);
146
147 accentBody = buildCommon.makeVList({
148 positionType: "firstBaseline",
149 children: [
150 {type: "elem", elem: body},
151 {
152 type: "elem",
153 elem: accentBody,
154 wrapperClasses: ["svg-align"],
155 wrapperStyle: skew > 0
156 ? {
157 width: `calc(100% - ${2 * skew}em)`,
158 marginLeft: `${(2 * skew)}em`,
159 }
160 : undefined,
161 },
162 ],
163 }, options);
164 }
165
166 const accentWrap =
167 buildCommon.makeSpan(["mord", "accent"], [accentBody], options);
168
169 if (supSubGroup) {
170 // Here, we replace the "base" child of the supsub with our newly
171 // generated accent.
172 supSubGroup.children[0] = accentWrap;
173
174 // Since we don't rerun the height calculation after replacing the
175 // accent, we manually recalculate height.
176 supSubGroup.height = Math.max(accentWrap.height, supSubGroup.height);
177
178 // Accents should always be ords, even when their innards are not.
179 supSubGroup.classes[0] = "mord";
180
181 return supSubGroup;
182 } else {
183 return accentWrap;
184 }
185};
186
187const mathmlBuilder: MathMLBuilder<"accent"> = (group, options) => {
188 const accentNode =
189 group.isStretchy ?
190 stretchy.mathMLnode(group.label) :
191 new mathMLTree.MathNode("mo", [mml.makeText(group.label, group.mode)]);
192
193 const node = new mathMLTree.MathNode(
194 "mover",
195 [mml.buildGroup(group.base, options), accentNode]);
196
197 node.setAttribute("accent", "true");
198
199 return node;
200};
201
202const NON_STRETCHY_ACCENT_REGEX = new RegExp([
203 "\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
204 "\\check", "\\hat", "\\vec", "\\dot", "\\mathring",
205].map(accent => `\\${accent}`).join("|"));
206
207// Accents
208defineFunction({
209 type: "accent",
210 names: [
211 "\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
212 "\\check", "\\hat", "\\vec", "\\dot", "\\mathring", "\\widecheck",
213 "\\widehat", "\\widetilde", "\\overrightarrow", "\\overleftarrow",
214 "\\Overrightarrow", "\\overleftrightarrow", "\\overgroup",
215 "\\overlinesegment", "\\overleftharpoon", "\\overrightharpoon",
216 ],
217 props: {
218 numArgs: 1,
219 },
220 handler: (context, args) => {
221 const base = args[0];
222
223 const isStretchy = !NON_STRETCHY_ACCENT_REGEX.test(context.funcName);
224 const isShifty = !isStretchy ||
225 context.funcName === "\\widehat" ||
226 context.funcName === "\\widetilde" ||
227 context.funcName === "\\widecheck";
228
229 return {
230 type: "accent",
231 mode: context.parser.mode,
232 label: context.funcName,
233 isStretchy: isStretchy,
234 isShifty: isShifty,
235 base: base,
236 };
237 },
238 htmlBuilder,
239 mathmlBuilder,
240});
241
242// Text-mode accents
243defineFunction({
244 type: "accent",
245 names: [
246 "\\'", "\\`", "\\^", "\\~", "\\=", "\\u", "\\.", '\\"',
247 "\\r", "\\H", "\\v", "\\textcircled",
248 ],
249 props: {
250 numArgs: 1,
251 allowedInText: true,
252 allowedInMath: false,
253 },
254 handler: (context, args) => {
255 const base = args[0];
256
257 return {
258 type: "accent",
259 mode: context.parser.mode,
260 label: context.funcName,
261 isStretchy: false,
262 isShifty: true,
263 base: base,
264 };
265 },
266 htmlBuilder,
267 mathmlBuilder,
268});