BussproofsUtil.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2018-2022 The MathJax Consortium
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /**
  18. * @fileoverview Postfilter utility for the Bussproofs package.
  19. *
  20. * @author v.sorge@mathjax.org (Volker Sorge)
  21. */
  22. import ParseOptions from '../ParseOptions.js';
  23. import NodeUtil from '../NodeUtil.js';
  24. import ParseUtil from '../ParseUtil.js';
  25. import {MmlNode} from '../../../core/MmlTree/MmlNode.js';
  26. import {Property} from '../../../core/Tree/Node.js';
  27. import {MathItem} from '../../../core/MathItem.js';
  28. import {MathDocument} from '../../../core/MathDocument.js';
  29. type MATHITEM = MathItem<any, any, any>;
  30. type MATHDOCUMENT = MathDocument<any, any, any>;
  31. type FilterData = {math: MATHITEM, document: MATHDOCUMENT, data: ParseOptions};
  32. /**
  33. * Global constants local to the module. They instantiate an output jax for
  34. * bounding box computation.
  35. */
  36. let doc: MATHDOCUMENT = null;
  37. let item: MATHITEM = null;
  38. /**
  39. * Get the bounding box of a node.
  40. * @param {MmlNode} node The target node.
  41. */
  42. let getBBox = function(node: MmlNode) {
  43. item.root = node;
  44. let {w: width} = (doc.outputJax as any).getBBox(item, doc);
  45. return width;
  46. };
  47. /**
  48. * Get the actual table that represents the inference rule, i.e., the rule
  49. * without the label. We ignore preceding elements or spaces.
  50. *
  51. * @param {MmlNode} node The out node representing the inference.
  52. * @return {MmlNode} The actual table representing the inference rule.
  53. */
  54. let getRule = function(node: MmlNode): MmlNode {
  55. let i = 0;
  56. while (node && !NodeUtil.isType(node, 'mtable')) {
  57. if (NodeUtil.isType(node, 'text')) {
  58. return null;
  59. }
  60. if (NodeUtil.isType(node, 'mrow')) {
  61. node = node.childNodes[0] as MmlNode;
  62. i = 0;
  63. continue;
  64. }
  65. node = node.parent.childNodes[i] as MmlNode;
  66. i++;
  67. }
  68. return node;
  69. };
  70. /*******************************
  71. * Convenience methods for retrieving bits of the proof tree.
  72. */
  73. /**
  74. * Gets premises of an inference rule.
  75. * @param {MmlNode} rule The rule.
  76. * @param {string} direction Up or down.
  77. * @return {MmlNode} The premisses.
  78. */
  79. let getPremises = function(rule: MmlNode, direction: string): MmlNode {
  80. return rule.childNodes[direction === 'up' ? 1 : 0].childNodes[0].
  81. childNodes[0].childNodes[0].childNodes[0] as MmlNode;
  82. };
  83. /**
  84. * Gets nth premise.
  85. * @param {MmlNode} premises The premises.
  86. * @param {number} n Number of premise to get.
  87. * @return {MmlNode} The nth premise.
  88. */
  89. let getPremise = function(premises: MmlNode, n: number): MmlNode {
  90. return premises.childNodes[n].childNodes[0].childNodes[0] as MmlNode;
  91. };
  92. /**
  93. * Gets first premise.
  94. * @param {MmlNode} premises The premises.
  95. * @return {MmlNode} The first premise.
  96. */
  97. let firstPremise = function(premises: MmlNode): MmlNode {
  98. return getPremise(premises, 0) as MmlNode;
  99. };
  100. /**
  101. * Gets last premise.
  102. * @param {MmlNode} premises The premises.
  103. * @return {MmlNode} The last premise.
  104. */
  105. let lastPremise = function(premises: MmlNode): MmlNode {
  106. return getPremise(premises, premises.childNodes.length - 1);
  107. };
  108. /**
  109. * Get conclusion in an inference rule.
  110. * @param {MmlNode} rule The rule.
  111. * @param {string} direction Up or down.
  112. * @return {MmlNode} The conclusion.
  113. */
  114. let getConclusion = function(rule: MmlNode, direction: string): MmlNode {
  115. return rule.childNodes[direction === 'up' ? 0 : 1].childNodes[0].childNodes[0].childNodes[0] as MmlNode;
  116. };
  117. /**
  118. * Gets the actual column element in an inference rule. I.e., digs down through
  119. * row, padding and space elements.
  120. * @param {MmlNode} inf The rule.
  121. * @return {MmlNode} The mtd element.
  122. */
  123. let getColumn = function(inf: MmlNode): MmlNode {
  124. while (inf && !NodeUtil.isType(inf, 'mtd')) {
  125. inf = inf.parent as MmlNode;
  126. }
  127. return inf;
  128. };
  129. /**
  130. * Gets the next sibling of an inference rule.
  131. * @param {MmlNode} inf The inference rule.
  132. * @return {MmlNode} The next sibling.
  133. */
  134. let nextSibling = function(inf: MmlNode): MmlNode {
  135. return inf.parent.childNodes[inf.parent.childNodes.indexOf(inf) + 1] as MmlNode;
  136. };
  137. /**
  138. * Gets the previous sibling of an inference rule.
  139. * @param {MmlNode} inf The inference rule.
  140. * @return {MmlNode} The previous sibling.
  141. */
  142. // @ts-ignore
  143. let previousSibling = function(inf: MmlNode): MmlNode {
  144. return inf.parent.childNodes[inf.parent.childNodes.indexOf(inf) - 1] as MmlNode;
  145. };
  146. /**
  147. * Get the parent inference rule.
  148. * @param {MmlNode} inf The inference rule.
  149. * @return {MmlNode} Its parent.
  150. */
  151. let getParentInf = function(inf: MmlNode): MmlNode {
  152. while (inf && getProperty(inf, 'inference') == null) {
  153. inf = inf.parent as MmlNode;
  154. }
  155. return inf;
  156. };
  157. // Computes bbox spaces
  158. //
  159. //
  160. /**
  161. * Computes spacing left or right of an inference rule. In the case of
  162. * right: right space + right label
  163. * left: left space + left label
  164. * @param {MmlNode} inf The overall proof tree.
  165. * @param {MmlNode} rule The particular inference rule.
  166. * @param {boolean = false} right True for right, o/w left.
  167. * @return {number} The spacing next to the rule.
  168. */
  169. let getSpaces = function(inf: MmlNode, rule: MmlNode, right: boolean = false): number {
  170. let result = 0;
  171. if (inf === rule) {
  172. return result;
  173. }
  174. if (inf !== rule.parent) {
  175. let children = inf.childNodes as MmlNode[];
  176. let index = right ? children.length - 1 : 0;
  177. if (NodeUtil.isType(children[index], 'mspace')) {
  178. result += getBBox(children[index]);
  179. }
  180. inf = rule.parent;
  181. }
  182. if (inf === rule) {
  183. return result;
  184. }
  185. let children = inf.childNodes as MmlNode[];
  186. let index = right ? children.length - 1 : 0;
  187. if (children[index] !== rule) {
  188. result += getBBox(children[index]);
  189. }
  190. return result;
  191. };
  192. // - Get rule T from Wrapper W.
  193. // - Get conclusion C in T.
  194. // - w: Preceding/following space/label.
  195. // - (x - y)/2: Distance from left boundary to middle of C.
  196. /**
  197. * Computes an space adjustment value to move the inference rule.
  198. * @param {MmlNode} inf The inference rule.
  199. * @param {boolean = false} right True if adjustments are on the right.
  200. * @return {number} The adjustment value.
  201. */
  202. let adjustValue = function(inf: MmlNode, right: boolean = false): number {
  203. let rule = getRule(inf);
  204. let conc = getConclusion(rule, getProperty(rule, 'inferenceRule') as string);
  205. // TODO: Here we have to improve sequent adjustment!
  206. let w = getSpaces(inf, rule, right);
  207. let x = getBBox(rule);
  208. let y = getBBox(conc);
  209. return w + ((x - y) / 2);
  210. };
  211. /**
  212. * Adds (positive or negative) space in the column containing the inference rule.
  213. * @param {ParseOptions} config The parser configuration.
  214. * @param {MmlNode} inf The inference rule to place.
  215. * @param {number} space The space to be added.
  216. * @param {boolean = false} right True if adjustment is on the right.
  217. */
  218. let addSpace = function(config: ParseOptions, inf: MmlNode,
  219. space: number, right: boolean = false) {
  220. if (getProperty(inf, 'inferenceRule') ||
  221. getProperty(inf, 'labelledRule')) {
  222. const mrow = config.nodeFactory.create('node', 'mrow');
  223. inf.parent.replaceChild(mrow, inf);
  224. mrow.setChildren([inf]);
  225. moveProperties(inf, mrow);
  226. inf = mrow;
  227. }
  228. // TODO: Simplify below as we now have a definite mrow.
  229. const index = right ? inf.childNodes.length - 1 : 0;
  230. let mspace = inf.childNodes[index] as MmlNode;
  231. if (NodeUtil.isType(mspace, 'mspace')) {
  232. NodeUtil.setAttribute(
  233. mspace, 'width',
  234. ParseUtil.Em(ParseUtil.dimen2em(
  235. NodeUtil.getAttribute(mspace, 'width') as string) + space));
  236. return;
  237. }
  238. mspace = config.nodeFactory.create('node', 'mspace', [],
  239. {width: ParseUtil.Em(space)});
  240. if (right) {
  241. inf.appendChild(mspace);
  242. return;
  243. }
  244. mspace.parent = inf;
  245. inf.childNodes.unshift(mspace);
  246. };
  247. /**
  248. * Propagates properties up the tree.
  249. * @param {MmlNode} src The source node.
  250. * @param {MmlNode} dest The destination node.
  251. */
  252. let moveProperties = function(src: MmlNode, dest: MmlNode) {
  253. let props = ['inference', 'proof', 'maxAdjust', 'labelledRule'];
  254. props.forEach(x => {
  255. let value = getProperty(src, x);
  256. if (value != null) {
  257. setProperty(dest, x, value);
  258. removeProperty(src, x);
  259. }
  260. });
  261. };
  262. /********************************
  263. * The following methods deal with sequents. There are still issues with the
  264. * spatial layout, though.
  265. */
  266. // Sequents look like this: table row 3 cells
  267. // The table has the 'sequent' property.
  268. // The row is the node that is actually saved in the config object.
  269. /**
  270. * Method to adjust sequent positioning after the tree is computed.
  271. * @param {ParseOptions} config Parser configuration options.
  272. */
  273. let adjustSequents = function(config: ParseOptions) {
  274. let sequents = config.nodeLists['sequent'];
  275. if (!sequents) {
  276. return;
  277. }
  278. for (let i = sequents.length - 1, seq; seq = sequents[i]; i--) {
  279. if (getProperty(seq, 'sequentProcessed')) {
  280. removeProperty(seq, 'sequentProcessed');
  281. continue;
  282. }
  283. let collect = [];
  284. let inf = getParentInf(seq);
  285. if (getProperty(inf, 'inference') !== 1) {
  286. continue;
  287. }
  288. collect.push(seq);
  289. while (getProperty(inf, 'inference') === 1) {
  290. // In case we have a table with a label.
  291. inf = getRule(inf);
  292. let premise = firstPremise(getPremises(inf, getProperty(inf, 'inferenceRule') as string));
  293. let sequent = (getProperty(premise, 'inferenceRule')) ?
  294. // If the first premise is an inference rule, check the conclusions for a sequent.
  295. getConclusion(premise, getProperty(premise, 'inferenceRule') as string) :
  296. // Otherwise it is a hyp and we have to check the formula itself.
  297. premise;
  298. if (getProperty(sequent, 'sequent')) {
  299. seq = sequent.childNodes[0] as MmlNode;
  300. collect.push(seq);
  301. setProperty(seq, 'sequentProcessed', true);
  302. }
  303. inf = premise;
  304. }
  305. adjustSequentPairwise(config, collect);
  306. }
  307. };
  308. /**
  309. * Add spaces to the sequents where necessary.
  310. * @param {ParseOptions} config Parser configuration options.
  311. * @param {MmlNode} sequent The sequent inference rule.
  312. * @param {number} position Position of formula to adjust (0 or 2).
  313. * @param {string} direction Left or right of the turnstyle.
  314. * @param {number} width The space to add to the formulas.
  315. */
  316. const addSequentSpace = function(config: ParseOptions, sequent: MmlNode,
  317. position: number, direction: string, width: number) {
  318. let mspace = config.nodeFactory.create('node', 'mspace', [],
  319. {width: ParseUtil.Em(width)});
  320. if (direction === 'left') {
  321. let row = sequent.childNodes[position].childNodes[0] as MmlNode;
  322. mspace.parent = row;
  323. row.childNodes.unshift(mspace);
  324. } else {
  325. sequent.childNodes[position].appendChild(mspace);
  326. }
  327. setProperty(sequent.parent, 'sequentAdjust_' + direction, width);
  328. };
  329. /**
  330. * Adjusts the sequent positioning for a list of inference rules by pairwise
  331. * adjusting the width of formulas in sequents. I.e.,
  332. * A,B |- C
  333. * ------------
  334. * A |- B,C
  335. *
  336. * will be adjusted to
  337. *
  338. * A, B |- C
  339. * ----------------
  340. * A |- B,C
  341. *
  342. * @param {ParseOptions} config Parser configuration options.
  343. * @param {MmlNode[]} sequents The list of sequents.
  344. */
  345. const adjustSequentPairwise = function(config: ParseOptions, sequents: MmlNode[]) {
  346. let top = sequents.pop();
  347. while (sequents.length) {
  348. let bottom = sequents.pop();
  349. let [left, right] = compareSequents(top, bottom);
  350. if (getProperty(top.parent, 'axiom')) {
  351. addSequentSpace(config, left < 0 ? top : bottom, 0, 'left', Math.abs(left));
  352. addSequentSpace(config, right < 0 ? top : bottom, 2, 'right', Math.abs(right));
  353. }
  354. top = bottom;
  355. }
  356. };
  357. /**
  358. * Compares the top and bottom sequent of a inference rule
  359. * Top: A |- B
  360. * ----------
  361. * Bottom: C |- D
  362. *
  363. * @param {MmlNode} top Top sequent.
  364. * @param {MmlNode} bottom Bottom sequent.
  365. * @return {[number, number]} The delta for left and right side of the sequents.
  366. */
  367. const compareSequents = function(top: MmlNode, bottom: MmlNode): [number, number] {
  368. const tr = getBBox(top.childNodes[2] as MmlNode);
  369. const br = getBBox(bottom.childNodes[2] as MmlNode);
  370. const tl = getBBox(top.childNodes[0] as MmlNode);
  371. const bl = getBBox(bottom.childNodes[0] as MmlNode);
  372. // Deltas
  373. const dl = tl - bl;
  374. const dr = tr - br;
  375. return [dl, dr];
  376. };
  377. // For every inference rule we adjust the width of ruler by subtracting and
  378. // adding suitable spaces around the rule. The algorithm in detail.
  379. //
  380. // Notions that we need:
  381. //
  382. //
  383. // * Inference: The inference consisting either of an inference rule or a
  384. // structure containing the rule plus 0 - 2 labels and spacing
  385. // elements. s l{0,1} t r{0,1} s', m,n \in IN_0
  386. //
  387. // Technically this is realised as nested rows, if the spaces
  388. // and/or labels exist:
  389. // mr s mr l t r /mr s' /mr
  390. //
  391. // * InferenceRule: The rule without the labels and spacing.
  392. //
  393. // * Conclusion: The element forming the conclusion of the rule. In
  394. // downwards inferences this is the final row of the table.
  395. //
  396. // * Premises: The premises of the rule. In downwards inferences this is the
  397. // first row of the rule. Note that this is a rule itself,
  398. // with one column for each premise and an empty column
  399. // inbetween.
  400. //
  401. // * |x|: Width of bounding box of element x.
  402. //
  403. // Left adjustment:
  404. //
  405. // * For the given inference I:
  406. // + compute rule R of I
  407. // + compute premises P of I
  408. // + compute premise P_f, P_l as first and last premise of I
  409. //
  410. // * If P_f is an inference rule:
  411. // + compute adjust value a_f for wrapper W_f of P_f
  412. // + add -a_f space to wrapper W_f
  413. // + add a_f space to wrapper W
  414. //
  415. // * If P_l is an inference rule:
  416. // + compute adjust value a_l for wrapper W_l of P_l
  417. // + if I has (right) label L: a_l = a_l + |L|
  418. // + add -a_l space to P_l
  419. // + a_l = max(a_l, A_I), where A_I is saved ajust value in the
  420. // "maxAdjust" attribute of I.
  421. //
  422. // + Case I is proof: Add a_l space to inf. (Correct after proof.)
  423. // + Case I has sibling: Add a_l space to sibling. (Correct after column.)
  424. // + Otherwise: Propagate a_l by
  425. // ++ find direct parent infererence rule I'
  426. // ++ Set A_{I'} = a_l.
  427. //
  428. /**
  429. * Implements the above algorithm.
  430. * @param {FilterData} arg The parser configuration and mathitem to filter.
  431. */
  432. export let balanceRules = function(arg: FilterData) {
  433. item = new arg.document.options.MathItem('', null, arg.math.display);
  434. let config = arg.data;
  435. adjustSequents(config);
  436. let inferences = config.nodeLists['inference'] || [];
  437. for (let inf of inferences) {
  438. let isProof = getProperty(inf, 'proof');
  439. // This currently only works with downwards rules.
  440. let rule = getRule(inf);
  441. let premises = getPremises(rule, getProperty(rule, 'inferenceRule') as string);
  442. let premiseF = firstPremise(premises);
  443. if (getProperty(premiseF, 'inference')) {
  444. let adjust = adjustValue(premiseF);
  445. if (adjust) {
  446. addSpace(config, premiseF, -adjust);
  447. let w = getSpaces(inf, rule, false);
  448. addSpace(config, inf, adjust - w);
  449. }
  450. }
  451. // Right adjust:
  452. let premiseL = lastPremise(premises);
  453. if (getProperty(premiseL, 'inference') == null) {
  454. continue;
  455. }
  456. let adjust = adjustValue(premiseL, true);
  457. addSpace(config, premiseL, -adjust, true);
  458. let w = getSpaces(inf, rule, true);
  459. let maxAdjust = getProperty(inf, 'maxAdjust') as number;
  460. if (maxAdjust != null) {
  461. adjust = Math.max(adjust, maxAdjust);
  462. }
  463. let column: MmlNode;
  464. if (isProof || !(column = getColumn(inf))) {
  465. // After the tree we add a space with the accumulated max value.
  466. // If the element is not in a column, we know we have some noise and the
  467. // proof is an mrow around the final inference.
  468. addSpace(config,
  469. // in case the rule has been moved into an mrow in Left Adjust.
  470. getProperty(inf, 'proof') ? inf : inf.parent, adjust - w, true);
  471. continue;
  472. }
  473. let sibling = nextSibling(column);
  474. if (sibling) {
  475. // If there is a next column, it is the empty one and we make it wider by
  476. // the accumulated max value.
  477. const pos = config.nodeFactory.create('node', 'mspace', [],
  478. {width: adjust - w + 'em'});
  479. sibling.appendChild(pos);
  480. inf.removeProperty('maxAdjust');
  481. continue;
  482. }
  483. let parentRule = getParentInf(column);
  484. if (!parentRule) {
  485. continue;
  486. }
  487. // We are currently in rightmost inference, so we propagate the max
  488. // correction value up in the tree.
  489. adjust = getProperty(parentRule, 'maxAdjust') ?
  490. Math.max(getProperty(parentRule, 'maxAdjust') as number, adjust) : adjust;
  491. setProperty(parentRule, 'maxAdjust', adjust);
  492. }
  493. };
  494. /**
  495. * Facilities for semantically relevant properties. These are used by SRE and
  496. * are always prefixed with bspr_.
  497. */
  498. let property_prefix = 'bspr_';
  499. let blacklistedProperties = {
  500. [property_prefix + 'maxAdjust']: true
  501. };
  502. /**
  503. * Sets a bussproofs property used for postprocessing and to convey
  504. * semantics. Uses the bspr prefix.
  505. * @param {MmlNode} node The node.
  506. * @param {string} property The property to set.
  507. * @param {Property} value Its value.
  508. */
  509. export let setProperty = function(node: MmlNode, property: string, value: Property) {
  510. NodeUtil.setProperty(node, property_prefix + property, value);
  511. };
  512. /**
  513. * Gets a bussproofs property.
  514. * @param {MmlNode} node The node.
  515. * @param {string} property The property to retrieve.
  516. * @return {Property} The property object.
  517. */
  518. export let getProperty = function(node: MmlNode, property: string): Property {
  519. return NodeUtil.getProperty(node, property_prefix + property);
  520. };
  521. /**
  522. * Removes a bussproofs property.
  523. * @param {MmlNode} node
  524. * @param {string} property
  525. */
  526. export let removeProperty = function(node: MmlNode, property: string) {
  527. node.removeProperty(property_prefix + property);
  528. };
  529. /**
  530. * Postprocessor that adds properties as attributes to the nodes, unless they
  531. * are blacklisted.
  532. * @param {FilterData} arg The object to post-process.
  533. */
  534. export let makeBsprAttributes = function(arg: FilterData) {
  535. arg.data.root.walkTree((mml: MmlNode, _data?: any) => {
  536. let attr: string[] = [];
  537. mml.getPropertyNames().forEach(x => {
  538. if (!blacklistedProperties[x] && x.match(RegExp('^' + property_prefix))) {
  539. attr.push(x + ':' + mml.getProperty(x));
  540. }
  541. });
  542. if (attr.length) {
  543. NodeUtil.setAttribute(mml, 'semantics', attr.join(';'));
  544. }
  545. });
  546. };
  547. /**
  548. * Preprocessor that sets the document and jax for bounding box computations
  549. * @param {FilterData} arg The object to pre-process.
  550. */
  551. export let saveDocument = function (arg: FilterData) {
  552. doc = arg.document;
  553. if (!('getBBox' in doc.outputJax)) {
  554. throw Error('The bussproofs extension requires an output jax with a getBBox() method');
  555. }
  556. };
  557. /**
  558. * Clear the document when we are done
  559. * @param {FilterData} arg The object to pre-process.
  560. */
  561. export let clearDocument = function (_arg: FilterData) {
  562. doc = null;
  563. };