UNPKG

8.97 kBJavaScriptView Raw
1// @flow
2import {defineFunctionBuilders} from "../defineFunction";
3import buildCommon from "../buildCommon";
4import {SymbolNode} from "../domTree";
5import mathMLTree from "../mathMLTree";
6import utils from "../utils";
7import Style from "../Style";
8import {checkNodeType} from "../parseNode";
9
10import * as html from "../buildHTML";
11import * as mml from "../buildMathML";
12import * as accent from "./accent";
13import * as horizBrace from "./horizBrace";
14import * as op from "./op";
15
16import type Options from "../Options";
17import type {ParseNode} from "../parseNode";
18import type {HtmlBuilder} from "../defineFunction";
19import type {MathNodeType} from "../mathMLTree";
20
21/**
22 * Sometimes, groups perform special rules when they have superscripts or
23 * subscripts attached to them. This function lets the `supsub` group know that
24 * Sometimes, groups perform special rules when they have superscripts or
25 * its inner element should handle the superscripts and subscripts instead of
26 * handling them itself.
27 */
28const htmlBuilderDelegate = function(
29 group: ParseNode<"supsub">,
30 options: Options,
31): ?HtmlBuilder<*> {
32 const base = group.base;
33 if (!base) {
34 return null;
35 } else if (base.type === "op") {
36 // Operators handle supsubs differently when they have limits
37 // (e.g. `\displaystyle\sum_2^3`)
38 const delegate = base.limits &&
39 (options.style.size === Style.DISPLAY.size ||
40 base.alwaysHandleSupSub);
41 return delegate ? op.htmlBuilder : null;
42 } else if (base.type === "accent") {
43 return utils.isCharacterBox(base.base) ? accent.htmlBuilder : null;
44 } else if (base.type === "horizBrace") {
45 const isSup = !group.sub;
46 return isSup === base.isOver ? horizBrace.htmlBuilder : null;
47 } else {
48 return null;
49 }
50};
51
52// Super scripts and subscripts, whose precise placement can depend on other
53// functions that precede them.
54defineFunctionBuilders({
55 type: "supsub",
56 htmlBuilder(group, options) {
57 // Superscript and subscripts are handled in the TeXbook on page
58 // 445-446, rules 18(a-f).
59
60 // Here is where we defer to the inner group if it should handle
61 // superscripts and subscripts itself.
62 const builderDelegate = htmlBuilderDelegate(group, options);
63 if (builderDelegate) {
64 return builderDelegate(group, options);
65 }
66
67 const {base: valueBase, sup: valueSup, sub: valueSub} = group;
68 const base = html.buildGroup(valueBase, options);
69 let supm;
70 let subm;
71
72 const metrics = options.fontMetrics();
73
74 // Rule 18a
75 let supShift = 0;
76 let subShift = 0;
77
78 const isCharacterBox = valueBase && utils.isCharacterBox(valueBase);
79 if (valueSup) {
80 const newOptions = options.havingStyle(options.style.sup());
81 supm = html.buildGroup(valueSup, newOptions, options);
82 if (!isCharacterBox) {
83 supShift = base.height - newOptions.fontMetrics().supDrop
84 * newOptions.sizeMultiplier / options.sizeMultiplier;
85 }
86 }
87
88 if (valueSub) {
89 const newOptions = options.havingStyle(options.style.sub());
90 subm = html.buildGroup(valueSub, newOptions, options);
91 if (!isCharacterBox) {
92 subShift = base.depth + newOptions.fontMetrics().subDrop
93 * newOptions.sizeMultiplier / options.sizeMultiplier;
94 }
95 }
96
97 // Rule 18c
98 let minSupShift;
99 if (options.style === Style.DISPLAY) {
100 minSupShift = metrics.sup1;
101 } else if (options.style.cramped) {
102 minSupShift = metrics.sup3;
103 } else {
104 minSupShift = metrics.sup2;
105 }
106
107 // scriptspace is a font-size-independent size, so scale it
108 // appropriately for use as the marginRight.
109 const multiplier = options.sizeMultiplier;
110 const marginRight = (0.5 / metrics.ptPerEm) / multiplier + "em";
111
112 let marginLeft = null;
113 if (subm) {
114 // Subscripts shouldn't be shifted by the base's italic correction.
115 // Account for that by shifting the subscript back the appropriate
116 // amount. Note we only do this when the base is a single symbol.
117 const isOiint =
118 group.base && group.base.type === "op" && group.base.name &&
119 (group.base.name === "\\oiint" || group.base.name === "\\oiiint");
120 if (base instanceof SymbolNode || isOiint) {
121 // $FlowFixMe
122 marginLeft = -base.italic + "em";
123 }
124 }
125
126 let supsub;
127 if (supm && subm) {
128 supShift = Math.max(
129 supShift, minSupShift, supm.depth + 0.25 * metrics.xHeight);
130 subShift = Math.max(subShift, metrics.sub2);
131
132 const ruleWidth = metrics.defaultRuleThickness;
133
134 // Rule 18e
135 const maxWidth = 4 * ruleWidth;
136 if ((supShift - supm.depth) - (subm.height - subShift) < maxWidth) {
137 subShift = maxWidth - (supShift - supm.depth) + subm.height;
138 const psi = 0.8 * metrics.xHeight - (supShift - supm.depth);
139 if (psi > 0) {
140 supShift += psi;
141 subShift -= psi;
142 }
143 }
144
145 const vlistElem = [
146 {type: "elem", elem: subm, shift: subShift, marginRight,
147 marginLeft},
148 {type: "elem", elem: supm, shift: -supShift, marginRight},
149 ];
150
151 supsub = buildCommon.makeVList({
152 positionType: "individualShift",
153 children: vlistElem,
154 }, options);
155 } else if (subm) {
156 // Rule 18b
157 subShift = Math.max(
158 subShift, metrics.sub1,
159 subm.height - 0.8 * metrics.xHeight);
160
161 const vlistElem =
162 [{type: "elem", elem: subm, marginLeft, marginRight}];
163
164 supsub = buildCommon.makeVList({
165 positionType: "shift",
166 positionData: subShift,
167 children: vlistElem,
168 }, options);
169 } else if (supm) {
170 // Rule 18c, d
171 supShift = Math.max(supShift, minSupShift,
172 supm.depth + 0.25 * metrics.xHeight);
173
174 supsub = buildCommon.makeVList({
175 positionType: "shift",
176 positionData: -supShift,
177 children: [{type: "elem", elem: supm, marginRight}],
178 }, options);
179 } else {
180 throw new Error("supsub must have either sup or sub.");
181 }
182
183 // Wrap the supsub vlist in a span.msupsub to reset text-align.
184 const mclass = html.getTypeOfDomTree(base, "right") || "mord";
185 return buildCommon.makeSpan([mclass],
186 [base, buildCommon.makeSpan(["msupsub"], [supsub])],
187 options);
188 },
189 mathmlBuilder(group, options) {
190 // Is the inner group a relevant horizonal brace?
191 let isBrace = false;
192 let isOver;
193 let isSup;
194
195 const horizBrace = checkNodeType(group.base, "horizBrace");
196 if (horizBrace) {
197 isSup = !!group.sup;
198 if (isSup === horizBrace.isOver) {
199 isBrace = true;
200 isOver = horizBrace.isOver;
201 }
202 }
203
204 if (group.base && group.base.type === "op") {
205 group.base.parentIsSupSub = true;
206 }
207
208 const children = [mml.buildGroup(group.base, options)];
209
210 if (group.sub) {
211 children.push(mml.buildGroup(group.sub, options));
212 }
213
214 if (group.sup) {
215 children.push(mml.buildGroup(group.sup, options));
216 }
217
218 let nodeType: MathNodeType;
219 if (isBrace) {
220 nodeType = (isOver ? "mover" : "munder");
221 } else if (!group.sub) {
222 const base = group.base;
223 if (base && base.type === "op" && base.limits &&
224 (options.style === Style.DISPLAY || base.alwaysHandleSupSub)) {
225 nodeType = "mover";
226 } else {
227 nodeType = "msup";
228 }
229 } else if (!group.sup) {
230 const base = group.base;
231 if (base && base.type === "op" && base.limits &&
232 (options.style === Style.DISPLAY || base.alwaysHandleSupSub)) {
233 nodeType = "munder";
234 } else {
235 nodeType = "msub";
236 }
237 } else {
238 const base = group.base;
239 if (base && base.type === "op" && base.limits &&
240 options.style === Style.DISPLAY) {
241 nodeType = "munderover";
242 } else {
243 nodeType = "msubsup";
244 }
245 }
246
247 const node = new mathMLTree.MathNode(nodeType, children);
248
249 return node;
250 },
251});
252