mirror of
https://github.com/Smaug123/KaTeX
synced 2025-10-11 22:18:41 +00:00
extract accent and accentunder into their own files (#1048)
* extract accent and accentunder into their own files * cleanup how we apply custom styles to accentBody wrappers * address feedback from code review * fix typo * define a flow type for a CSS style * clone 'style' object passed to methods in domTree * forgot that we don't support object spread yet
This commit is contained in:
@@ -15,7 +15,7 @@ import type Options from "./Options";
|
|||||||
import type ParseNode from "./ParseNode";
|
import type ParseNode from "./ParseNode";
|
||||||
import type {CharacterMetrics} from "./fontMetrics";
|
import type {CharacterMetrics} from "./fontMetrics";
|
||||||
import type {Mode} from "./types";
|
import type {Mode} from "./types";
|
||||||
import type {DomChildNode, CombinableDomNode} from "./domTree";
|
import type {DomChildNode, CombinableDomNode, CssStyle} from "./domTree";
|
||||||
|
|
||||||
// The following have to be loaded from Main-Italic font, using class mainit
|
// The following have to be loaded from Main-Italic font, using class mainit
|
||||||
const mainitLetters = [
|
const mainitLetters = [
|
||||||
@@ -318,8 +318,9 @@ const makeSpan = function(
|
|||||||
classes?: string[],
|
classes?: string[],
|
||||||
children?: DomChildNode[],
|
children?: DomChildNode[],
|
||||||
options?: Options,
|
options?: Options,
|
||||||
|
style?: CssStyle,
|
||||||
): domTree.span {
|
): domTree.span {
|
||||||
const span = new domTree.span(classes, children, options);
|
const span = new domTree.span(classes, children, options, style);
|
||||||
|
|
||||||
sizeElementFromChildren(span);
|
sizeElementFromChildren(span);
|
||||||
|
|
||||||
@@ -392,6 +393,7 @@ type VListElem = {|
|
|||||||
marginLeft?: string,
|
marginLeft?: string,
|
||||||
marginRight?: string,
|
marginRight?: string,
|
||||||
wrapperClasses?: string[],
|
wrapperClasses?: string[],
|
||||||
|
wrapperStyle?: CssStyle,
|
||||||
|};
|
|};
|
||||||
type VListElemAndShift = {|
|
type VListElemAndShift = {|
|
||||||
type: "elem",
|
type: "elem",
|
||||||
@@ -400,6 +402,7 @@ type VListElemAndShift = {|
|
|||||||
marginLeft?: string,
|
marginLeft?: string,
|
||||||
marginRight?: string,
|
marginRight?: string,
|
||||||
wrapperClasses?: string[],
|
wrapperClasses?: string[],
|
||||||
|
wrapperStyle?: CssStyle,
|
||||||
|};
|
|};
|
||||||
type VListKern = {| type: "kern", size: number |};
|
type VListKern = {| type: "kern", size: number |};
|
||||||
|
|
||||||
@@ -530,8 +533,9 @@ const makeVList = function(params: VListParam, options: Options): domTree.span {
|
|||||||
} else {
|
} else {
|
||||||
const elem = child.elem;
|
const elem = child.elem;
|
||||||
const classes = child.wrapperClasses || [];
|
const classes = child.wrapperClasses || [];
|
||||||
|
const style = child.wrapperStyle || {};
|
||||||
|
|
||||||
const childWrap = makeSpan(classes, [pstrut, elem]);
|
const childWrap = makeSpan(classes, [pstrut, elem], undefined, style);
|
||||||
childWrap.style.top = (-pstrutSize - currPos - elem.depth) + "em";
|
childWrap.style.top = (-pstrutSize - currPos - elem.depth) + "em";
|
||||||
if (child.marginLeft) {
|
if (child.marginLeft) {
|
||||||
childWrap.style.marginLeft = child.marginLeft;
|
childWrap.style.marginLeft = child.marginLeft;
|
||||||
@@ -762,6 +766,7 @@ export default {
|
|||||||
makeOrd,
|
makeOrd,
|
||||||
makeVerb,
|
makeVerb,
|
||||||
staticSvg,
|
staticSvg,
|
||||||
|
svgData,
|
||||||
tryCombineChars,
|
tryCombineChars,
|
||||||
prependChildren,
|
prependChildren,
|
||||||
spacingFunctions,
|
spacingFunctions,
|
||||||
|
172
src/buildHTML.js
172
src/buildHTML.js
@@ -459,155 +459,6 @@ groupTypes.font = function(group, options) {
|
|||||||
return buildGroup(group.value.body, options.withFontFamily(font));
|
return buildGroup(group.value.body, options.withFontFamily(font));
|
||||||
};
|
};
|
||||||
|
|
||||||
groupTypes.accent = function(group, options) {
|
|
||||||
// Accents are handled in the TeXbook pg. 443, rule 12.
|
|
||||||
let base = group.value.base;
|
|
||||||
|
|
||||||
let supsubGroup;
|
|
||||||
if (group.type === "supsub") {
|
|
||||||
// If our base is a character box, and we have superscripts and
|
|
||||||
// subscripts, the supsub will defer to us. In particular, we want
|
|
||||||
// to attach the superscripts and subscripts to the inner body (so
|
|
||||||
// that the position of the superscripts and subscripts won't be
|
|
||||||
// affected by the height of the accent). We accomplish this by
|
|
||||||
// sticking the base of the accent into the base of the supsub, and
|
|
||||||
// rendering that, while keeping track of where the accent is.
|
|
||||||
|
|
||||||
// The supsub group is the group that was passed in
|
|
||||||
const supsub = group;
|
|
||||||
// The real accent group is the base of the supsub group
|
|
||||||
group = supsub.value.base;
|
|
||||||
// The character box is the base of the accent group
|
|
||||||
base = group.value.base;
|
|
||||||
// Stick the character box into the base of the supsub group
|
|
||||||
supsub.value.base = base;
|
|
||||||
|
|
||||||
// Rerender the supsub group with its new base, and store that
|
|
||||||
// result.
|
|
||||||
supsubGroup = buildGroup(supsub, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the base group
|
|
||||||
const body = buildGroup(base, options.havingCrampedStyle());
|
|
||||||
|
|
||||||
// Does the accent need to shift for the skew of a character?
|
|
||||||
const mustShift = group.value.isShifty && utils.isCharacterBox(base);
|
|
||||||
|
|
||||||
// Calculate the skew of the accent. This is based on the line "If the
|
|
||||||
// nucleus is not a single character, let s = 0; otherwise set s to the
|
|
||||||
// kern amount for the nucleus followed by the \skewchar of its font."
|
|
||||||
// Note that our skew metrics are just the kern between each character
|
|
||||||
// and the skewchar.
|
|
||||||
let skew = 0;
|
|
||||||
if (mustShift) {
|
|
||||||
// If the base is a character box, then we want the skew of the
|
|
||||||
// innermost character. To do that, we find the innermost character:
|
|
||||||
const baseChar = utils.getBaseElem(base);
|
|
||||||
// Then, we render its group to get the symbol inside it
|
|
||||||
const baseGroup = buildGroup(baseChar, options.havingCrampedStyle());
|
|
||||||
// Finally, we pull the skew off of the symbol.
|
|
||||||
skew = baseGroup.skew;
|
|
||||||
// Note that we now throw away baseGroup, because the layers we
|
|
||||||
// removed with getBaseElem might contain things like \color which
|
|
||||||
// we can't get rid of.
|
|
||||||
// TODO(emily): Find a better way to get the skew
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate the amount of space between the body and the accent
|
|
||||||
const clearance = Math.min(
|
|
||||||
body.height,
|
|
||||||
options.fontMetrics().xHeight);
|
|
||||||
|
|
||||||
// Build the accent
|
|
||||||
let accentBody;
|
|
||||||
if (!group.value.isStretchy) {
|
|
||||||
let accent;
|
|
||||||
if (group.value.label === "\\vec") {
|
|
||||||
// Before version 0.9, \vec used the combining font glyph U+20D7.
|
|
||||||
// But browsers, especially Safari, are not consistent in how they
|
|
||||||
// render combining characters when not preceded by a character.
|
|
||||||
// So now we use an SVG.
|
|
||||||
// If Safari reforms, we should consider reverting to the glyph.
|
|
||||||
accent = buildCommon.staticSvg("vec", options);
|
|
||||||
accent.width = parseFloat(accent.style.width);
|
|
||||||
} else {
|
|
||||||
accent = buildCommon.makeSymbol(
|
|
||||||
group.value.label, "Main-Regular", group.mode, options);
|
|
||||||
}
|
|
||||||
// Remove the italic correction of the accent, because it only serves to
|
|
||||||
// shift the accent over to a place we don't want.
|
|
||||||
accent.italic = 0;
|
|
||||||
|
|
||||||
accentBody = makeSpan(["accent-body"], [accent]);
|
|
||||||
|
|
||||||
// CSS defines `.katex .accent .accent-body { width: 0 }`
|
|
||||||
// so that the accent doesn't contribute to the bounding box.
|
|
||||||
// We need to shift the character by its width (effectively half
|
|
||||||
// its width) to compensate.
|
|
||||||
let left = -accent.width / 2;
|
|
||||||
|
|
||||||
// Shift the accent over by the skew.
|
|
||||||
left += skew;
|
|
||||||
|
|
||||||
// The \H character that the fonts use is a combining character, and
|
|
||||||
// thus shows up much too far to the left. To account for this, we add
|
|
||||||
// a manual shift of the width of one space.
|
|
||||||
// TODO(emily): Fix this in a better way, like by changing the font
|
|
||||||
if (group.value.label === '\\H') {
|
|
||||||
left += 0.5; // twice width of space, or width of accent
|
|
||||||
}
|
|
||||||
|
|
||||||
accentBody.style.left = left + "em";
|
|
||||||
|
|
||||||
accentBody = buildCommon.makeVList({
|
|
||||||
positionType: "firstBaseline",
|
|
||||||
children: [
|
|
||||||
{type: "elem", elem: body},
|
|
||||||
{type: "kern", size: -clearance},
|
|
||||||
{type: "elem", elem: accentBody},
|
|
||||||
],
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
} else {
|
|
||||||
accentBody = stretchy.svgSpan(group, options);
|
|
||||||
|
|
||||||
accentBody = buildCommon.makeVList({
|
|
||||||
positionType: "firstBaseline",
|
|
||||||
children: [
|
|
||||||
{type: "elem", elem: body},
|
|
||||||
{type: "elem", elem: accentBody},
|
|
||||||
],
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
const styleSpan = accentBody.children[0].children[0].children[1];
|
|
||||||
styleSpan.classes.push("svg-align"); // text-align: left;
|
|
||||||
if (skew > 0) {
|
|
||||||
// Shorten the accent and nudge it to the right.
|
|
||||||
styleSpan.style.width = `calc(100% - ${2 * skew}em)`;
|
|
||||||
styleSpan.style.marginLeft = (2 * skew) + "em";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const accentWrap = makeSpan(["mord", "accent"], [accentBody], options);
|
|
||||||
|
|
||||||
if (supsubGroup) {
|
|
||||||
// Here, we replace the "base" child of the supsub with our newly
|
|
||||||
// generated accent.
|
|
||||||
supsubGroup.children[0] = accentWrap;
|
|
||||||
|
|
||||||
// Since we don't rerun the height calculation after replacing the
|
|
||||||
// accent, we manually recalculate height.
|
|
||||||
supsubGroup.height = Math.max(accentWrap.height, supsubGroup.height);
|
|
||||||
|
|
||||||
// Accents should always be ords, even when their innards are not.
|
|
||||||
supsubGroup.classes[0] = "mord";
|
|
||||||
|
|
||||||
return supsubGroup;
|
|
||||||
} else {
|
|
||||||
return accentWrap;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
groupTypes.horizBrace = function(group, options) {
|
groupTypes.horizBrace = function(group, options) {
|
||||||
const style = options.style;
|
const style = options.style;
|
||||||
|
|
||||||
@@ -700,29 +551,6 @@ groupTypes.horizBrace = function(group, options) {
|
|||||||
[vlist], options);
|
[vlist], options);
|
||||||
};
|
};
|
||||||
|
|
||||||
groupTypes.accentUnder = function(group, options) {
|
|
||||||
// Treat under accents much like underlines.
|
|
||||||
const innerGroup = buildGroup(group.value.base, options);
|
|
||||||
|
|
||||||
const accentBody = stretchy.svgSpan(group, options);
|
|
||||||
const kern = (/tilde/.test(group.value.label) ? 0.12 : 0);
|
|
||||||
|
|
||||||
// Generate the vlist, with the appropriate kerns
|
|
||||||
const vlist = buildCommon.makeVList({
|
|
||||||
positionType: "bottom",
|
|
||||||
positionData: accentBody.height + kern,
|
|
||||||
children: [
|
|
||||||
{type: "elem", elem: accentBody},
|
|
||||||
{type: "kern", size: kern},
|
|
||||||
{type: "elem", elem: innerGroup},
|
|
||||||
],
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
vlist.children[0].children[0].children[0].classes.push("svg-align");
|
|
||||||
|
|
||||||
return makeSpan(["mord", "accentunder"], [vlist], options);
|
|
||||||
};
|
|
||||||
|
|
||||||
groupTypes.xArrow = function(group, options) {
|
groupTypes.xArrow = function(group, options) {
|
||||||
const style = options.style;
|
const style = options.style;
|
||||||
|
|
||||||
|
@@ -225,24 +225,6 @@ groupTypes.supsub = function(group, options) {
|
|||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
|
|
||||||
groupTypes.accent = function(group, options) {
|
|
||||||
let accentNode;
|
|
||||||
if (group.value.isStretchy) {
|
|
||||||
accentNode = stretchy.mathMLnode(group.value.label);
|
|
||||||
} else {
|
|
||||||
accentNode = new mathMLTree.MathNode(
|
|
||||||
"mo", [makeText(group.value.label, group.mode)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const node = new mathMLTree.MathNode(
|
|
||||||
"mover",
|
|
||||||
[buildGroup(group.value.base, options), accentNode]);
|
|
||||||
|
|
||||||
node.setAttribute("accent", "true");
|
|
||||||
|
|
||||||
return node;
|
|
||||||
};
|
|
||||||
|
|
||||||
groupTypes.spacing = function(group) {
|
groupTypes.spacing = function(group) {
|
||||||
let node;
|
let node;
|
||||||
|
|
||||||
@@ -314,16 +296,6 @@ groupTypes.sizing = function(group, options) {
|
|||||||
return node;
|
return node;
|
||||||
};
|
};
|
||||||
|
|
||||||
groupTypes.accentUnder = function(group, options) {
|
|
||||||
const accentNode = stretchy.mathMLnode(group.value.label);
|
|
||||||
const node = new mathMLTree.MathNode(
|
|
||||||
"munder",
|
|
||||||
[buildGroup(group.value.body, options), accentNode]
|
|
||||||
);
|
|
||||||
node.setAttribute("accentunder", "true");
|
|
||||||
return node;
|
|
||||||
};
|
|
||||||
|
|
||||||
groupTypes.horizBrace = function(group, options) {
|
groupTypes.horizBrace = function(group, options) {
|
||||||
const accentNode = stretchy.mathMLnode(group.value.label);
|
const accentNode = stretchy.mathMLnode(group.value.label);
|
||||||
return new mathMLTree.MathNode(
|
return new mathMLTree.MathNode(
|
||||||
|
@@ -49,6 +49,8 @@ export type DomChildNode = span | anchor | svgNode | symbolNode;
|
|||||||
|
|
||||||
export type SvgChildNode = pathNode | lineNode;
|
export type SvgChildNode = pathNode | lineNode;
|
||||||
|
|
||||||
|
export type CssStyle = {[name: string]: string};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This node represents a span node, with a className, a list of children, and
|
* This node represents a span node, with a className, a list of children, and
|
||||||
* an inline style. It also contains information about its height, depth, and
|
* an inline style. It also contains information about its height, depth, and
|
||||||
@@ -61,20 +63,21 @@ class span implements CombinableDomNode {
|
|||||||
depth: number;
|
depth: number;
|
||||||
width: ?number;
|
width: ?number;
|
||||||
maxFontSize: number;
|
maxFontSize: number;
|
||||||
style: {[string]: string};
|
style: CssStyle;
|
||||||
attributes: {[string]: string};
|
attributes: {[string]: string};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
classes?: string[],
|
classes?: string[],
|
||||||
children?: DomChildNode[],
|
children?: DomChildNode[],
|
||||||
options?: Options,
|
options?: Options,
|
||||||
|
style?: CssStyle,
|
||||||
) {
|
) {
|
||||||
this.classes = classes || [];
|
this.classes = classes || [];
|
||||||
this.children = children || [];
|
this.children = children || [];
|
||||||
this.height = 0;
|
this.height = 0;
|
||||||
this.depth = 0;
|
this.depth = 0;
|
||||||
this.maxFontSize = 0;
|
this.maxFontSize = 0;
|
||||||
this.style = {};
|
this.style = Object.assign({}, style);
|
||||||
this.attributes = {};
|
this.attributes = {};
|
||||||
if (options) {
|
if (options) {
|
||||||
if (options.style.isTight()) {
|
if (options.style.isTight()) {
|
||||||
@@ -192,7 +195,7 @@ class anchor implements CombinableDomNode {
|
|||||||
height: number;
|
height: number;
|
||||||
depth: number;
|
depth: number;
|
||||||
maxFontSize: number;
|
maxFontSize: number;
|
||||||
style: {[string]: string};
|
style: CssStyle;
|
||||||
attributes: {[string]: string};
|
attributes: {[string]: string};
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -386,7 +389,7 @@ class symbolNode implements CombinableDomNode {
|
|||||||
width: number;
|
width: number;
|
||||||
maxFontSize: number;
|
maxFontSize: number;
|
||||||
classes: string[];
|
classes: string[];
|
||||||
style: {[string]: string};
|
style: CssStyle;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
value: string,
|
value: string,
|
||||||
@@ -396,7 +399,7 @@ class symbolNode implements CombinableDomNode {
|
|||||||
skew?: number,
|
skew?: number,
|
||||||
width?: number,
|
width?: number,
|
||||||
classes?: string[],
|
classes?: string[],
|
||||||
style?: {[string]: string},
|
style?: CssStyle,
|
||||||
) {
|
) {
|
||||||
this.value = value;
|
this.value = value;
|
||||||
this.height = height || 0;
|
this.height = height || 0;
|
||||||
@@ -405,7 +408,7 @@ class symbolNode implements CombinableDomNode {
|
|||||||
this.skew = skew || 0;
|
this.skew = skew || 0;
|
||||||
this.width = width || 0;
|
this.width = width || 0;
|
||||||
this.classes = classes || [];
|
this.classes = classes || [];
|
||||||
this.style = style || {};
|
this.style = Object.assign({}, style);
|
||||||
this.maxFontSize = 0;
|
this.maxFontSize = 0;
|
||||||
|
|
||||||
// Mark text from non-Latin scripts with specific classes so that we
|
// Mark text from non-Latin scripts with specific classes so that we
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
// @flow
|
// @flow
|
||||||
/** Include this to ensure that all functions are defined. */
|
/** Include this to ensure that all functions are defined. */
|
||||||
import utils from "./utils";
|
|
||||||
import ParseError from "./ParseError";
|
import ParseError from "./ParseError";
|
||||||
import ParseNode from "./ParseNode";
|
import ParseNode from "./ParseNode";
|
||||||
import {
|
import {
|
||||||
@@ -26,6 +25,8 @@ const defineFunction = function(
|
|||||||
_defineFunction({names, props, handler});
|
_defineFunction({names, props, handler});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO(kevinb): have functions return an object and call defineFunction with
|
||||||
|
// that object in this file instead of relying on side-effects.
|
||||||
import "./functions/sqrt";
|
import "./functions/sqrt";
|
||||||
|
|
||||||
import "./functions/color";
|
import "./functions/color";
|
||||||
@@ -222,55 +223,7 @@ defineFunction([
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Accents
|
import "./functions/accent";
|
||||||
defineFunction([
|
|
||||||
"\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
|
|
||||||
"\\check", "\\hat", "\\vec", "\\dot",
|
|
||||||
"\\widehat", "\\widetilde", "\\overrightarrow", "\\overleftarrow",
|
|
||||||
"\\Overrightarrow", "\\overleftrightarrow", "\\overgroup",
|
|
||||||
"\\overlinesegment", "\\overleftharpoon", "\\overrightharpoon",
|
|
||||||
], {
|
|
||||||
numArgs: 1,
|
|
||||||
}, function(context, args) {
|
|
||||||
const base = args[0];
|
|
||||||
|
|
||||||
const isStretchy = !utils.contains([
|
|
||||||
"\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
|
|
||||||
"\\check", "\\hat", "\\vec", "\\dot",
|
|
||||||
], context.funcName);
|
|
||||||
|
|
||||||
const isShifty = !isStretchy || utils.contains([
|
|
||||||
"\\widehat", "\\widetilde",
|
|
||||||
], context.funcName);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "accent",
|
|
||||||
label: context.funcName,
|
|
||||||
isStretchy: isStretchy,
|
|
||||||
isShifty: isShifty,
|
|
||||||
base: base,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Text-mode accents
|
|
||||||
defineFunction([
|
|
||||||
"\\'", "\\`", "\\^", "\\~", "\\=", "\\u", "\\.", '\\"',
|
|
||||||
"\\r", "\\H", "\\v",
|
|
||||||
], {
|
|
||||||
numArgs: 1,
|
|
||||||
allowedInText: true,
|
|
||||||
allowedInMath: false,
|
|
||||||
}, function(context, args) {
|
|
||||||
const base = args[0];
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "accent",
|
|
||||||
label: context.funcName,
|
|
||||||
isStretchy: false,
|
|
||||||
isShifty: true,
|
|
||||||
base: base,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Horizontal stretchy braces
|
// Horizontal stretchy braces
|
||||||
defineFunction([
|
defineFunction([
|
||||||
@@ -288,19 +241,7 @@ defineFunction([
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Stretchy accents under the body
|
// Stretchy accents under the body
|
||||||
defineFunction([
|
import "./functions/accentunder";
|
||||||
"\\underleftarrow", "\\underrightarrow", "\\underleftrightarrow",
|
|
||||||
"\\undergroup", "\\underlinesegment", "\\utilde",
|
|
||||||
], {
|
|
||||||
numArgs: 1,
|
|
||||||
}, function(context, args) {
|
|
||||||
const base = args[0];
|
|
||||||
return {
|
|
||||||
type: "accentUnder",
|
|
||||||
label: context.funcName,
|
|
||||||
base: base,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stretchy arrows with an optional argument
|
// Stretchy arrows with an optional argument
|
||||||
defineFunction([
|
defineFunction([
|
||||||
|
246
src/functions/accent.js
Normal file
246
src/functions/accent.js
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
// @flow
|
||||||
|
import defineFunction from "../defineFunction";
|
||||||
|
import buildCommon from "../buildCommon";
|
||||||
|
import mathMLTree from "../mathMLTree";
|
||||||
|
import utils from "../utils";
|
||||||
|
import stretchy from "../stretchy";
|
||||||
|
|
||||||
|
import * as html from "../buildHTML";
|
||||||
|
import * as mml from "../buildMathML";
|
||||||
|
|
||||||
|
const htmlBuilder = (group, options) => {
|
||||||
|
// Accents are handled in the TeXbook pg. 443, rule 12.
|
||||||
|
let base = group.value.base;
|
||||||
|
|
||||||
|
let supsubGroup;
|
||||||
|
if (group.type === "supsub") {
|
||||||
|
// If our base is a character box, and we have superscripts and
|
||||||
|
// subscripts, the supsub will defer to us. In particular, we want
|
||||||
|
// to attach the superscripts and subscripts to the inner body (so
|
||||||
|
// that the position of the superscripts and subscripts won't be
|
||||||
|
// affected by the height of the accent). We accomplish this by
|
||||||
|
// sticking the base of the accent into the base of the supsub, and
|
||||||
|
// rendering that, while keeping track of where the accent is.
|
||||||
|
|
||||||
|
// The supsub group is the group that was passed in
|
||||||
|
const supsub = group;
|
||||||
|
// The real accent group is the base of the supsub group
|
||||||
|
group = supsub.value.base;
|
||||||
|
// The character box is the base of the accent group
|
||||||
|
base = group.value.base;
|
||||||
|
// Stick the character box into the base of the supsub group
|
||||||
|
supsub.value.base = base;
|
||||||
|
|
||||||
|
// Rerender the supsub group with its new base, and store that
|
||||||
|
// result.
|
||||||
|
supsubGroup = html.buildGroup(supsub, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the base group
|
||||||
|
const body = html.buildGroup(base, options.havingCrampedStyle());
|
||||||
|
|
||||||
|
// Does the accent need to shift for the skew of a character?
|
||||||
|
const mustShift = group.value.isShifty && utils.isCharacterBox(base);
|
||||||
|
|
||||||
|
// Calculate the skew of the accent. This is based on the line "If the
|
||||||
|
// nucleus is not a single character, let s = 0; otherwise set s to the
|
||||||
|
// kern amount for the nucleus followed by the \skewchar of its font."
|
||||||
|
// Note that our skew metrics are just the kern between each character
|
||||||
|
// and the skewchar.
|
||||||
|
let skew = 0;
|
||||||
|
if (mustShift) {
|
||||||
|
// If the base is a character box, then we want the skew of the
|
||||||
|
// innermost character. To do that, we find the innermost character:
|
||||||
|
const baseChar = utils.getBaseElem(base);
|
||||||
|
// Then, we render its group to get the symbol inside it
|
||||||
|
const baseGroup = html.buildGroup(baseChar, options.havingCrampedStyle());
|
||||||
|
// Finally, we pull the skew off of the symbol.
|
||||||
|
skew = baseGroup.skew;
|
||||||
|
// Note that we now throw away baseGroup, because the layers we
|
||||||
|
// removed with getBaseElem might contain things like \color which
|
||||||
|
// we can't get rid of.
|
||||||
|
// TODO(emily): Find a better way to get the skew
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate the amount of space between the body and the accent
|
||||||
|
const clearance = Math.min(
|
||||||
|
body.height,
|
||||||
|
options.fontMetrics().xHeight);
|
||||||
|
|
||||||
|
// Build the accent
|
||||||
|
let accentBody;
|
||||||
|
if (!group.value.isStretchy) {
|
||||||
|
let accent;
|
||||||
|
let width: number;
|
||||||
|
if (group.value.label === "\\vec") {
|
||||||
|
// Before version 0.9, \vec used the combining font glyph U+20D7.
|
||||||
|
// But browsers, especially Safari, are not consistent in how they
|
||||||
|
// render combining characters when not preceded by a character.
|
||||||
|
// So now we use an SVG.
|
||||||
|
// If Safari reforms, we should consider reverting to the glyph.
|
||||||
|
accent = buildCommon.staticSvg("vec", options);
|
||||||
|
width = buildCommon.svgData.vec[1];
|
||||||
|
} else {
|
||||||
|
accent = buildCommon.makeSymbol(
|
||||||
|
group.value.label, "Main-Regular", group.mode, options);
|
||||||
|
// Remove the italic correction of the accent, because it only serves to
|
||||||
|
// shift the accent over to a place we don't want.
|
||||||
|
accent.italic = 0;
|
||||||
|
width = accent.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
accentBody = buildCommon.makeSpan(["accent-body"], [accent]);
|
||||||
|
|
||||||
|
// CSS defines `.katex .accent .accent-body { width: 0 }`
|
||||||
|
// so that the accent doesn't contribute to the bounding box.
|
||||||
|
// We need to shift the character by its width (effectively half
|
||||||
|
// its width) to compensate.
|
||||||
|
let left = -width / 2;
|
||||||
|
|
||||||
|
// Shift the accent over by the skew.
|
||||||
|
left += skew;
|
||||||
|
|
||||||
|
// The \H character that the fonts use is a combining character, and
|
||||||
|
// thus shows up much too far to the left. To account for this, we add
|
||||||
|
// a manual shift of the width of one space.
|
||||||
|
// TODO(emily): Fix this in a better way, like by changing the font
|
||||||
|
if (group.value.label === '\\H') {
|
||||||
|
left += 0.5; // twice width of space, or width of accent
|
||||||
|
}
|
||||||
|
|
||||||
|
accentBody.style.left = left + "em";
|
||||||
|
|
||||||
|
accentBody = buildCommon.makeVList({
|
||||||
|
positionType: "firstBaseline",
|
||||||
|
children: [
|
||||||
|
{type: "elem", elem: body},
|
||||||
|
{type: "kern", size: -clearance},
|
||||||
|
{type: "elem", elem: accentBody},
|
||||||
|
],
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
accentBody = stretchy.svgSpan(group, options);
|
||||||
|
|
||||||
|
accentBody = buildCommon.makeVList({
|
||||||
|
positionType: "firstBaseline",
|
||||||
|
children: [
|
||||||
|
{type: "elem", elem: body},
|
||||||
|
{
|
||||||
|
type: "elem",
|
||||||
|
elem: accentBody,
|
||||||
|
wrapperClasses: ["svg-align"],
|
||||||
|
wrapperStyle: skew > 0
|
||||||
|
? {
|
||||||
|
width: `calc(100% - ${2 * skew}em)`,
|
||||||
|
marginLeft: `${(2 * skew)}em`,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accentWrap =
|
||||||
|
buildCommon.makeSpan(["mord", "accent"], [accentBody], options);
|
||||||
|
|
||||||
|
if (supsubGroup) {
|
||||||
|
// Here, we replace the "base" child of the supsub with our newly
|
||||||
|
// generated accent.
|
||||||
|
supsubGroup.children[0] = accentWrap;
|
||||||
|
|
||||||
|
// Since we don't rerun the height calculation after replacing the
|
||||||
|
// accent, we manually recalculate height.
|
||||||
|
supsubGroup.height = Math.max(accentWrap.height, supsubGroup.height);
|
||||||
|
|
||||||
|
// Accents should always be ords, even when their innards are not.
|
||||||
|
supsubGroup.classes[0] = "mord";
|
||||||
|
|
||||||
|
return supsubGroup;
|
||||||
|
} else {
|
||||||
|
return accentWrap;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const mathmlBuilder = (group, options) => {
|
||||||
|
let accentNode;
|
||||||
|
if (group.value.isStretchy) {
|
||||||
|
accentNode = stretchy.mathMLnode(group.value.label);
|
||||||
|
} else {
|
||||||
|
accentNode = new mathMLTree.MathNode(
|
||||||
|
"mo", [mml.makeText(group.value.label, group.mode)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const node = new mathMLTree.MathNode(
|
||||||
|
"mover",
|
||||||
|
[mml.buildGroup(group.value.base, options), accentNode]);
|
||||||
|
|
||||||
|
node.setAttribute("accent", "true");
|
||||||
|
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
|
||||||
|
const NON_STRETCHY_ACCENT_REGEX = new RegExp([
|
||||||
|
"\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
|
||||||
|
"\\check", "\\hat", "\\vec", "\\dot",
|
||||||
|
].map(accent => `\\${accent}`).join("|"));
|
||||||
|
|
||||||
|
// Accents
|
||||||
|
defineFunction({
|
||||||
|
type: "accent",
|
||||||
|
names: [
|
||||||
|
"\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
|
||||||
|
"\\check", "\\hat", "\\vec", "\\dot",
|
||||||
|
"\\widehat", "\\widetilde", "\\overrightarrow", "\\overleftarrow",
|
||||||
|
"\\Overrightarrow", "\\overleftrightarrow", "\\overgroup",
|
||||||
|
"\\overlinesegment", "\\overleftharpoon", "\\overrightharpoon",
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
numArgs: 1,
|
||||||
|
},
|
||||||
|
handler: (context, args) => {
|
||||||
|
const base = args[0];
|
||||||
|
|
||||||
|
const isStretchy = !NON_STRETCHY_ACCENT_REGEX.test(context.funcName);
|
||||||
|
const isShifty = !isStretchy ||
|
||||||
|
context.funcName === "\\widehat" ||
|
||||||
|
context.funcName === "\\widetilde";
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "accent",
|
||||||
|
label: context.funcName,
|
||||||
|
isStretchy: isStretchy,
|
||||||
|
isShifty: isShifty,
|
||||||
|
base: base,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
htmlBuilder,
|
||||||
|
mathmlBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Text-mode accents
|
||||||
|
defineFunction({
|
||||||
|
type: "accent",
|
||||||
|
names: [
|
||||||
|
"\\'", "\\`", "\\^", "\\~", "\\=", "\\u", "\\.", '\\"',
|
||||||
|
"\\r", "\\H", "\\v",
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
numArgs: 1,
|
||||||
|
allowedInText: true,
|
||||||
|
allowedInMath: false,
|
||||||
|
},
|
||||||
|
handler: (context, args) => {
|
||||||
|
const base = args[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "accent",
|
||||||
|
label: context.funcName,
|
||||||
|
isStretchy: false,
|
||||||
|
isShifty: true,
|
||||||
|
base: base,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
htmlBuilder,
|
||||||
|
mathmlBuilder,
|
||||||
|
});
|
57
src/functions/accentunder.js
Normal file
57
src/functions/accentunder.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
// @flow
|
||||||
|
// Horizontal overlap functions
|
||||||
|
import defineFunction from "../defineFunction";
|
||||||
|
import buildCommon from "../buildCommon";
|
||||||
|
import mathMLTree from "../mathMLTree";
|
||||||
|
import stretchy from "../stretchy";
|
||||||
|
|
||||||
|
import * as html from "../buildHTML";
|
||||||
|
import * as mml from "../buildMathML";
|
||||||
|
|
||||||
|
defineFunction({
|
||||||
|
type: "accentUnder",
|
||||||
|
names: [
|
||||||
|
"\\underleftarrow", "\\underrightarrow", "\\underleftrightarrow",
|
||||||
|
"\\undergroup", "\\underlinesegment", "\\utilde",
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
numArgs: 1,
|
||||||
|
},
|
||||||
|
handler: (context, args) => {
|
||||||
|
const base = args[0];
|
||||||
|
return {
|
||||||
|
type: "accentUnder",
|
||||||
|
label: context.funcName,
|
||||||
|
base: base,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
htmlBuilder: (group, options) => {
|
||||||
|
// Treat under accents much like underlines.
|
||||||
|
const innerGroup = html.buildGroup(group.value.base, options);
|
||||||
|
|
||||||
|
const accentBody = stretchy.svgSpan(group, options);
|
||||||
|
const kern = group.value.label === "\\utilde" ? 0.12 : 0;
|
||||||
|
|
||||||
|
// Generate the vlist, with the appropriate kerns
|
||||||
|
const vlist = buildCommon.makeVList({
|
||||||
|
positionType: "bottom",
|
||||||
|
positionData: accentBody.height + kern,
|
||||||
|
children: [
|
||||||
|
{type: "elem", elem: accentBody, wrapperClasses: ["svg-align"]},
|
||||||
|
{type: "kern", size: kern},
|
||||||
|
{type: "elem", elem: innerGroup},
|
||||||
|
],
|
||||||
|
}, options);
|
||||||
|
|
||||||
|
return buildCommon.makeSpan(["mord", "accentunder"], [vlist], options);
|
||||||
|
},
|
||||||
|
mathmlBuilder: (group, options) => {
|
||||||
|
const accentNode = stretchy.mathMLnode(group.value.label);
|
||||||
|
const node = new mathMLTree.MathNode(
|
||||||
|
"munder",
|
||||||
|
[mml.buildGroup(group.value.body, options), accentNode]
|
||||||
|
);
|
||||||
|
node.setAttribute("accentunder", "true");
|
||||||
|
return node;
|
||||||
|
},
|
||||||
|
});
|
Reference in New Issue
Block a user