@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.
- package/LICENSE +21 -0
- package/README.md +326 -2
- package/dist/cas.external.js +2 -0
- package/dist/cas.js +2 -0
- package/dist/chunks/calculate-Cz87Lt0N.js +2 -0
- package/dist/chunks/draw-Bev17BJJ.js +2 -0
- package/dist/chunks/edit-C65xXzo8.js +2 -0
- package/dist/chunks/edit-M68aRA5o.js +2 -0
- package/dist/chunks/html-BimAqn7x.js +2 -0
- package/dist/chunks/html-CupluJSh.js +2 -0
- package/dist/chunks/lite-3Fb-OHbt.js +2 -0
- package/dist/fonts/GUST-FONT-LICENSE.txt +30 -0
- package/dist/fonts/fonts.css +17 -0
- package/dist/fonts/lmroman12-italic.woff2 +0 -0
- package/dist/fonts/lmroman12-regular.woff2 +0 -0
- package/dist/inlinemath-cas.external.global.min.js +2 -0
- package/dist/inlinemath-cas.global.min.js +2 -0
- package/dist/inlinemath-lite-calc.global.min.js +2 -0
- package/dist/inlinemath-lite.external.global.min.js +2 -0
- package/dist/inlinemath-lite.global.min.js +2 -0
- package/dist/inlinemath.css +158 -0
- package/dist/inlinemath.external.global.min.js +2 -0
- package/dist/inlinemath.external.js +2 -0
- package/dist/inlinemath.global.min.js +2 -0
- package/dist/inlinemath.js +2 -0
- package/dist/lite-calc.js +2 -0
- package/dist/lite.external.js +2 -0
- package/dist/lite.js +2 -0
- package/dist/markdown.external.js +2 -0
- package/dist/markdown.js +2 -0
- package/fonts/GUST-FONT-LICENSE.txt +30 -0
- package/fonts/fonts.css +17 -0
- package/fonts/lmroman12-italic.woff2 +0 -0
- package/fonts/lmroman12-regular.woff2 +0 -0
- package/inlinemath.css +158 -0
- package/package.json +95 -4
- package/src/calculator.js +6 -0
- package/src/cas.d.ts +3 -0
- package/src/cas.js +149 -0
- package/src/draw.js +72 -0
- package/src/edit.js +96 -0
- package/src/glyphs.js +40 -0
- package/src/html.js +32 -0
- package/src/index.d.ts +3 -0
- package/src/index.js +17 -0
- package/src/layout.js +649 -0
- package/src/lite-calc.d.ts +3 -0
- package/src/lite-calc.js +15 -0
- package/src/lite.d.ts +3 -0
- package/src/lite.js +18 -0
- package/src/markdown.d.ts +3 -0
- package/src/markdown.js +676 -0
- package/src/mathzetta.js +18 -0
- package/src/select.js +108 -0
- package/src/types.d.ts +204 -0
- package/src/variables.js +228 -0
- package/src/view.js +382 -0
package/dist/markdown.js
ADDED
|
@@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,"""),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
|
+
|
package/fonts/fonts.css
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
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, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
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
|
+
}
|