@mathzetta/inlinemath 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +326 -2
  3. package/dist/cas.external.js +2 -0
  4. package/dist/cas.js +2 -0
  5. package/dist/chunks/calculate-Cz87Lt0N.js +2 -0
  6. package/dist/chunks/draw-Bev17BJJ.js +2 -0
  7. package/dist/chunks/edit-C65xXzo8.js +2 -0
  8. package/dist/chunks/edit-M68aRA5o.js +2 -0
  9. package/dist/chunks/html-BimAqn7x.js +2 -0
  10. package/dist/chunks/html-CupluJSh.js +2 -0
  11. package/dist/chunks/lite-3Fb-OHbt.js +2 -0
  12. package/dist/fonts/GUST-FONT-LICENSE.txt +30 -0
  13. package/dist/fonts/fonts.css +17 -0
  14. package/dist/fonts/lmroman12-italic.woff2 +0 -0
  15. package/dist/fonts/lmroman12-regular.woff2 +0 -0
  16. package/dist/inlinemath-cas.external.global.min.js +2 -0
  17. package/dist/inlinemath-cas.global.min.js +2 -0
  18. package/dist/inlinemath-lite-calc.global.min.js +2 -0
  19. package/dist/inlinemath-lite.external.global.min.js +2 -0
  20. package/dist/inlinemath-lite.global.min.js +2 -0
  21. package/dist/inlinemath.css +158 -0
  22. package/dist/inlinemath.external.global.min.js +2 -0
  23. package/dist/inlinemath.external.js +2 -0
  24. package/dist/inlinemath.global.min.js +2 -0
  25. package/dist/inlinemath.js +2 -0
  26. package/dist/lite-calc.js +2 -0
  27. package/dist/lite.external.js +2 -0
  28. package/dist/lite.js +2 -0
  29. package/dist/markdown.external.js +2 -0
  30. package/dist/markdown.js +2 -0
  31. package/fonts/GUST-FONT-LICENSE.txt +30 -0
  32. package/fonts/fonts.css +17 -0
  33. package/fonts/lmroman12-italic.woff2 +0 -0
  34. package/fonts/lmroman12-regular.woff2 +0 -0
  35. package/inlinemath.css +158 -0
  36. package/package.json +95 -4
  37. package/src/calculator.js +6 -0
  38. package/src/cas.d.ts +3 -0
  39. package/src/cas.js +149 -0
  40. package/src/draw.js +72 -0
  41. package/src/edit.js +96 -0
  42. package/src/glyphs.js +40 -0
  43. package/src/html.js +32 -0
  44. package/src/index.d.ts +3 -0
  45. package/src/index.js +17 -0
  46. package/src/layout.js +649 -0
  47. package/src/lite-calc.d.ts +3 -0
  48. package/src/lite-calc.js +15 -0
  49. package/src/lite.d.ts +3 -0
  50. package/src/lite.js +18 -0
  51. package/src/markdown.d.ts +3 -0
  52. package/src/markdown.js +676 -0
  53. package/src/mathzetta.js +18 -0
  54. package/src/select.js +108 -0
  55. package/src/types.d.ts +204 -0
  56. package/src/variables.js +228 -0
  57. package/src/view.js +382 -0
