layout_renderer.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import { Debugger } from '../common/debugger.js';
  2. import * as DomUtil from '../common/dom_util.js';
  3. import * as EngineConst from '../common/engine_const.js';
  4. import { Engine } from '../common/engine.js';
  5. import * as AudioUtil from './audio_util.js';
  6. import { XmlRenderer } from './xml_renderer.js';
  7. export class LayoutRenderer extends XmlRenderer {
  8. constructor() {
  9. super(...arguments);
  10. this.values = new Map();
  11. }
  12. finalize(str) {
  13. setRelValues(this.values.get('rel'));
  14. return setTwoDim(str);
  15. }
  16. pause(_pause) {
  17. return '';
  18. }
  19. prosodyElement(attr, value) {
  20. return attr === EngineConst.personalityProps.LAYOUT ? `<${value}>` : '';
  21. }
  22. closeTag(tag) {
  23. return `</${tag}>`;
  24. }
  25. markup(descrs) {
  26. this.values.clear();
  27. const result = [];
  28. let content = [];
  29. for (const descr of descrs) {
  30. if (!descr.layout) {
  31. content.push(descr);
  32. continue;
  33. }
  34. result.push(this.processContent(content));
  35. content = [];
  36. const [pref, layout, value] = this.layoutValue(descr.layout);
  37. if (pref === 'begin') {
  38. result.push('<' + layout + (value ? ` value="${value}"` : '') + '>');
  39. continue;
  40. }
  41. if (pref === 'end') {
  42. result.push('</' + layout + '>');
  43. continue;
  44. }
  45. console.warn('Something went wrong with layout markup: ' + layout);
  46. }
  47. result.push(this.processContent(content));
  48. return result.join('');
  49. }
  50. processContent(content) {
  51. const result = [];
  52. const markup = AudioUtil.personalityMarkup(content);
  53. for (let i = 0, descr; (descr = markup[i]); i++) {
  54. if (descr.span) {
  55. result.push(this.merge(descr.span));
  56. continue;
  57. }
  58. if (AudioUtil.isPauseElement(descr)) {
  59. continue;
  60. }
  61. }
  62. return result.join('');
  63. }
  64. layoutValue(layout) {
  65. const match = layout.match(/^(begin|end|)(.*\D)(\d*)$/);
  66. const value = match[3];
  67. if (!value) {
  68. return [match[1], match[2], ''];
  69. }
  70. layout = match[2];
  71. if (!this.values.has(layout)) {
  72. this.values.set(layout, {});
  73. }
  74. this.values.get(layout)[value] = true;
  75. return [match[1], layout, value];
  76. }
  77. }
  78. LayoutRenderer.options = {
  79. cayleyshort: Engine.getInstance().cayleyshort,
  80. linebreaks: Engine.getInstance().linebreaks
  81. };
  82. let twodExpr = '';
  83. const handlers = {
  84. TABLE: handleTable,
  85. CASES: handleCases,
  86. CAYLEY: handleCayley,
  87. MATRIX: handleMatrix,
  88. CELL: recurseTree,
  89. FENCE: recurseTree,
  90. ROW: recurseTree,
  91. FRACTION: handleFraction,
  92. NUMERATOR: handleFractionPart,
  93. DENOMINATOR: handleFractionPart,
  94. REL: handleRelation,
  95. OP: handleRelation
  96. };
  97. function applyHandler(element) {
  98. const tag = DomUtil.tagName(element);
  99. const handler = handlers[tag];
  100. return handler ? handler(element) : element.textContent;
  101. }
  102. const relValues = new Map();
  103. function setRelValues(values) {
  104. relValues.clear();
  105. if (!values)
  106. return;
  107. const keys = Object.keys(values)
  108. .map((x) => parseInt(x))
  109. .sort();
  110. for (let i = 0, key; (key = keys[i]); i++) {
  111. relValues.set(key, i + 1);
  112. }
  113. }
  114. function setTwoDim(str) {
  115. twodExpr = '';
  116. const dom = DomUtil.parseInput(`<all>${str}</all>`);
  117. Debugger.getInstance().output(DomUtil.formatXml(dom.toString()));
  118. twodExpr = recurseTree(dom);
  119. return twodExpr;
  120. }
  121. function combineContent(str1, str2) {
  122. if (!str1 || !str2) {
  123. return str1 + str2;
  124. }
  125. const height1 = strHeight(str1);
  126. const height2 = strHeight(str2);
  127. const diff = height1 - height2;
  128. str1 = diff < 0 ? padCell(str1, height2, strWidth(str1)) : str1;
  129. str2 = diff > 0 ? padCell(str2, height1, strWidth(str2)) : str2;
  130. const lines1 = str1.split(/\r\n|\r|\n/);
  131. const lines2 = str2.split(/\r\n|\r|\n/);
  132. const result = [];
  133. for (let i = 0; i < lines1.length; i++) {
  134. result.push(lines1[i] + lines2[i]);
  135. }
  136. return result.join('\n');
  137. }
  138. function recurseTree(dom) {
  139. let result = '';
  140. for (const child of Array.from(dom.childNodes)) {
  141. if (child.nodeType === DomUtil.NodeType.TEXT_NODE) {
  142. result = combineContent(result, child.textContent);
  143. continue;
  144. }
  145. result = combineContent(result, applyHandler(child));
  146. }
  147. return result;
  148. }
  149. function strHeight(str) {
  150. return str.split(/\r\n|\r|\n/).length;
  151. }
  152. function strWidth(str) {
  153. return str.split(/\r\n|\r|\n/).reduce((max, x) => Math.max(x.length, max), 0);
  154. }
  155. function padHeight(str, height) {
  156. const padding = height - strHeight(str);
  157. return str + (padding > 0 ? new Array(padding + 1).join('\n') : '');
  158. }
  159. function padWidth(str, width) {
  160. const lines = str.split(/\r\n|\r|\n/);
  161. const result = [];
  162. for (const line of lines) {
  163. const padding = width - line.length;
  164. result.push(line + (padding > 0 ? new Array(padding + 1).join('⠀') : ''));
  165. }
  166. return result.join('\n');
  167. }
  168. function padCell(str, height, width) {
  169. str = padHeight(str, height);
  170. return padWidth(str, width);
  171. }
  172. function assembleRows(matrix) {
  173. const children = Array.from(matrix.childNodes);
  174. const mat = [];
  175. for (const row of children) {
  176. if (row.nodeType !== DomUtil.NodeType.ELEMENT_NODE) {
  177. continue;
  178. }
  179. mat.push(handleRow(row));
  180. }
  181. return mat;
  182. }
  183. function getMaxParameters(mat) {
  184. const maxHeight = mat.reduce((max, x) => Math.max(x.height, max), 0);
  185. const maxWidth = [];
  186. for (let i = 0; i < mat[0].width.length; i++) {
  187. maxWidth.push(mat.map((x) => x.width[i]).reduce((max, x) => Math.max(max, x), 0));
  188. }
  189. return [maxHeight, maxWidth];
  190. }
  191. function combineCells(mat, maxWidth) {
  192. const newMat = [];
  193. for (const row of mat) {
  194. if (row.height === 0) {
  195. continue;
  196. }
  197. const newCells = [];
  198. for (let i = 0; i < row.cells.length; i++) {
  199. newCells.push(padCell(row.cells[i], row.height, maxWidth[i]));
  200. }
  201. row.cells = newCells;
  202. newMat.push(row);
  203. }
  204. return newMat;
  205. }
  206. function combineRows(mat, maxHeight) {
  207. if (maxHeight === 1) {
  208. return mat
  209. .map((row) => row.lfence + row.cells.join(row.sep) + row.rfence)
  210. .join('\n');
  211. }
  212. const result = [];
  213. for (const row of mat) {
  214. const sep = verticalArrange(row.sep, row.height);
  215. let str = row.cells.shift();
  216. while (row.cells.length) {
  217. str = combineContent(str, sep);
  218. str = combineContent(str, row.cells.shift());
  219. }
  220. str = combineContent(verticalArrange(row.lfence, row.height), str);
  221. str = combineContent(str, verticalArrange(row.rfence, row.height));
  222. result.push(str);
  223. result.push(row.lfence + new Array(strWidth(str) - 3).join(row.sep) + row.rfence);
  224. }
  225. return result.slice(0, -1).join('\n');
  226. }
  227. function verticalArrange(char, height) {
  228. let str = '';
  229. while (height) {
  230. str += char + '\n';
  231. height--;
  232. }
  233. return str.slice(0, -1);
  234. }
  235. function getFence(node) {
  236. if (node.nodeType === DomUtil.NodeType.ELEMENT_NODE &&
  237. DomUtil.tagName(node) === 'FENCE') {
  238. return applyHandler(node);
  239. }
  240. return '';
  241. }
  242. function handleMatrix(matrix) {
  243. let mat = assembleRows(matrix);
  244. const [maxHeight, maxWidth] = getMaxParameters(mat);
  245. mat = combineCells(mat, maxWidth);
  246. return combineRows(mat, maxHeight);
  247. }
  248. function handleTable(table) {
  249. let mat = assembleRows(table);
  250. mat.forEach((row) => {
  251. row.cells = row.cells.slice(1).slice(0, -1);
  252. row.width = row.width.slice(1).slice(0, -1);
  253. });
  254. const [maxHeight, maxWidth] = getMaxParameters(mat);
  255. mat = combineCells(mat, maxWidth);
  256. return combineRows(mat, maxHeight);
  257. }
  258. function handleCases(cases) {
  259. let mat = assembleRows(cases);
  260. mat.forEach((row) => {
  261. row.cells = row.cells.slice(0, -1);
  262. row.width = row.width.slice(0, -1);
  263. });
  264. const [maxHeight, maxWidth] = getMaxParameters(mat);
  265. mat = combineCells(mat, maxWidth);
  266. return combineRows(mat, maxHeight);
  267. }
  268. function handleCayley(cayley) {
  269. let mat = assembleRows(cayley);
  270. mat.forEach((row) => {
  271. row.cells = row.cells.slice(1).slice(0, -1);
  272. row.width = row.width.slice(1).slice(0, -1);
  273. row.sep = row.sep + row.sep;
  274. });
  275. const [maxHeight, maxWidth] = getMaxParameters(mat);
  276. const bar = {
  277. lfence: '',
  278. rfence: '',
  279. cells: maxWidth.map((x) => '⠐' + new Array(x).join('⠒')),
  280. width: maxWidth,
  281. height: 1,
  282. sep: mat[0].sep
  283. };
  284. if (Engine.getInstance().cayleyshort && mat[0].cells[0] === '⠀') {
  285. bar.cells[0] = '⠀';
  286. }
  287. mat.splice(1, 0, bar);
  288. mat = combineCells(mat, maxWidth);
  289. return combineRows(mat, maxHeight);
  290. }
  291. function handleRow(row) {
  292. const children = Array.from(row.childNodes);
  293. const lfence = getFence(children[0]);
  294. const rfence = getFence(children[children.length - 1]);
  295. if (lfence) {
  296. children.shift();
  297. }
  298. if (rfence) {
  299. children.pop();
  300. }
  301. let sep = '';
  302. const cells = [];
  303. for (const child of children) {
  304. if (child.nodeType === DomUtil.NodeType.TEXT_NODE) {
  305. sep = child.textContent;
  306. continue;
  307. }
  308. const result = applyHandler(child);
  309. cells.push(result);
  310. }
  311. return {
  312. lfence: lfence,
  313. rfence: rfence,
  314. sep: sep,
  315. cells: cells,
  316. height: cells.reduce((max, x) => Math.max(strHeight(x), max), 0),
  317. width: cells.map(strWidth)
  318. };
  319. }
  320. function centerCell(cell, width) {
  321. const cw = strWidth(cell);
  322. const center = (width - cw) / 2;
  323. const [lpad, rpad] = Math.floor(center) === center
  324. ? [center, center]
  325. : [Math.floor(center), Math.ceil(center)];
  326. const lines = cell.split(/\r\n|\r|\n/);
  327. const result = [];
  328. const [lstr, rstr] = [
  329. new Array(lpad + 1).join('⠀'),
  330. new Array(rpad + 1).join('⠀')
  331. ];
  332. for (const line of lines) {
  333. result.push(lstr + line + rstr);
  334. }
  335. return result.join('\n');
  336. }
  337. function handleFraction(frac) {
  338. const [open, num, , den, close] = Array.from(frac.childNodes);
  339. const numerator = applyHandler(num);
  340. const denominator = applyHandler(den);
  341. const nwidth = strWidth(numerator);
  342. const dwidth = strWidth(denominator);
  343. let maxWidth = Math.max(nwidth, dwidth);
  344. const bar = open + new Array(maxWidth + 1).join('⠒') + close;
  345. maxWidth = bar.length;
  346. return (`${centerCell(numerator, maxWidth)}\n${bar}\n` +
  347. `${centerCell(denominator, maxWidth)}`);
  348. }
  349. function handleFractionPart(prt) {
  350. const fchild = prt.firstChild;
  351. const content = recurseTree(prt);
  352. if (fchild && fchild.nodeType === DomUtil.NodeType.ELEMENT_NODE) {
  353. if (DomUtil.tagName(fchild) === 'ENGLISH') {
  354. return '⠰' + content;
  355. }
  356. if (DomUtil.tagName(fchild) === 'NUMBER') {
  357. return '⠼' + content;
  358. }
  359. }
  360. return content;
  361. }
  362. function handleRelation(rel) {
  363. if (!Engine.getInstance().linebreaks) {
  364. return recurseTree(rel);
  365. }
  366. const value = relValues.get(parseInt(rel.getAttribute('value')));
  367. return (value ? `<br value="${value}"/>` : '') + recurseTree(rel);
  368. }