temml 0.10.23 → 0.10.29

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,7 +14,7 @@
14
14
  * https://mit-license.org/
15
15
  */
16
16
 
17
- const version = "0.10.23";
17
+ const version = "0.10.29";
18
18
 
19
19
  function postProcess(block) {
20
20
  const labelMap = {};
@@ -65,6 +65,4 @@
65
65
  exports.postProcess = postProcess;
66
66
  exports.version = version;
67
67
 
68
- Object.defineProperty(exports, '__esModule', { value: true });
69
-
70
68
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "temml",
3
- "version": "0.10.23",
3
+ "version": "0.10.29",
4
4
  "description": "TeX to MathML conversion in JavaScript.",
5
5
  "main": "dist/temml.js",
6
6
  "engines": {
@@ -28,10 +28,13 @@
28
28
  ],
29
29
  "license": "MIT",
30
30
  "devDependencies": {
31
- "eslint": "^8.39.0",
31
+ "@eslint/eslintrc": "^3.1.0",
32
+ "@eslint/js": "^9.9.0",
33
+ "eslint": "^9.9.0",
32
34
  "esm": "^3.2.25",
33
- "rollup": "^2.66.1",
34
- "terser": "^5.14.2"
35
+ "globals": "^15.9.0",
36
+ "rollup": "^4.20.0",
37
+ "terser": "^5.31.6"
35
38
  },
36
39
  "scripts": {
37
40
  "lint": "eslint temml.js src",
@@ -39,7 +42,7 @@
39
42
  "visual-test": "node utils/buildTests.js",
40
43
  "test": "yarn lint && node utils/buildTests.js && yarn unit-test",
41
44
  "minify": "terser test/temml.js -o site/assets/temml.min.js -c -m && terser contrib/mhchem/mhchem.js -o site/assets/mhchem.min.js -c -m",
42
- "build": "rollup --config ./utils/rollupConfig.js && yarn minify && node utils/insertPlugins.js",
45
+ "build": "rollup --config ./utils/rollupConfig.mjs && yarn minify && node utils/insertPlugins.js",
43
46
  "docs": "node utils/buildDocs.js",
44
47
  "dist": "yarn build && node ./utils/copyfiles.js && terser contrib/auto-render/test/auto-render.js -o contrib/auto-render/dist/auto-render.min.js -c -m"
45
48
  }
package/src/Parser.js CHANGED
@@ -16,6 +16,7 @@ import unicodeAccents from /*preval*/ "./unicodeAccents";
16
16
  import unicodeSymbols from /*preval*/ "./unicodeSymbols";
17
17
 
18
18
  const binLeftCancellers = ["bin", "op", "open", "punct", "rel"];
19
+ const sizeRegEx = /([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/
19
20
 
20
21
  /**
21
22
  * This file contains the parser used to parse out a TeX expression from the
@@ -679,7 +680,7 @@ export default class Parser {
679
680
  res.text = "0pt"; // Enable \above{}
680
681
  isBlank = true; // This is here specifically for \genfrac
681
682
  }
682
- const match = /([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/.exec(res.text);
683
+ const match = sizeRegEx.exec(res.text);
683
684
  if (!match) {
684
685
  throw new ParseError("Invalid size: '" + res.text + "'", res);
685
686
  }
@@ -885,7 +886,7 @@ export default class Parser {
885
886
  // At this point, we should have a symbol, possibly with accents.
886
887
  // First expand any accented base symbol according to unicodeSymbols.
887
888
  if (Object.prototype.hasOwnProperty.call(unicodeSymbols, text[0]) &&
888
- !symbols[this.mode][text[0]]) {
889
+ this.mode === "math" && !symbols[this.mode][text[0]]) {
889
890
  // This behavior is not strict (XeTeX-compatible) in math mode.
890
891
  if (this.settings.strict && this.mode === "math") {
891
892
  throw new ParseError(`Accented Unicode text character "${text[0]}" used in ` + `math mode`,
@@ -895,7 +896,9 @@ export default class Parser {
895
896
  text = unicodeSymbols[text[0]] + text.slice(1);
896
897
  }
897
898
  // Strip off any combining characters
898
- const match = combiningDiacriticalMarksEndRegex.exec(text);
899
+ const match = this.mode === "math"
900
+ ? combiningDiacriticalMarksEndRegex.exec(text)
901
+ : null
899
902
  if (match) {
900
903
  text = text.substring(0, match.index);
901
904
  if (text === "i") {
@@ -948,7 +951,7 @@ export default class Parser {
948
951
  };
949
952
  }
950
953
  symbol = s;
951
- } else if (text.charCodeAt(0) >= 0x80) {
954
+ } else if (text.charCodeAt(0) >= 0x80 || combiningDiacriticalMarksEndRegex.exec(text)) {
952
955
  // no symbol for e.g. ^
953
956
  if (this.settings.strict && this.mode === "math") {
954
957
  throw new ParseError(`Unicode text character "${text[0]}" used in math mode`, nucleus)
package/src/Settings.js CHANGED
@@ -42,7 +42,11 @@ export default class Settings {
42
42
  */
43
43
  isTrusted(context) {
44
44
  if (context.url && !context.protocol) {
45
- context.protocol = utils.protocolFromUrl(context.url);
45
+ const protocol = utils.protocolFromUrl(context.url);
46
+ if (protocol == null) {
47
+ return false
48
+ }
49
+ context.protocol = protocol
46
50
  }
47
51
  const trust = typeof this.trust === "function" ? this.trust(context) : this.trust;
48
52
  return Boolean(trust);
@@ -34,49 +34,72 @@ export const makeText = function(text, mode, style) {
34
34
  return new mathMLTree.TextNode(text);
35
35
  };
36
36
 
37
+ const copyChar = (newRow, child) => {
38
+ if (newRow.children.length === 0 ||
39
+ newRow.children[newRow.children.length - 1].type !== "mtext") {
40
+ const mtext = new mathMLTree.MathNode(
41
+ "mtext",
42
+ [new mathMLTree.TextNode(child.children[0].text)]
43
+ )
44
+ newRow.children.push(mtext)
45
+ } else {
46
+ newRow.children[newRow.children.length - 1].children[0].text += child.children[0].text
47
+ }
48
+ }
49
+
37
50
  export const consolidateText = mrow => {
38
51
  // If possible, consolidate adjacent <mtext> elements into a single element.
39
52
  if (mrow.type !== "mrow" && mrow.type !== "mstyle") { return mrow }
40
53
  if (mrow.children.length === 0) { return mrow } // empty group, e.g., \text{}
41
- if (!mrow.children[0].attributes || mrow.children[0].type !== "mtext") { return mrow }
42
- const variant = mrow.children[0].attributes.mathvariant || ""
43
- const mtext = new mathMLTree.MathNode(
44
- "mtext",
45
- [new mathMLTree.TextNode(mrow.children[0].children[0].text)]
46
- )
47
- for (let i = 1; i < mrow.children.length; i++) {
48
- // Check each child and, if possible, copy the character into child[0].
49
- const localVariant = mrow.children[i].attributes.mathvariant || ""
50
- if (mrow.children[i].type === "mrow") {
51
- const childRow = mrow.children[i]
52
- for (let j = 0; j < childRow.children.length; j++) {
53
- // We'll also check the children of a mrow. One level only. No recursion.
54
- const childVariant = childRow.children[j].attributes.mathvariant || ""
55
- if (childVariant !== variant || childRow.children[j].type !== "mtext") {
56
- return mrow // At least one element cannot be consolidated. Get out.
57
- } else {
58
- mtext.children[0].text += childRow.children[j].children[0].text
54
+ const newRow = new mathMLTree.MathNode("mrow")
55
+ for (let i = 0; i < mrow.children.length; i++) {
56
+ const child = mrow.children[i];
57
+ if (child.type === "mtext" && Object.keys(child.attributes).length === 0) {
58
+ copyChar(newRow, child)
59
+ } else if (child.type === "mrow") {
60
+ // We'll also check the children of an mrow. One level only. No recursion.
61
+ let canConsolidate = true
62
+ for (let j = 0; j < child.children.length; j++) {
63
+ const grandChild = child.children[j];
64
+ if (grandChild.type !== "mtext" || Object.keys(child.attributes).length !== 0) {
65
+ canConsolidate = false
66
+ break
67
+ }
68
+ }
69
+ if (canConsolidate) {
70
+ for (let j = 0; j < child.children.length; j++) {
71
+ const grandChild = child.children[j];
72
+ copyChar(newRow, grandChild)
59
73
  }
74
+ } else {
75
+ newRow.children.push(child)
60
76
  }
61
- } else if (localVariant !== variant || mrow.children[i].type !== "mtext") {
62
- return mrow
63
77
  } else {
64
- mtext.children[0].text += mrow.children[i].children[0].text
78
+ newRow.children.push(child)
65
79
  }
66
80
  }
67
- // Firefox does not render a space at either end of an <mtext> string.
68
- // To get proper rendering, we replace leading or trailing spaces with no-break spaces.
69
- if (mtext.children[0].text.charAt(0) === " ") {
70
- mtext.children[0].text = "\u00a0" + mtext.children[0].text.slice(1)
71
- }
72
- const L = mtext.children[0].text.length
73
- if (L > 0 && mtext.children[0].text.charAt(L - 1) === " ") {
74
- mtext.children[0].text = mtext.children[0].text.slice(0, -1) + "\u00a0"
81
+ for (let i = 0; i < newRow.children.length; i++) {
82
+ if (newRow.children[i].type === "mtext") {
83
+ const mtext = newRow.children[i];
84
+ // Firefox does not render a space at either end of an <mtext> string.
85
+ // To get proper rendering, we replace leading or trailing spaces with no-break spaces.
86
+ if (mtext.children[0].text.charAt(0) === " ") {
87
+ mtext.children[0].text = "\u00a0" + mtext.children[0].text.slice(1)
88
+ }
89
+ const L = mtext.children[0].text.length
90
+ if (L > 0 && mtext.children[0].text.charAt(L - 1) === " ") {
91
+ mtext.children[0].text = mtext.children[0].text.slice(0, -1) + "\u00a0"
92
+ }
93
+ for (const [key, value] of Object.entries(mrow.attributes)) {
94
+ mtext.attributes[key] = value
95
+ }
96
+ }
75
97
  }
76
- for (const [key, value] of Object.entries(mrow.attributes)) {
77
- mtext.attributes[key] = value
98
+ if (newRow.children.length === 1 && newRow.children[0].type === "mtext") {
99
+ return newRow.children[0]; // A consolidated <mtext>
100
+ } else {
101
+ return newRow
78
102
  }
79
- return mtext
80
103
  }
81
104
 
82
105
  const numberRegEx = /^[0-9]$/
@@ -7,7 +7,8 @@ import { StyleLevel } from "../constants"
7
7
  import ParseError from "../ParseError";
8
8
  import { assertNodeType, assertSymbolNodeType } from "../parseNode";
9
9
  import { checkSymbolNodeType } from "../parseNode";
10
-
10
+ import { stringFromArg } from "../macros"
11
+ import { calculateSize } from "../units"
11
12
  import * as mml from "../buildMathML";
12
13
 
13
14
  // Helper functions
@@ -38,6 +39,24 @@ const validateAmsEnvironmentContext = context => {
38
39
  }
39
40
  }
40
41
 
42
+ const sizeRegEx = /([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/
43
+ const arrayGaps = macros => {
44
+ let arraystretch = macros.get("\\arraystretch")
45
+ if (typeof arraystretch !== "string") {
46
+ arraystretch = stringFromArg(arraystretch.tokens)
47
+ }
48
+ arraystretch = isNaN(arraystretch) ? null : Number(arraystretch)
49
+ let arraycolsepStr = macros.get("\\arraycolsep")
50
+ if (typeof arraycolsepStr !== "string") {
51
+ arraycolsepStr = stringFromArg(arraycolsepStr.tokens)
52
+ }
53
+ const match = sizeRegEx.exec(arraycolsepStr)
54
+ const arraycolsep = match
55
+ ? { number: +(match[1] + match[2]), unit: match[3] }
56
+ : null
57
+ return [arraystretch, arraycolsep]
58
+ }
59
+
41
60
  const getTag = (group, style, rowNum) => {
42
61
  let tag
43
62
  const tagContents = group.tags.shift()
@@ -76,12 +95,14 @@ function parseArray(
76
95
  {
77
96
  cols, // [{ type: string , align: l|c|r|null }]
78
97
  envClasses, // align(ed|at|edat) | array | cases | cd | small | multline
79
- addEqnNum, // boolean
80
- singleRow, // boolean
98
+ addEqnNum, // boolean
99
+ singleRow, // boolean
81
100
  emptySingleRow, // boolean
82
- maxNumCols, // number
83
- leqno // boolean
84
- },
101
+ maxNumCols, // number
102
+ leqno, // boolean
103
+ arraystretch, // number | null
104
+ arraycolsep // size value | null
105
+ },
85
106
  scriptLevel
86
107
  ) {
87
108
  parser.gullet.beginGroup();
@@ -111,7 +132,6 @@ function parseArray(
111
132
  // Test for \hline at the top of the array.
112
133
  hLinesBeforeRow.push(getHLines(parser));
113
134
 
114
- // eslint-disable-next-line no-constant-condition
115
135
  while (true) {
116
136
  // Parse each cell in its own group (namespace)
117
137
  let cell = parser.parseExpression(false, singleRow ? "\\end" : "\\\\");
@@ -211,7 +231,9 @@ function parseArray(
211
231
  addEqnNum,
212
232
  scriptLevel,
213
233
  tags,
214
- leqno
234
+ leqno,
235
+ arraystretch,
236
+ arraycolsep
215
237
  };
216
238
  }
217
239
 
@@ -301,12 +323,18 @@ const mathmlBuilder = function(group, style) {
301
323
  }
302
324
 
303
325
  if (group.envClasses.length > 0) {
304
- const pad = group.envClasses.includes("jot")
326
+ let pad = group.envClasses.includes("jot")
305
327
  ? "0.7" // 0.5ex + 0.09em top & bot padding
306
328
  : group.envClasses.includes("small")
307
329
  ? "0.35"
308
330
  : "0.5" // 0.5ex default top & bot padding
309
- const sidePadding = group.envClasses.includes("abut")
331
+ if (group.arraystretch && group.arraystretch !== 1) {
332
+ // In LaTeX, \arraystretch is a factor applied to a 12pt strut height.
333
+ // It defines a baseline to baseline distance.
334
+ // Here, we do an approximation of that approach.
335
+ pad = String(1.4 * group.arraystretch - 0.8)
336
+ }
337
+ let sidePadding = group.envClasses.includes("abut")
310
338
  ? "0"
311
339
  : group.envClasses.includes("cases")
312
340
  ? "0"
@@ -315,6 +343,12 @@ const mathmlBuilder = function(group, style) {
315
343
  : group.envClasses.includes("cd")
316
344
  ? "0.25"
317
345
  : "0.4" // default side padding
346
+ let sidePadUnit = "em"
347
+ if (group.arraycolsep) {
348
+ const arraySidePad = calculateSize(group.arraycolsep, style)
349
+ sidePadding = arraySidePad.number
350
+ sidePadUnit = arraySidePad.unit
351
+ }
318
352
 
319
353
  const numCols = tbl.length === 0 ? 0 : tbl[0].children.length
320
354
 
@@ -333,7 +367,8 @@ const mathmlBuilder = function(group, style) {
333
367
  // Padding
334
368
  for (let i = 0; i < tbl.length; i++) {
335
369
  for (let j = 0; j < tbl[i].children.length; j++) {
336
- tbl[i].children[j].style.padding = `${pad}ex ${sidePad(j, 1)}em ${pad}ex ${sidePad(j, 0)}em`
370
+ tbl[i].children[j].style.padding = `${pad}ex ${sidePad(j, 1)}${sidePadUnit}`
371
+ + ` ${pad}ex ${sidePad(j, 0)}${sidePadUnit}`
337
372
  }
338
373
  }
339
374
 
@@ -587,10 +622,13 @@ defineEnvironment({
587
622
  }
588
623
  throw new ParseError("Unknown column alignment: " + ca, nde);
589
624
  });
625
+ const [arraystretch, arraycolsep] = arrayGaps(context.parser.gullet.macros)
590
626
  const res = {
591
627
  cols,
592
628
  envClasses: ["array"],
593
- maxNumCols: cols.length
629
+ maxNumCols: cols.length,
630
+ arraystretch,
631
+ arraycolsep
594
632
  };
595
633
  return parseArray(context.parser, res, dCellStyle(context.envName));
596
634
  },
@@ -656,6 +694,7 @@ defineEnvironment({
656
694
  }
657
695
  const res = parseArray(context.parser, payload, "text")
658
696
  res.cols = new Array(res.body[0].length).fill({ type: "align", align: colAlign })
697
+ const [arraystretch, arraycolsep] = arrayGaps(context.parser.gullet.macros)
659
698
  return delimiters
660
699
  ? {
661
700
  type: "leftright",
@@ -663,7 +702,9 @@ defineEnvironment({
663
702
  body: [res],
664
703
  left: delimiters[0],
665
704
  right: delimiters[1],
666
- rightColor: undefined // \right uninfluenced by \color in array
705
+ rightColor: undefined, // \right uninfluenced by \color in array
706
+ arraystretch,
707
+ arraycolsep
667
708
  }
668
709
  : res;
669
710
  },
@@ -71,7 +71,7 @@ export function parseCD(parser) {
71
71
  parser.gullet.beginGroup();
72
72
  parser.gullet.macros.set("\\cr", "\\\\\\relax");
73
73
  parser.gullet.beginGroup();
74
- while (true) { // eslint-disable-line no-constant-condition
74
+ while (true) {
75
75
  // Get the parse nodes for the next row.
76
76
  parsedRows.push(parser.parseExpression(false, "\\\\"));
77
77
  parser.gullet.endGroup();
@@ -71,6 +71,20 @@ const needWebkitShift = new Set([
71
71
  "\\'", "\\^", "\\~", "\\=", "\\u", "\\.", '\\"', "\\r", "\\H", "\\v"
72
72
  ])
73
73
 
74
+ const combiningChar = {
75
+ "\\`": "\u0300",
76
+ "\\'": "\u0301",
77
+ "\\^": "\u0302",
78
+ "\\~": "\u0303",
79
+ "\\=": "\u0304",
80
+ "\\u": "\u0306",
81
+ "\\.": "\u0307",
82
+ '\\"': "\u0308",
83
+ "\\r": "\u030A",
84
+ "\\H": "\u030B",
85
+ "\\v": "\u030C"
86
+ }
87
+
74
88
  // Accents
75
89
  defineFunction({
76
90
  type: "accent",
@@ -140,13 +154,24 @@ defineFunction({
140
154
  console.log(`Temml parse error: Command ${context.funcName} is invalid in math mode.`)
141
155
  }
142
156
 
143
- return {
144
- type: "accent",
145
- mode: mode,
146
- label: context.funcName,
147
- isStretchy: false,
148
- base: base
149
- };
157
+ if (mode === "text" && base.text && base.text.length === 1
158
+ && context.funcName in combiningChar && smalls.indexOf(base.text) > -1) {
159
+ // Return a combining accent character
160
+ return {
161
+ type: "textord",
162
+ mode: "text",
163
+ text: base.text + combiningChar[context.funcName]
164
+ }
165
+ } else {
166
+ // Build up the accent
167
+ return {
168
+ type: "accent",
169
+ mode: mode,
170
+ label: context.funcName,
171
+ isStretchy: false,
172
+ base: base
173
+ }
174
+ }
150
175
  },
151
176
  mathmlBuilder
152
177
  });
@@ -141,6 +141,9 @@ defineFunction({
141
141
 
142
142
  if (funcName === "\\edef" || funcName === "\\xdef") {
143
143
  tokens = parser.gullet.expandTokens(tokens);
144
+ if (tokens.length > parser.gullet.settings.maxExpand) {
145
+ throw new ParseError("Too many expansions in an " + funcName);
146
+ }
144
147
  tokens.reverse(); // to fit in with stack order
145
148
  }
146
149
  // Final arg is the expansion of the macro
@@ -249,8 +252,7 @@ defineFunction({
249
252
 
250
253
  parser.gullet.macros.set(
251
254
  name,
252
- { tokens, numArgs },
253
- !parser.settings.strict
255
+ { tokens, numArgs }
254
256
  )
255
257
 
256
258
  return { type: "internal", mode: parser.mode };
@@ -22,52 +22,62 @@ const mathmlBuilder = (group, style) => {
22
22
  padding()
23
23
  ])
24
24
  } else {
25
- node = new mathMLTree.MathNode("mrow", [mml.buildGroup(group.body, style)])
25
+ node = new mathMLTree.MathNode("menclose", [mml.buildGroup(group.body, style)])
26
26
  }
27
27
  switch (group.label) {
28
28
  case "\\overline":
29
- node.style.padding = "0.1em 0 0 0"
30
- node.style.borderTop = "0.065em solid"
29
+ node.setAttribute("notation", "top") // for Firefox & WebKit
30
+ node.classes.push("tml-overline") // for Chromium
31
31
  break
32
32
  case "\\underline":
33
- node.style.padding = "0 0 0.1em 0"
34
- node.style.borderBottom = "0.065em solid"
33
+ node.setAttribute("notation", "bottom")
34
+ node.classes.push("tml-underline")
35
35
  break
36
36
  case "\\cancel":
37
- // We can't use an inline background-gradient. It does not work client-side.
38
- // So set a class and put the rule in the external CSS file.
39
- node.classes.push("tml-cancel")
37
+ node.setAttribute("notation", "updiagonalstrike")
38
+ node.children.push(new mathMLTree.MathNode("mrow", [], ["tml-cancel", "upstrike"]))
40
39
  break
41
40
  case "\\bcancel":
42
- node.classes.push("tml-bcancel")
41
+ node.setAttribute("notation", "downdiagonalstrike")
42
+ node.children.push(new mathMLTree.MathNode("mrow", [], ["tml-cancel", "downstrike"]))
43
+ break
44
+ case "\\sout":
45
+ node.setAttribute("notation", "horizontalstrike")
46
+ node.children.push(new mathMLTree.MathNode("mrow", [], ["tml-cancel", "sout"]))
47
+ break
48
+ case "\\xcancel":
49
+ node.setAttribute("notation", "updiagonalstrike downdiagonalstrike")
50
+ node.classes.push("tml-xcancel")
43
51
  break
44
- /*
45
52
  case "\\longdiv":
46
- node.setAttribute("notation", "longdiv");
53
+ node.setAttribute("notation", "longdiv")
54
+ node.classes.push("longdiv-top")
55
+ node.children.push(new mathMLTree.MathNode("mrow", [], ["longdiv-arc"]))
47
56
  break
48
57
  case "\\phase":
49
- node.setAttribute("notation", "phasorangle");
50
- break */
51
- case "\\angl":
52
- node.style.padding = "0.03889em 0.03889em 0 0.03889em"
53
- node.style.borderTop = "0.049em solid"
54
- node.style.borderRight = "0.049em solid"
55
- node.style.marginRight = "0.03889em"
58
+ node.setAttribute("notation", "phasorangle")
59
+ node.classes.push("phasor-bottom")
60
+ node.children.push(new mathMLTree.MathNode("mrow", [], ["phasor-angle"]))
56
61
  break
57
- case "\\sout":
58
- node.style.backgroundImage = 'linear-gradient(black, black)'
59
- node.style.backgroundRepeat = 'no-repeat'
60
- node.style.backgroundSize = '100% 1.5px'
61
- node.style.backgroundPosition = '0 center'
62
+ case "\\textcircled":
63
+ node.setAttribute("notation", "circle")
64
+ node.classes.push("circle-pad")
65
+ node.children.push(new mathMLTree.MathNode("mrow", [], ["textcircle"]))
66
+ break
67
+ case "\\angl":
68
+ node.setAttribute("notation", "actuarial")
69
+ node.classes.push("actuarial")
62
70
  break
63
71
  case "\\boxed":
64
72
  // \newcommand{\boxed}[1]{\fbox{\m@th$\displaystyle#1$}} from amsmath.sty
65
- node.style = { padding: "3pt 0 3pt 0", border: "1px solid" }
73
+ node.setAttribute("notation", "box")
74
+ node.classes.push("tml-box")
66
75
  node.setAttribute("scriptlevel", "0")
67
76
  node.setAttribute("displaystyle", "true")
68
77
  break
69
78
  case "\\fbox":
70
- node.style = { padding: "3pt", border: "1px solid" }
79
+ node.setAttribute("notation", "box")
80
+ node.classes.push("tml-fbox")
71
81
  break
72
82
  case "\\fcolorbox":
73
83
  case "\\colorbox": {
@@ -80,14 +90,11 @@ const mathmlBuilder = (group, style) => {
80
90
  const style = { padding: "3pt 0 3pt 0" }
81
91
 
82
92
  if (group.label === "\\fcolorbox") {
83
- style.border = "0.06em solid " + String(group.borderColor)
93
+ style.border = "0.0667em solid " + String(group.borderColor)
84
94
  }
85
95
  node.style = style
86
96
  break
87
97
  }
88
- case "\\xcancel":
89
- node.classes.push("tml-xcancel")
90
- break
91
98
  }
92
99
  if (group.backgroundColor) {
93
100
  node.setAttribute("mathbackground", group.backgroundColor);
@@ -180,8 +187,8 @@ defineFunction({
180
187
 
181
188
  defineFunction({
182
189
  type: "enclose",
183
- names: ["\\angl", "\\cancel", "\\bcancel", "\\xcancel", "\\sout", "\\overline", "\\boxed"],
184
- // , "\\phase", "\\longdiv"
190
+ names: ["\\angl", "\\cancel", "\\bcancel", "\\xcancel", "\\sout", "\\overline",
191
+ "\\boxed", "\\longdiv", "\\phase"],
185
192
  props: {
186
193
  numArgs: 1
187
194
  },
@@ -215,3 +222,25 @@ defineFunction({
215
222
  },
216
223
  mathmlBuilder
217
224
  });
225
+
226
+
227
+ defineFunction({
228
+ type: "enclose",
229
+ names: ["\\textcircled"],
230
+ props: {
231
+ numArgs: 1,
232
+ argTypes: ["text"],
233
+ allowedInArgument: true,
234
+ allowedInText: true
235
+ },
236
+ handler({ parser, funcName }, args) {
237
+ const body = args[0];
238
+ return {
239
+ type: "enclose",
240
+ mode: parser.mode,
241
+ label: funcName,
242
+ body
243
+ };
244
+ },
245
+ mathmlBuilder
246
+ });
@@ -12,7 +12,7 @@ const ordAtomTypes = ["textord", "mathord", "atom"]
12
12
  const noSuccessor = ["\\smallint"];
13
13
 
14
14
  // Math operators (e.g. \sin) need a space between these types and themselves:
15
- export const ordTypes = ["textord", "mathord", "ordgroup", "close", "leftright"];
15
+ export const ordTypes = ["textord", "mathord", "ordgroup", "close", "leftright", "font"];
16
16
 
17
17
  // NOTE: Unlike most `builders`s, this one handles not only "op", but also
18
18
  // "supsub" since some of them (like \int) can affect super/subscripting.
@@ -32,6 +32,13 @@ const mathmlBuilder = (group, style) => {
32
32
  node = new mathMLTree.MathNode("mo", [mml.makeText(group.name, group.mode)]);
33
33
  if (noSuccessor.includes(group.name)) {
34
34
  node.setAttribute("largeop", "false")
35
+ } else if (group.limits) {
36
+ // This is a workaround for a MathML/Chromium bug.
37
+ // This is being applied to singleCharBigOps, which are not really stretchy.
38
+ // But by setting the stretchy attribute, Chromium will vertically center
39
+ // big ops around the math axis. This is needed since STIX TWO does not do so.
40
+ // TODO: Remove this hack when MathML & Chromium fix their problem.
41
+ node.setAttribute("stretchy", "true")
35
42
  } else {
36
43
  node.setAttribute("movablelimits", "false")
37
44
  }
@@ -81,6 +88,9 @@ const singleCharBigOps = {
81
88
  "\u2a04": "\\biguplus",
82
89
  "\u2a05": "\\bigsqcap",
83
90
  "\u2a06": "\\bigsqcup",
91
+ "\u2a03": "\\bigcupdot",
92
+ "\u2a07": "\\bigdoublevee",
93
+ "\u2a08": "\\bigdoublewedge",
84
94
  "\u2a09": "\\bigtimes"
85
95
  };
86
96
 
@@ -91,8 +101,12 @@ defineFunction({
91
101
  "\\bigvee",
92
102
  "\\bigwedge",
93
103
  "\\biguplus",
104
+ "\\bigcupplus",
105
+ "\\bigcupdot",
94
106
  "\\bigcap",
95
107
  "\\bigcup",
108
+ "\\bigdoublevee",
109
+ "\\bigdoublewedge",
96
110
  "\\intop",
97
111
  "\\prod",
98
112
  "\\sum",
@@ -55,9 +55,9 @@ defineFunctionBuilders({
55
55
  if (group.sup) {
56
56
  const sup = mml.buildGroup(group.sup, childStyle)
57
57
  const testNode = sup.type === "mrow" ? sup.children[0] : sup
58
- if ((testNode.type === "mo" && testNode.classes.includes("tml-prime"))
58
+ if ((testNode && testNode.type === "mo" && testNode.classes.includes("tml-prime"))
59
59
  && group.base && group.base.text && group.base.text === "f") {
60
- // Chromium does not address italic correction on prime. Prevent f′ from overlapping.
60
+ // Chromium does not address italic correction on prime. Prevent f′ from overlapping.
61
61
  testNode.classes.push("prime-pad")
62
62
  }
63
63
  children.push(sup)