@@ -0,0 +1,2 @@
1
+ /* Inline Math 0.2.0 — MIT licence; includes mathzetta (and @mathzetta/cas, for the CAS) */
2
+ import{t as e,a as t}from"./chunks/html-CupluJSh.js";import{a as n}from"./chunks/calculate-Cz87Lt0N.js";import{v as s,o as r,r as a,u as i,s as o,E as l,b as u,n as d,i as p}from"./chunks/edit-M68aRA5o.js";export{V as Variables,c as createVariables}from"./chunks/edit-M68aRA5o.js";import{p as m,n as f,g as h,d as $}from"./chunks/lite-3Fb-OHbt.js";const g=new RegExp("(\\d+)","g");function v(e,{delimiters:t=["$$","$","\\[","\\("]}={}){const n=[];let s="",r=0;const a=String(e),i=e=>0===e||"\n"===a[e-1],o=(e,t,s)=>(n.push({source:e,display:t,raw:s,start:r,end:r+s.length}),`${n.length-1}`);for(;r<a.length;){if(i(r)){const e=/^( {0,3})(`{3,}|~{3,})([^\n]*)\n?/.exec(a.slice(r));if(e){const t=e[2],n=new RegExp(`\\n {0,3}${"`"===t[0]?"`":"~"}{${t.length},}[ \\t]*(?=\\n|$)`),i=a.slice(r+e[0].length),c=n.exec(`\n${i}`),l=c?i.slice(0,Math.max(0,c.index-1)):i,u=e[0].length+(c?c.index-1+c[0].length:i.length),d=a.slice(r,r+u);"math"===e[3].trim()?s+=o(l,!0,d):s+=d,r+=u;continue}}const e=a[r];if("`"===e){const e=/^`+/.exec(a.slice(r))[0],t=a.indexOf(e,r+e.length);if(t>-1){s+=a.slice(r,t+e.length),r=t+e.length;continue}s+=e,r+=e.length;continue}if("\\"===e){const e=a.slice(r,r+2),n="\\["===e?"\\]":"\\("===e?"\\)":null;if(n&&t.includes(e)){const t=a.indexOf(n,r+2);if(t>-1){s+=o(a.slice(r+2,t),"\\["===e,a.slice(r,t+2)),r=t+2;continue}}s+=e,r+=2;continue}if("$"===e)if("$"===a[r+1]&&t.includes("$$")){const e=a.indexOf("$$",r+2);if(e>-1){s+=o(a.slice(r+2,e),!0,a.slice(r,e+2)),r=e+2;continue}}else if(t.includes("$")&&a[r+1]&&!/\s/.test(a[r+1])){const e=b(a,r+1);if(e>-1){s+=o(a.slice(r+1,e),!1,a.slice(r,e+1)),r=e+1;continue}}s+=e,r++}return{text:s,math:n}}function b(e,t){for(let n=t;n<e.length;n++){const t=e[n];if("\\"!==t){if("`"===t)return-1;if("\n"===t&&/^\n[ \t]*\n/.test(e.slice(n)))return-1;if("$"===t&&!/\s/.test(e[n-1])&&!/\d/.test(e[n+1]??"")&&"$"!==e[n+1])return n}else n++}return-1}const x=e=>String(e).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;"),y=/^\s*([A-Za-z\u0370-\u03ff](?:_\{?\w+\}?)?)\s*=\s*([^=<>≤≥]+)$/;function k(s,r,a,i){const{text:c,math:m}=v(s,r),f=a?a.definitions(i):new Map,g=e=>a?a.textOf(e):e.text,b={...r.scope??{}},k={},w=new Map,S=new Set,j=[],E=()=>{for(const e of Object.keys(k))delete k[e];Object.assign(k,b);for(const e of f.keys())delete k[e];let e=[...f.values()];for(;e.length;){const t=e.filter(e=>{try{const t=n(g(e),k);if("number"==typeof t.value||"bigint"==typeof t.value||Array.isArray(t.value))return k[e.name]=t.exact?t.text:t.value,!1}catch{}return!0});if(t.length===e.length)break;e=t}const t={},s=_({});for(const e of f.keys()){const n=s(e);if(n)try{t[e]=h(o(n,t=>t===e?null:s(t)).tree)}catch{}}k[l]=t},_=t=>{const n=new Map;return s=>{const r=f.get(s);if(!r)return null;if(!n.has(s)){let a=null;try{a=e(g(r),t)}catch{}n.set(s,a)}return n.get(s)}};E();const M=m.map((s,a)=>{const{source:i,display:c}=s,l=c?"mv-md-display":"mv-md-math",m=(e,t="")=>`<span class="${l}${t}" data-mv-md="${a}">${e}</span>`;try{const l={...r.math,display:c,blocks:r.blocks,scope:k},h=function(t){let n=t.trim();const s=n.startsWith("_");s&&(n=n.slice(1).trimStart());let r=n.indexOf(":="),a=2;r<0&&s&&(r=n.search(/(?<![<>!≤≥=])=(?!=)/),a=1);const i=s?{error:"a hidden $_…$ saves a variable: $_A := 2+2$"}:null;if(r<=0)return i;const o=n.slice(0,r).trim(),c=n.slice(r+a).trim();let l=null;try{l=/\{\{/.test(o)?null:d(e(o))}catch{}return null!==l&&c?{hidden:s,lhs:o,name:l,rhs:c,input:p(c)}:i}(i);if(h&&h.error)throw new Error(h.error);if(h){const{name:n,input:r,hidden:i}=h,c=r?{name:n,text:r.default,input:r,key:`input:${JSON.stringify(r)}`}:{name:n,text:h.rhs,input:null,key:`expr:${h.rhs}`,start:s.start,end:s.end};let d="";if(r){const e=function(e,t){const{input:n,name:s}=e,r=`class="mv-md-input mv-md-${n.kind}" data-mv-var="${x(s)}" aria-label="${x(s)}"`;if("dropdown"===n.kind)return`<select ${r}>${n.options.map(e=>`<option value="${x(e)}"${e===t?" selected":""}>${x(e)}</option>`).join("")}</select>`;return"slider"===n.kind?`<input type="range" ${r} min="${n.min}" max="${n.max}" step="${n.step}" value="${x(t)}"><output class="mv-md-output" data-mv-out="${x(s)}">${x(t)}</output>`:`<input type="text" ${r} value="${x(t)}" size="${Math.max(4,t.length+1)}" spellcheck="false" autocomplete="off">`}(c,g(c));d=i?e:`${t(h.lhs,l)}<span class="mv-md-assign" aria-hidden="true">≔</span>${e}`,S.add(a)}else{const n=o(e(h.rhs,l),_(l)).tree;if(!i){const s=$({type:"op",op:"=",args:[u(e(h.lhs)),void 0===n.id?n:u(n)]});j[a]=s,d=function(e){return e.replace('<span class="mv-rel">=</span>','<span class="mv-rel mv-rel-def">≔</span>').replace(/(aria-label="[^"]*?) equals /,"$1 is defined as ").replace("<mo>=</mo>","<mo>≔</mo>")}(t(s,l))}}return f.delete(n),f.set(n,c),w.set(n,c),E(),i&&!r?"":m(d,r?" mv-md-input-def":"")}const{tree:v,changed:M}=o(e(i,l),_(l));M&&$(v),j[a]=v;const O=t(v,l);return!M&&!1!==r.define&&function(e,t,s){const r=y.exec(e);if(!r||/\{\{/.test(r[2]))return!1;const a=r[1].replace(/[{}]/g,"");if(s.has(a))return!1;try{const e=n(r[2],t);if("number"==typeof e.value||"bigint"==typeof e.value||Array.isArray(e.value))return t[a]=e.exact?e.text:e.value,!0}catch{}return!1}(i,b,f)&&E(),m(O)}catch(e){return`<span class="mv-md-error" data-mv-md="${a}" title="${x(e.message)}"><code>${x(s.raw)}</code></span>`}});return{text:c,math:m,drawn:M,defs:w,inputs:S,trees:j}}const w=/^(https?:|mailto:|#|\/|\.{0,2}\/|[^:]*$)/i,S=e=>w.test(e.trim())?x(e.trim()):"#";function j(e,t,n){const s=[],r=e=>(s.push(e),`${s.length-1}`);let a=e;return a=a.replace(/(`+)([\s\S]*?[^`])\1(?!`)/g,(e,t,s)=>r(`<code>${x(n(s.trim()))}</code>`)),a=a.replace(/\\([\\`*_{}[\]()#+\-.!|~$<>"'])/g,(e,t)=>r(x(t))),a=a.replace(/<((?:https?:\/\/|mailto:)[^\s>]+)>/g,(e,t)=>r(`<a href="${S(t)}">${x(t)}</a>`)),t.html&&(a=a.replace(/<\/?[A-Za-z][^>]*>/g,e=>r(e))),a=a.replace(/!\[([^\]]*)\]\(\s*((?:[^\s()]|\([^\s()]*\))+)(?:\s+"([^"]*)")?\s*\)/g,(e,t,n,s)=>r(`<img src="${S(n)}" alt="${x(t)}"${s?` title="${x(s)}"`:""}>`)),a=a.replace(/\[([^\]]+)\]\(\s*((?:[^\s()]|\([^\s()]*\))+)(?:\s+"([^"]*)")?\s*\)/g,(e,s,a,i)=>r(`<a href="${S(a)}"${i?` title="${x(i)}"`:""}>${j(s,t,n)}</a>`)),a=x(a),a=a.replace(/(\*\*|__)(?=\S)([\s\S]*?\S)\1/g,"<strong>$2</strong>"),a=a.replace(/(^|[^*\w])\*(?=\S)([\s\S]*?\S)\*(?!\*)/g,"$1<em>$2</em>"),a=a.replace(/(^|[^_\w])_(?=\S)([\s\S]*?\S)_(?![_\w])/g,"$1<em>$2</em>"),a=a.replace(/~~(?=\S)([\s\S]*?\S)~~/g,"<del>$1</del>"),a=a.replace(/( {2,}|\\)\n/g,"<br>\n"),a.replace(/(\d+)/g,(e,t)=>s[Number(t)])}const E=/^( {0,3})(#{1,6}\s|>|```|~~~|([-*+]|\d{1,9}[.)])\s|(\*[ \t]*){3,}$|(-[ \t]*){3,}$|(_[ \t]*){3,}$)/,_=/^( {0,3})([-*+]|\d{1,9}[.)])(\s+|$)/,M=/^\s*\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/;function O(e,t,n){const s=[];let r=0;const a=e=>j(e,t,n);for(;r<e.length;){const i=e[r];if(!i.trim()){r++;continue}const o=/^ {0,3}(`{3,}|~{3,})\s*([^\s`]*)/.exec(i);if(o){const t=[];for(r++;r<e.length&&!new RegExp(`^ {0,3}${"`"===o[1][0]?"`":"~"}{${o[1].length},}\\s*$`).test(e[r]);)t.push(e[r++]);r++;const a=o[2]?` class="language-${x(o[2])}"`:"";s.push(`<pre><code${a}>${x(n(t.join("\n")))}</code></pre>`);continue}const c=/^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/.exec(i);if(c){s.push(`<h${c[1].length}>${a(c[2])}</h${c[1].length}>`),r++;continue}if(/^ {0,3}((\*[ \t]*){3,}|(-[ \t]*){3,}|(_[ \t]*){3,})$/.test(i)){s.push("<hr>"),r++;continue}if(/^ {0,3}>/.test(i)){const a=[];for(;r<e.length&&e[r].trim()&&(/^ {0,3}>/.test(e[r])||a.length);)a.push(e[r].replace(/^ {0,3}> ?/,"")),r++;s.push(`<blockquote>\n${O(a,t,n)}\n</blockquote>`);continue}const l=_.exec(i);if(l){const a=/\d/.test(l[2]),i=a?parseInt(l[2],10):1,o=[];let c=!1;for(;r<e.length;){const t=_.exec(e[r]);if(!t||/\d/.test(t[2])!==a)break;const n=t[0].length,s=[e[r].slice(n)];for(r++;r<e.length;){const t=e[r];if(!t.trim()){const t=e[r+1];if(void 0!==t&&t.startsWith(" ".repeat(Math.min(n,4)))&&t.trim()){s.push(""),c=!0,r++;continue}void 0!==t&&_.test(t)&&(c=!0);break}if(t.startsWith(" ".repeat(Math.min(n,4))))s.push(t.slice(Math.min(n,4)));else{if(_.test(t)||E.test(t))break;s.push(t)}r++}for(o.push(s);r<e.length&&!e[r].trim()&&void 0!==e[r+1]&&_.test(e[r+1]);)r++}const u=a?"ol":"ul",d=o.map(e=>{let s=O(e,t,n);return c||(s=s.replace(/^<p>([\s\S]*?)<\/p>/,"$1").replace(/\n<p>([\s\S]*?)<\/p>/g,"\n$1")),`<li>${s}</li>`});s.push(`<${u}${a&&1!==i?` start="${i}"`:""}>\n${d.join("\n")}\n</${u}>`);continue}if(i.includes("|")&&void 0!==e[r+1]&&M.test(e[r+1])&&e[r+1].includes("-")){const t=e=>e.trim().replace(/^\|/,"").replace(/\|$/,"").split(/(?<!\\)\|/).map(e=>e.trim()),n=t(e[r+1]).map(e=>e.startsWith(":")&&e.endsWith(":")?"center":e.endsWith(":")?"right":e.startsWith(":")?"left":""),o=(e,t,s)=>`<${e}${n[s]?` style="text-align:${n[s]}"`:""}>${a(t)}</${e}>`,c=t(i);r+=2;const l=[];for(;r<e.length&&e[r].trim()&&e[r].includes("|");)l.push(t(e[r++]));s.push(`<table>\n<thead><tr>${c.map((e,t)=>o("th",e,t)).join("")}</tr></thead>\n<tbody>\n${l.map(e=>`<tr>${c.map((t,n)=>o("td",e[n]??"",n)).join("")}</tr>`).join("\n")}\n</tbody>\n</table>`);continue}if(/^( {4}|\t)/.test(i)){const t=[];for(;r<e.length&&(/^( {4}|\t)/.test(e[r])||!e[r].trim());)t.push(e[r++].replace(/^( {4}|\t)/,""));for(;t.length&&!t[t.length-1].trim();)t.pop();s.push(`<pre><code>${x(n(t.join("\n")))}</code></pre>`);continue}const u=[];for(;r<e.length&&e[r].trim()&&(!u.length||!E.test(e[r])||/^ {0,3}[-=]+\s*$/.test(e[r]));){if(u.length&&/^ {0,3}(=+|-+)\s*$/.test(e[r])){const t="="===e[r].trim()[0]?1:2;s.push(`<h${t}>${a(u.join("\n"))}</h${t}>`),u.length=0,r++;break}u.push(e[r].replace(/^ {0,3}/,"")),r++}u.length&&s.push(`<p>${a(u.join("\n"))}</p>`)}return s.join("\n")}function A(e,t={},n=e=>e){return O(String(e).replace(/\r\n?/g,"\n").split("\n"),t,n)}function N({text:e,math:t,drawn:n},s){return(s.render?s.render(e):A(e,s,e=>e.replace(g,(e,n)=>t[Number(n)].raw))).replace(new RegExp("<p>\\s*(\\d+)\\s*</p>","g"),(e,s)=>t[Number(s)].display&&n[Number(s)]?`<div class="mv-md-block">${n[Number(s)]}</div>`:e).replace(g,(e,t)=>n[Number(t)]).replace(/<p>\s*<\/p>\n?/g,"")}function L(e,t={}){return N(k(e,t,t.variables||null),t)}const W=new WeakMap,q=new Set;function C(){for(const e of q)e.connected()&&!e.element.isConnected&&e.stop(!0)}function R(t,n,o={}){const c=void 0===o.variables?s:o.variables||null;C();const l=W.get(t);l&&l.stop(l.store!==c);let u=k(n,o,c,t);if(t.innerHTML=N(u,o),!c)return W.delete(t),t;c.own(t,u.defs);let d=t.isConnected;const p=n=>{const s=n.target,r=s&&s.dataset&&s.dataset.mvVar;if(void 0!==r&&t.contains(s)){if("text"===s.type)try{e(s.value,{blocks:o.blocks,scope:{}}),s.removeAttribute("aria-invalid")}catch{return void s.setAttribute("aria-invalid","true")}c.set(r,s.value)}},h=s=>{const l=s.target.closest?.("[data-node]"),d=l&&l.closest("[data-mv-md]");if(!d||!t.contains(d))return;const p=Number(d.dataset.mvMd),h=u.trees[p];if(!h)return;const $=Number(l.dataset.node),g=m(h,$)||[];let v=null;for(let e=g.length;e>=0&&!v;e--){const t=f(h,g.slice(0,e));t&&void 0!==t.from&&(v={node:t,depth:e})}const b=v&&c.definitions().get(v.node.from);if(b&&b.input&&"textbox"===b.input.kind){s.preventDefault();const n=b.name,u=t.ownerDocument.querySelector(`[data-mv-var="${CSS.escape(n)}"]`),d=o.editable&&o.editable.toText;if(!d)return void(u&&(u.focus(),u.select()));const p=t=>e(t),m=f(h,g);return void r(l,d(i(m,p)),{label:`Edit ${n}`,parse:t=>e(t,{blocks:o.blocks}),commit:e=>{const t=a(i(v.node,p),g.slice(v.depth),e);c.set(n,d(t))}})}const x=b&&u.defs.get(b.name),y=x&&x.key===b.key&&void 0!==x.start?{...u.math[p],start:x.start,end:x.end,raw:n.slice(x.start,x.end)}:u.math[p];t.dispatchEvent(new CustomEvent("inlinemath:select",{bubbles:!0,detail:{index:p,start:y.start,end:y.end,raw:y.raw,source:y.source,id:$,path:g}}))};t.addEventListener("input",p),t.addEventListener("change",p),t.addEventListener("dblclick",h);const $=c.subscribe(e=>{e!==t&&(t.isConnected?(d=!0,C(),(()=>{const e=k(n,o,c,t);e.drawn.forEach((n,s)=>{if(n===u.drawn[s]||e.inputs.has(s))return;const r=t.querySelector(`[data-mv-md="${s}"]`);r&&(r.outerHTML=n)});for(const n of t.querySelectorAll("[data-mv-var], [data-mv-out]")){const t=e.defs.get(n.dataset.mvVar??n.dataset.mvOut);if(!t)continue;const s=c.textOf(t);void 0!==n.dataset.mvOut?n.textContent=s:n.value!==s&&n!==n.ownerDocument?.activeElement&&(n.value=s)}u=e})()):d&&g(!0))});function g(e){$(),t.removeEventListener("input",p),t.removeEventListener("change",p),t.removeEventListener("dblclick",h),q.delete(v),W.delete(t),e&&c.own(t,null)}const v={element:t,store:c,stop:g,connected:()=>d};return q.add(v),W.set(t,v),t}export{v as extractMath,L as markdownToHTML,A as renderMarkdown,R as renderMarkdownInto,s as variables};
@@ -0,0 +1,30 @@
1
+ % This is a preliminary version (2006-09-30), barring acceptance from
2
+ % the LaTeX Project Team and other feedback, of the GUST Font License.
3
+ % (GUST is the Polish TeX Users Group, http://www.gust.org.pl)
4
+ %
5
+ % For the most recent version of this license see
6
+ % http://www.gust.org.pl/fonts/licenses/GUST-FONT-LICENSE.txt
7
+ % or
8
+ % http://tug.org/fonts/licenses/GUST-FONT-LICENSE.txt
9
+ %
10
+ % This work may be distributed and/or modified under the conditions
11
+ % of the LaTeX Project Public License, either version 1.3c of this
12
+ % license or (at your option) any later version.
13
+ %
14
+ % Please also observe the following clause:
15
+ % 1) it is requested, but not legally required, that derived works be
16
+ % distributed only after changing the names of the fonts comprising this
17
+ % work and given in an accompanying "manifest", and that the
18
+ % files comprising the Work, as listed in the manifest, also be given
19
+ % new names. Any exceptions to this request are also given in the
20
+ % manifest.
21
+ %
22
+ % We recommend the manifest be given in a separate file named
23
+ % MANIFEST-<fontid>.txt, where <fontid> is some unique identification
24
+ % of the font family. If a separate "readme" file accompanies the Work,
25
+ % we recommend a name of the form README-<fontid>.txt.
26
+ %
27
+ % The latest version of the LaTeX Project Public License is in
28
+ % http://www.latex-project.org/lppl.txt and version 1.3c or later
29
+ % is part of all distributions of LaTeX version 2006/05/20 or later.
30
+
@@ -0,0 +1,17 @@
1
+ /*
2
+ * Optional: Latin Modern Roman (the TeX look), about 95 kB for both files.
3
+ * Link this before inlinemath.css; without it Inline Math uses the system serif.
4
+ * Licence: GUST Font License, see GUST-FONT-LICENSE.txt.
5
+ */
6
+ @font-face {
7
+ font-family: "Inline Math Roman";
8
+ src: url("lmroman12-regular.woff2") format("woff2");
9
+ font-style: normal;
10
+ font-display: swap;
11
+ }
12
+ @font-face {
13
+ font-family: "Inline Math Roman";
14
+ src: url("lmroman12-italic.woff2") format("woff2");
15
+ font-style: italic;
16
+ font-display: swap;
17
+ }
Binary file
Binary file
package/inlinemath.css ADDED
@@ -0,0 +1,158 @@
1
+ /*
2
+ * Inline Math stylesheet. Everything is under .mv; colours are left to the page.
3
+ *
4
+ * The numbers marked "metric" must match the constants at the top of
5
+ * src/layout.js (line-height 1, bar 0.06em, gap 0.1em); everything else can
6
+ * be changed freely.
7
+ *
8
+ * Handy hooks:
9
+ * .mv-n[data-node="4"] one node (ids from MathZetta)
10
+ * .mv-var .mv-num .mv-word variables, numbers, function names
11
+ * .mv-op-frac .mv-op-pow … a kind of operation
12
+ * .mv-hl the class highlight() adds
13
+ * --mv-hl highlight colour
14
+ */
15
+
16
+ .mv {
17
+ display: inline-block;
18
+ font-family: "Inline Math Roman", "Latin Modern Roman", "STIX Two Text", "Cambria", "Times New Roman", serif;
19
+ font-size: 1.2em;
20
+ font-style: normal;
21
+ font-weight: normal;
22
+ line-height: 1; /* metric */
23
+ white-space: nowrap;
24
+ text-indent: 0;
25
+ text-align: left;
26
+ letter-spacing: 0;
27
+ }
28
+ .mv-display { display: block; text-align: center; margin: 0.6em 0; }
29
+ .mv span, .mv svg { line-height: 1; }
30
+ .mv-n { display: inline-block; border-radius: 0.12em; }
31
+
32
+ /* ---- letters and signs -------------------------------------------------- */
33
+ .mv-var { font-style: italic; padding-right: 0.03em; }
34
+ .mv-num, .mv-word, .mv-name, .mv-ord, .mv-text, .mv-d { font-style: normal; }
35
+ .mv-prime { margin-left: 0.04em; }
36
+ .mv-bin { margin: 0 0.22em; }
37
+ .mv-rel { margin: 0 0.28em; }
38
+ .mv-punct { margin-right: 0.17em; }
39
+ .mv-sign { margin-right: 0.04em; }
40
+ .mv-times { display: inline-block; width: 0.06em; }
41
+ .mv-callgap { display: inline-block; width: 0.05em; }
42
+ .mv-fnspace { display: inline-block; width: 0.17em; }
43
+ .mv-space { display: inline-block; width: 0.33em; }
44
+ .mv-dx { display: inline-block; margin-left: 0.17em; }
45
+ .mv-text { margin: 0 0.25em; }
46
+ /* Tighter spacing in limits and exponents: n=1, x→0. */
47
+ .mv-script .mv-rel, .mv-sup .mv-rel, .mv-sub .mv-rel { margin: 0 0.12em; }
48
+ .mv-script .mv-bin, .mv-sup .mv-bin, .mv-sub .mv-bin { margin: 0 0.1em; }
49
+
50
+ /* ---- fractions (metric: padding and border) ------------------------------ */
51
+ .mv-frac { display: inline-flex; flex-direction: column; align-items: stretch; margin: 0 0.12em; }
52
+ .mv-numer, .mv-denom { display: block; text-align: center; padding: 0 0.1em; }
53
+ .mv-numer { padding-bottom: 0.1em; }
54
+ .mv-denom { padding-top: 0.1em; border-top: 0.06em solid currentColor; }
55
+ .mv-frac-nobar > .mv-denom { border-top: 0; }
56
+ .mv-scaled { display: inline-block; }
57
+
58
+ /* ---- scripts ----------------------------------------------------------------- */
59
+ .mv-sup, .mv-sub { display: inline-block; margin-left: 0.04em; }
60
+ .mv-subsup, .mv-limits { display: inline-flex; flex-direction: column; justify-content: space-between; align-items: flex-start; margin-left: 0.04em; }
61
+ .mv-limits { margin-left: 0.02em; margin-right: 0.12em; }
62
+ .mv-limits > .mv-lower { margin-left: -0.3em; }
63
+ .mv-script { display: block; }
64
+ .mv-under { display: inline-flex; flex-direction: column; align-items: center; margin-right: 0.15em; }
65
+ .mv-under > .mv-script { margin-top: 0.05em; }
66
+ .mv-bigop { display: inline-flex; flex-direction: column; align-items: center; margin: 0 0.12em; }
67
+ .mv-bigop > .mv-script { display: block; margin: 0.08em 0; }
68
+ .mv-bigop > svg { display: block; }
69
+
70
+ /* ---- radicals (metric: border and padding-top) ----------------------------- */
71
+ .mv-sqrt { display: inline-block; white-space: nowrap; }
72
+ .mv-hook { display: inline-block; overflow: visible; margin-left: 0.05em; }
73
+ .mv-radicand { display: inline-block; border-top: 0.06em solid currentColor; padding: 0.1em 0.1em 0 0.06em; }
74
+ .mv-rootindex { display: inline-block; margin: 0 -0.32em 0 0.12em; }
75
+
76
+ /* ---- delimiters: drawn at the full height of what they enclose -------------- */
77
+ .mv-fence { display: inline-block; position: relative; }
78
+ .mv-delim { position: absolute; top: 0; bottom: 0; height: 100%; }
79
+ .mv-delim-l { left: 0.03em; }
80
+ .mv-delim-r { right: 0.03em; }
81
+ span.mv-delim { width: 0.16em; border: 0 solid currentColor; box-sizing: border-box; }
82
+ .mv-delim.mv-lbrack { border-width: 0.06em 0 0.06em 0.06em; left: 0.06em; }
83
+ .mv-delim.mv-rbrack { border-width: 0.06em 0.06em 0.06em 0; right: 0.06em; }
84
+ .mv-delim.mv-vert { border-left-width: 0.06em; width: 0; left: 0.07em; }
85
+ .mv-delim.mv-vert.mv-delim-r { left: auto; right: 0.07em; }
86
+ .mv-delim.mv-lfloor { border-width: 0 0 0.06em 0.06em; }
87
+ .mv-delim.mv-rfloor { border-width: 0 0.06em 0.06em 0; }
88
+ .mv-delim.mv-lceil { border-width: 0.06em 0 0 0.06em; }
89
+ .mv-delim.mv-rceil { border-width: 0.06em 0.06em 0 0; }
90
+
91
+ /* ---- shapes ----------------------------------------------------------------- */
92
+ .mv svg { overflow: visible; }
93
+ .mv svg path[stroke] { stroke-width: 0.065em; stroke-linecap: round; stroke-linejoin: round; }
94
+ .mv-intsign path[stroke] { stroke-width: 0.075em; }
95
+ .mv-bigsign { display: inline-block; margin: 0 0.06em; }
96
+ .mv-intsign { margin-right: 0.12em; }
97
+
98
+ /* ---- piecewise ------------------------------------------------------------- */
99
+ .mv-cases { display: inline-grid; grid-template-columns: auto auto; column-gap: 1em; row-gap: 0.25em; align-items: baseline; text-align: left; margin: 0 0.1em; }
100
+ .mv-case-value, .mv-case-cond { display: block; }
101
+ .mv-matrix { display: inline-grid; column-gap: 0.8em; row-gap: 0.3em; align-items: baseline; justify-items: center; margin: 0 0.15em; }
102
+ .mv-matrix-row { display: contents; }
103
+ .mv-matrix-cell { display: block; }
104
+
105
+ /* ---- decorations: \boxed \cancel \overline \vec … --------------------------- */
106
+ .mv-boxed { display: inline-block; border: 0.06em solid currentColor; padding: 0.16em 0.2em; }
107
+ .mv-over { display: inline-block; border-top: 0.06em solid currentColor; padding-top: 0.08em; }
108
+ .mv-underline { display: inline-block; border-bottom: 0.06em solid currentColor; padding-bottom: 0.06em; }
109
+ .mv-cancel { display: inline-block; position: relative; }
110
+ .mv-cancel-line { position: absolute; left: 0; top: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
111
+ .mv-accent { display: inline-block; position: relative; padding-top: 0.3em; }
112
+ .mv-accent-mark { position: absolute; top: 0; left: 0; right: 0; text-align: center; font-size: 0.7em; line-height: 0.6; }
113
+
114
+ /* ---- interaction -------------------------------------------------------------- */
115
+ .mv-hl { background: var(--mv-hl, rgba(255, 200, 0, 0.35)); box-shadow: 0 0 0 0.08em var(--mv-hl, rgba(255, 200, 0, 0.35)); }
116
+ /* Windows high contrast (and other forced colours) drops backgrounds and shadows: outline a highlight instead */
117
+ @media (forced-colors: active) {
118
+ .mv-hl { outline: 0.08em solid Highlight; outline-offset: 0.04em; }
119
+ }
120
+ .mv-clickable .mv-n { cursor: pointer; }
121
+ .mv-editable .mv-n:hover { outline: 1px dashed rgba(0, 0, 0, 0.25); }
122
+ /* the box editing a part in place (view.editable, or a Markdown textbox variable) */
123
+ .mv-edit { transform: translateY(-50%); z-index: 1000; box-sizing: content-box; padding: 0.1em 0.35em; font-family: inherit;
124
+ border: 1px solid #1a5fb4; border-radius: 0.25em; background: #fff; color: #111; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); }
125
+ .mv-edit[aria-invalid="true"] { border-color: #c33; outline: 2px solid #c33; }
126
+ /* the live region a view announces steps through ({ announce: true }): read, never seen */
127
+ .mv-sr-only, .mv-sr-math { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
128
+ .mv-clickable .mv-n:hover { outline: 1px dashed rgba(0, 0, 0, 0.25); }
129
+ /* the keyboard: the expression is one tab stop, and the part the arrow keys are on is ringed */
130
+ .mv[tabindex]:focus-visible { outline: 0.1em solid #1a5fb4; outline-offset: 0.12em; }
131
+ .mv[tabindex]:focus .mv-focus { outline: 0.1em solid #1a5fb4; outline-offset: 0.06em; border-radius: 0.1em; }
132
+ /* while a part is ringed, the whole expression's ring steps back so the part is the one that stands out */
133
+ .mv[tabindex]:focus-visible:has(.mv-focus) { outline: 1px dashed #1a5fb4; }
134
+ @media (forced-colors: active) {
135
+ .mv[tabindex]:focus-visible, .mv[tabindex]:focus .mv-focus { outline-color: Highlight; }
136
+ }
137
+
138
+ /* ---- steps (stepsHTML from '@mathzetta/inlinemath/cas') ---------------------------------------- */
139
+ .mv-steps { margin: 0; padding-left: 1.6em; }
140
+ .mv-step { margin: 0.5em 0 0.9em; }
141
+ .mv-step-name { display: block; font-size: 0.85em; font-weight: 600; color: #555; }
142
+ .mv-step-maths { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4em 0.8em; margin: 0.15em 0; }
143
+ .mv-step-arrow { color: #888; }
144
+ .mv-step-why { margin: 0.1em 0 0; font-size: 0.9em; color: #444; }
145
+
146
+ /* ---- Markdown with maths (markdownToHTML from '@mathzetta/inlinemath/markdown') ------------------- */
147
+ .mv-md-math { display: inline-block; vertical-align: middle; }
148
+ .mv-md-display { display: block; text-align: center; margin: 0.6em 0; overflow-x: auto; }
149
+ .mv-md-block { margin: 0.8em 0; }
150
+ .mv-md-error code { background: rgba(200, 60, 0, 0.08); color: #a33; border-bottom: 1px dotted #a33; }
151
+ /* saved variables: $A := 2+2$ draws A ≔ 2 + 2; $A := [[…]]$ an input that sets A */
152
+ .mv-md-input-def { display: inline-flex; align-items: center; gap: 0.3em; }
153
+ .mv-md-assign { margin: 0 0.1em; }
154
+ .mv-md-input { font: inherit; font-size: 0.9em; }
155
+ input.mv-md-textbox { font-family: inherit; padding: 0.05em 0.3em; field-sizing: content; min-width: 3ch; }
156
+ input.mv-md-textbox[aria-invalid="true"] { outline: 2px solid #c33; outline-offset: 1px; }
157
+ input.mv-md-slider { vertical-align: middle; }
158
+ .mv-md-output { font-variant-numeric: tabular-nums; min-width: 2ch; }
package/package.json CHANGED
@@ -1,6 +1,97 @@
1
1
  {
2
2
  "name": "@mathzetta/inlinemath",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Draw calculus-level maths as HTML and CSS from anything MathZetta reads, with every part addressable.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "types": "src/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/index.d.ts",
11
+ "default": "./src/index.js"
12
+ },
13
+ "./lite": {
14
+ "types": "./src/lite.d.ts",
15
+ "default": "./src/lite.js"
16
+ },
17
+ "./lite-calc": {
18
+ "types": "./src/lite-calc.d.ts",
19
+ "default": "./src/lite-calc.js"
20
+ },
21
+ "./cas": {
22
+ "types": "./src/cas.d.ts",
23
+ "default": "./src/cas.js"
24
+ },
25
+ "./markdown": {
26
+ "types": "./src/markdown.d.ts",
27
+ "default": "./src/markdown.js"
28
+ },
29
+ "./inlinemath.css": "./inlinemath.css",
30
+ "./fonts/fonts.css": "./fonts/fonts.css",
31
+ "./fonts/*": "./fonts/*",
32
+ "./dist/*": "./dist/*",
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "src/",
37
+ "dist/",
38
+ "fonts/",
39
+ "inlinemath.css",
40
+ "README.md",
41
+ "LICENSE"
42
+ ],
43
+ "scripts": {
44
+ "test": "node test/run.js",
45
+ "build": "node tools/build.mjs",
46
+ "prepublishOnly": "npm test && npm run build"
47
+ },
48
+ "peerDependencies": {
49
+ "@mathzetta/cas": "^0.1.0",
50
+ "mathzetta": "^1.1.0"
51
+ },
52
+ "devDependencies": {
53
+ "@rollup/plugin-terser": "^0.4.4",
54
+ "@mathzetta/cas": "^0.1.0",
55
+ "mathzetta": "^1.1.0",
56
+ "rollup": "^4.65.0"
57
+ },
58
+ "license": "MIT",
59
+ "engines": {
60
+ "node": ">=18"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "@mathzetta/cas": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "homepage": "https://inlinemath.com",
68
+ "unpkg": "dist/inlinemath.global.min.js",
69
+ "jsdelivr": "dist/inlinemath.global.min.js",
70
+ "publishConfig": {
71
+ "access": "public"
72
+ },
73
+ "keywords": [
74
+ "math",
75
+ "mathml",
76
+ "html",
77
+ "calculus",
78
+ "render",
79
+ "mathzetta",
80
+ "inline",
81
+ "markdown"
82
+ ],
83
+ "repository": {
84
+ "type": "git",
85
+ "url": "git+https://github.com/triplelog/mathzetta.git",
86
+ "directory": "packages/inlinemath"
87
+ },
88
+ "author": {
89
+ "name": "Robert Wilcox",
90
+ "email": "rwilcox@mathzetta.com",
91
+ "url": "https://mathzetta.com"
92
+ },
93
+ "bugs": {
94
+ "url": "https://github.com/triplelog/mathzetta/issues",
95
+ "email": "rwilcox@mathzetta.com"
96
+ }
97
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * MathZetta's calculator. Loading 'mathzetta/calculate' is what makes `{{3+4}}` in an input draw as
3
+ * 7: it registers the calculator with the parser. The full Inline Math (index.js, markdown, cas)
4
+ * imports this; the lite build (lite.js) doesn't.
5
+ */
6
+ export { calculateBlock, calculate } from 'mathzetta/calculate';
package/src/cas.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ // '@mathzetta/inlinemath/cas' — @mathzetta/cas in drawn maths (needs @mathzetta/cas). See types.d.ts.
2
+ export type { CAS, CASStep } from './types.js';
3
+ export { mathCAS, casBlocks, stepsHTML, tokensOf } from './types.js';
package/src/cas.js ADDED
@@ -0,0 +1,149 @@
1
+ /**
2
+ * '@mathzetta/inlinemath/cas' — @mathzetta/cas in Inline Math: `{{…}}` blocks that simplify,
3
+ * solve, differentiate …, and step lists drawn as maths.
4
+ *
5
+ * import { toHTML } from '@mathzetta/inlinemath';
6
+ * import { casBlocks, stepsHTML, mathCAS } from '@mathzetta/inlinemath/cas';
7
+ *
8
+ * toHTML('f′(x) = {{diff(x^3 sin x, x)}}', { blocks: casBlocks() })
9
+ * toHTML('{{solve(x^2 - 5x + 6 = 0, x)}}', { blocks: casBlocks() }) // x = 2 or x = 3
10
+ * stepsHTML(mathCAS().diff('x^2 sin x', 'x', { steps: 'brief' }).steps)
11
+ *
12
+ * Block functions: simplify, expand, factor, cancel, diff(f, x[, n]) (or der, derivative,
13
+ * differentiate), integrate(f, x[, a, b]) (or integral), solve(equation, x), approx(expr[, digits]);
14
+ * d/dx … and ∫ … without limits go to the CAS too. In Markdown, saved variables ($_f := x^3$)
15
+ * stand for their expressions: {{diff(f, x)}}. Everything else is MathZetta's calculator (calculateBlock), as without this.
16
+ *
17
+ * Needs @mathzetta/cas (a peer dependency, like mathzetta).
18
+ */
19
+
20
+ import { createCAS, toText as plainText } from '@mathzetta/cas';
21
+ import { parse as parsePostfix } from '@mathzetta/cas/engine';
22
+ import { parse, operator } from './mathzetta.js';
23
+ import { calculateBlock } from './calculator.js';
24
+ // Only this entry needs MathZetta's text writer (so only pages using inlinemath/cas map 'mathzetta/text').
25
+ import { toText } from 'mathzetta/text';
26
+ import { toHTML } from './html.js';
27
+
28
+ const EXPRESSIONS = Symbol.for('inlinemath.expressions'); // as variables.js
29
+
30
+ const shared = new Map();
31
+
32
+ /** How the complex functions are written for MathZetta: Re z, Im z, z̄ (an overline), arg z. */
33
+ const DRAWN = { re: 'Re', im: 'Im', conj: 'overline' };
34
+ const COMPLEX_NAMES = ['Re', 'Im', 're', 'im', 'conj', 'arg'];
35
+
36
+ /**
37
+ * A CAS node → MathZetta postfix tokens. Functions declared in a rule file
38
+ * (sinc) are MathZetta's user functions: x sinc → x 1 \:sinc.
39
+ */
40
+ export function tokensOf(node) {
41
+ const out = [];
42
+ const visit = (n) => {
43
+ if (n.type !== 'op') { out.push(n.token); return; }
44
+ n.args.forEach(visit);
45
+ if (operator(n.op)) {
46
+ if (operator(n.op).arity === -1) out.push(String(n.args.length));
47
+ out.push(n.op);
48
+ } else out.push(String(n.args.length), `\\:${DRAWN[n.op] ?? n.op}`);
49
+ };
50
+ visit(node);
51
+ return out;
52
+ }
53
+
54
+ /**
55
+ * A CAS that reads and writes MathZetta text. Options go to @mathzetta/cas's createCAS
56
+ * (rules: your rule files; complex: true for i = √-1); `names` are functions of your
57
+ * own, so `sinc(x)` is read as sinc and not as s·i·n·c(x). With complex, Re, Im,
58
+ * conj and arg are read as functions too.
59
+ */
60
+ export function mathCAS(options = {}) {
61
+ const key = options.rules || options.names ? null : options.complex ? 'complex' : 'default';
62
+ if (key && shared.has(key)) return shared.get(key);
63
+ const names = [...(options.names ?? []), ...(options.complex ? COMPLEX_NAMES : [])];
64
+ const cas = createCAS({
65
+ ...options,
66
+ read: (text) => parse(text, names.length ? { names } : {}),
67
+ // explanations read "3x^2", "sin(3x)"; a block's answer is MathZetta text, so it is read back exactly
68
+ format: plainText,
69
+ });
70
+ if (key) shared.set(key, cas);
71
+ return cas;
72
+ }
73
+
74
+ const CALLS = new Set(['simplify', 'expand', 'factor', 'cancel', 'diff', 'integrate', 'solve', 'approx']);
75
+ // other names people write for them
76
+ const ALIASES = { integral: 'integrate', differentiate: 'diff', approximate: 'approx', simp: 'simplify' };
77
+
78
+ /**
79
+ * A `{{…}}` handler: pass it as toHTML(input, { blocks: casBlocks() }).
80
+ * @param {object} [options] { cas } a CAS of your own (mathCAS({ rules })); { steps } how much working each call
81
+ * keeps (the CAS's steps option: true, 'brief', false), read from the handler's `last` after a block
82
+ */
83
+ export function casBlocks(options = {}) {
84
+ const cas = options.cas ?? mathCAS();
85
+ const how = options.steps === undefined ? {} : { steps: options.steps };
86
+ const handler = (text, scope) => {
87
+ let tokens;
88
+ try {
89
+ tokens = cas.read(text);
90
+ } catch {
91
+ return calculateBlock(text, scope);
92
+ }
93
+ // saved variables ($_f := x^3$ in Markdown) stand for their expressions: {{diff(f, x)}}
94
+ const saved = scope && scope[EXPRESSIONS];
95
+ if (saved) tokens = tokens.flatMap((t) => (Object.hasOwn(saved, t) ? saved[t] : [t]));
96
+ const head = tokens[tokens.length - 1];
97
+ const name = head?.startsWith('\\:') ? (ALIASES[head.slice(2)] ?? head.slice(2)) : null;
98
+ const symbolic = name && CALLS.has(name);
99
+ // d/dx …, ∫ … dx and ∫ from a to b … dx (MathZetta writes the limits as int's 3rd and 4th operands)
100
+ const calculus = head === 'der' || head === 'int';
101
+ if (!symbolic && !calculus) return calculateBlock(text, scope);
102
+ const node = cas.node(tokens);
103
+ const args = symbolic ? node.args : [node];
104
+ const word = (n) => (n ? n.s : undefined);
105
+ let r;
106
+ try {
107
+ switch (symbolic ? name : head === 'der' ? 'derivative' : 'antiderivative') {
108
+ case 'simplify': r = cas.simplify(args[0], how); break;
109
+ case 'expand': r = cas.expand(args[0], how); break;
110
+ case 'factor': r = cas.factor(args[0], how); break;
111
+ case 'cancel': r = cas.cancel(args[0], how); break;
112
+ case 'diff': r = cas.diff(args[0], word(args[1]), { ...how, order: args[2] ? Number(args[2].s) : 1 }); break;
113
+ case 'integrate': r = cas.integrate(args[0], word(args[1]), { ...how, ...(args[3] ? { from: args[2].s, to: args[3].s } : {}) }); break;
114
+ case 'solve': r = cas.solve(args[0], word(args[1]), how); break;
115
+ case 'approx': r = cas.approximate(args[0], { ...how, digits: args[1] ? Number(args[1].s) : 15 }); break;
116
+ case 'derivative': r = cas.diff(node.args[0], node.args[1].s, how); break;
117
+ default: r = cas.integrate(node, undefined, how); break;
118
+ }
119
+ } catch (error) {
120
+ // no answer: leave it as written, as calculateBlock does
121
+ return `(${text})`;
122
+ }
123
+ handler.last = r;
124
+ if (!r.result) return `"${r.text}"`;
125
+ const out = toText(tokensOf(r.result));
126
+ return /^-?\d+(\.\d+)?$/.test(out) ? out : `(${out})`;
127
+ };
128
+ return handler;
129
+ }
130
+
131
+ const escape = (s) => String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
132
+
133
+ /**
134
+ * Steps (from any CAS call) as an ordered list: each step's expression before,
135
+ * with the part a rule rewrote highlighted, the expression after, and the
136
+ * explanation. Options go to toHTML for both drawings (size, colors …).
137
+ * @returns {string} <ol class="mv-steps">…</ol>
138
+ */
139
+ export function stepsHTML(steps, options = {}) {
140
+ const items = steps.map((s) => {
141
+ const from = tokensOf(parsePostfix(s.from));
142
+ const before = toHTML(from, { ...options, highlight: s.path && s.path.length ? { path: s.path } : undefined });
143
+ const after = s.to === s.from ? '' : `<span class="mv-step-arrow" aria-label="becomes">⟶</span>${toHTML(tokensOf(parsePostfix(s.to)), options)}`;
144
+ const why = s.explanation ? `<p class="mv-step-why">${escape(s.explanation)}</p>` : '';
145
+ return `<li class="mv-step" data-rule="${escape(s.id)}"><span class="mv-step-name">${escape(s.name)}</span>`
146
+ + `<div class="mv-step-maths">${before}${after}</div>${why}</li>`;
147
+ });
148
+ return `<ol class="mv-steps">${items.join('')}</ol>`;
149
+ }
package/src/draw.js ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Input → tree → HTML string, with no calculator: the drawing both builds share (html.js adds the
3
+ * calculator for the full build; lite.js uses this as it is). No DOM needed, so this runs in Node too.
4
+ */
5
+
6
+ import { parseTree, fromPostfix } from './mathzetta.js';
7
+ import { layout } from './layout.js';
8
+ import { select, numberTree } from './select.js';
9
+
10
+ /**
11
+ * Anything MathZetta reads → a tree with node ids. `{{...}}` blocks are filled by `options.blocks`
12
+ * (with `options.scope`), else by whatever handler MathZetta has (the calculator once
13
+ * 'mathzetta/calculate' is loaded), else drawn as a group.
14
+ * @param {string|string[]|object} input expression text, standard postfix, or a tree
15
+ * @param {object} [options] { blocks, scope }
16
+ */
17
+ export function treeOf(input, options = {}) {
18
+ const { blocks } = options;
19
+ if (typeof input === 'string') return parseTree(input, blocks ? { blocks: (text) => blocks(text, options.scope) } : {});
20
+ if (Array.isArray(input)) return fromPostfix(input);
21
+ if (input && typeof input === 'object' && input.type) {
22
+ return input.id === undefined ? numberTree(structuredClone(input)) : input;
23
+ }
24
+ throw new Error('Inline Math needs an expression, standard postfix or a tree');
25
+ }
26
+
27
+ /**
28
+ * @param {string|string[]|object} input
29
+ * @param {object} [options] layout options (see DEFAULTS in layout.js) plus:
30
+ * @param {object|Array} [options.colors] { 'x^2': 'red', 3: '#06c' } or [[target, colour], …]
31
+ * @param {*} [options.highlight] target(s) to mark with the mv-hl class
32
+ * @param {object} [options.scope] variable values for `{{...}}` blocks
33
+ * @returns {string}
34
+ */
35
+ export function toHTML(input, options = {}) {
36
+ const tree = treeOf(input, options);
37
+ return layout(tree, layoutOptions(tree, options)).html;
38
+ }
39
+
40
+ /** Turn the friendly options (colors, highlight) into layout's attributes and style hooks. */
41
+ export function layoutOptions(tree, options) {
42
+ const { colors, highlight, scope, blocks, ...rest } = options;
43
+ const styles = new Map();
44
+ for (const [target, colour] of pairs(colors)) {
45
+ if (!isColour(colour)) continue; // one CSS colour value, nothing more: no `;`, no url(…)
46
+ for (const n of select(tree, target)) styles.set(n, `color:${colour}`);
47
+ }
48
+ const marked = new Set(highlight === undefined ? [] : select(tree, highlight));
49
+ if (!styles.size && !marked.size) return rest;
50
+ const prefix = rest.prefix || 'mv';
51
+ return {
52
+ ...rest,
53
+ style: (n) => [styles.get(n), rest.style && rest.style(n)].filter(Boolean).join(';'),
54
+ attributes: (n) => {
55
+ const extra = { ...(rest.attributes ? rest.attributes(n) : {}) };
56
+ if (marked.has(n)) extra.class = [extra.class, `${prefix}-hl`].filter(Boolean).join(' ');
57
+ return extra;
58
+ },
59
+ };
60
+ }
61
+
62
+ /** { 'x^2': 'red', 3: 'blue' } or [[target, colour]] → [[target, colour]]; numeric keys become ids. */
63
+ /** A colour a style attribute can hold on its own: red, #06c, rgb(0 102 204 / 50%), var(--accent). */
64
+ function isColour(value) {
65
+ return typeof value === 'string' && /^[#\w\s.,%()/+-]+$/.test(value) && !/url\s*\(|expression/i.test(value);
66
+ }
67
+
68
+ function pairs(colors) {
69
+ if (!colors) return [];
70
+ if (Array.isArray(colors)) return colors;
71
+ return Object.entries(colors).map(([k, v]) => [/^\d+$/.test(k) ? Number(k) : k, v]);
72
+ }