├── src ├── core │ ├── pkg │ │ └── .npmignore │ ├── src │ │ ├── katex │ │ │ ├── source.rs │ │ │ ├── mod.rs │ │ │ ├── types.rs │ │ │ └── constructor.rs │ │ ├── ext.rs │ │ ├── lib.rs │ │ ├── symbol.rs │ │ ├── node.rs │ │ ├── utils.rs │ │ └── main.rs │ └── Cargo.toml └── utils.js ├── CONTRIBUTING.md ├── .gitignore ├── test ├── index.css ├── package.json ├── webpack.config.js ├── index.html ├── utils.js └── index.js ├── lib ├── README.md └── katex │ ├── src │ ├── environments.js │ ├── functions │ │ ├── relax.js │ │ ├── ordgroup.js │ │ ├── htmlmathml.js │ │ ├── math.js │ │ ├── hbox.js │ │ ├── symbolsOp.js │ │ ├── tag.js │ │ ├── raisebox.js │ │ ├── vcenter.js │ │ ├── pmb.js │ │ ├── mathchoice.js │ │ ├── char.js │ │ ├── overline.js │ │ ├── underline.js │ │ ├── cr.js │ │ ├── kern.js │ │ ├── symbolsOrd.js │ │ ├── accentunder.js │ │ ├── verb.js │ │ ├── text.js │ │ ├── environment.js │ │ ├── styling.js │ │ ├── href.js │ │ ├── color.js │ │ ├── lap.js │ │ ├── symbolsSpacing.js │ │ ├── rule.js │ │ ├── sizing.js │ │ ├── font.js │ │ ├── html.js │ │ ├── smash.js │ │ ├── phantom.js │ │ ├── sqrt.js │ │ ├── utils │ │ │ └── assembleSupSub.js │ │ ├── horizBrace.js │ │ └── includegraphics.js │ ├── unicodeAccents.js │ ├── unicodeSymbols.js │ ├── types.js │ ├── SourceLocation.js │ ├── Token.js │ ├── parseTree.js │ ├── functions.js │ ├── buildTree.js │ ├── tree.js │ ├── unicodeSupOrSub.js │ ├── spacingData.js │ ├── ParseError.js │ ├── Style.js │ ├── defineEnvironment.js │ ├── defineMacro.js │ ├── utils.js │ ├── unicodeScripts.js │ ├── units.js │ ├── Namespace.js │ ├── wide-character.js │ └── Lexer.js │ ├── contrib │ ├── mathtex-script-type │ │ └── mathtex-script-type.js │ ├── copy-tex │ │ ├── copy-tex.js │ │ └── katex2tex.js │ └── auto-render │ │ ├── splitAtDelimiters.js │ │ └── auto-render.js │ └── cli.js ├── Makefile ├── esbuild.config.mjs ├── LICENSE ├── .github └── workflows │ └── static.yml ├── scripts.config.js ├── package.json ├── wypst.js ├── README.md └── scripts └── symbol_gen.js /src/core/pkg/.npmignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Setup 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | node_modules 3 | dist 4 | *.tgz 5 | .envrc 6 | -------------------------------------------------------------------------------- /test/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | display: flex; 3 | } 4 | 5 | #katex, #typst, #wypst { 6 | flex: 1; 7 | } 8 | -------------------------------------------------------------------------------- /src/core/src/katex/source.rs: -------------------------------------------------------------------------------- 1 | // Reference: SourceLocation.js 2 | 3 | use serde::Serialize; 4 | 5 | #[derive(Clone, Serialize)] 6 | pub struct SourceLocation { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /lib/README.md: -------------------------------------------------------------------------------- 1 | # lib 2 | Libraries that may be difficult to work with when installing directly with npm. Currently katex has flow types which I chose not to deal when building and thus I made a stripped version of it. 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: wasm 2 | wasm: 3 | cd src/core && wasm-pack build --target web 4 | 5 | .PHONY: wasm-dev 6 | wasm-dev: 7 | cd src/core && wasm-pack build --target web --dev 8 | 9 | .PHONY: build 10 | build: 11 | node esbuild.config.mjs 12 | -------------------------------------------------------------------------------- /src/core/src/katex/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod symbol; 2 | pub mod node; 3 | pub mod source; 4 | pub mod types; 5 | pub mod constructor; 6 | 7 | pub use symbol::*; 8 | pub use node::*; 9 | pub use source::*; 10 | pub use types::*; 11 | pub use constructor::*; 12 | -------------------------------------------------------------------------------- /lib/katex/src/environments.js: -------------------------------------------------------------------------------- 1 | // 2 | import {_environments} from "./defineEnvironment"; 3 | 4 | const environments = _environments; 5 | 6 | export default environments; 7 | 8 | // All environment definitions should be imported below 9 | import "./environments/array"; 10 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "wypst": "file:.." 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/katex/src/functions/relax.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction from "../defineFunction"; 3 | 4 | defineFunction({ 5 | type: "internal", 6 | names: ["\\relax"], 7 | props: { 8 | numArgs: 0, 9 | allowedInText: true, 10 | }, 11 | handler({parser}) { 12 | return { 13 | type: "internal", 14 | mode: parser.mode, 15 | }; 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /lib/katex/src/functions/ordgroup.js: -------------------------------------------------------------------------------- 1 | // 2 | import {defineFunctionBuilders} from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | 5 | import * as html from "../buildHTML"; 6 | import * as mml from "../buildMathML"; 7 | 8 | defineFunctionBuilders({ 9 | type: "ordgroup", 10 | htmlBuilder(group, options) { 11 | if (group.semisimple) { 12 | return buildCommon.makeFragment( 13 | html.buildExpression(group.body, options, false)); 14 | } 15 | return buildCommon.makeSpan( 16 | ["mord"], html.buildExpression(group.body, options, true), options); 17 | }, 18 | mathmlBuilder(group, options) { 19 | return mml.buildExpressionRow(group.body, options, true); 20 | }, 21 | }); 22 | 23 | -------------------------------------------------------------------------------- /lib/katex/src/unicodeAccents.js: -------------------------------------------------------------------------------- 1 | // Mapping of Unicode accent characters to their LaTeX equivalent in text and 2 | // math mode (when they exist). 3 | // This exports a CommonJS module, allowing to be required in unicodeSymbols 4 | // without transpiling. 5 | module.exports = { 6 | '\u0301': {text: "\\'", math: '\\acute'}, 7 | '\u0300': {text: '\\`', math: '\\grave'}, 8 | '\u0308': {text: '\\"', math: '\\ddot'}, 9 | '\u0303': {text: '\\~', math: '\\tilde'}, 10 | '\u0304': {text: '\\=', math: '\\bar'}, 11 | '\u0306': {text: '\\u', math: '\\breve'}, 12 | '\u030c': {text: '\\v', math: '\\check'}, 13 | '\u0302': {text: '\\^', math: '\\hat'}, 14 | '\u0307': {text: '\\.', math: '\\dot'}, 15 | '\u030a': {text: '\\r', math: '\\mathring'}, 16 | '\u030b': {text: '\\H'}, 17 | '\u0327': {text: '\\c'}, 18 | }; 19 | -------------------------------------------------------------------------------- /src/core/src/ext.rs: -------------------------------------------------------------------------------- 1 | use typst; 2 | 3 | pub trait DelimiterOpenClose { 4 | fn open(self) -> char; 5 | fn close(self) -> char; 6 | } 7 | 8 | impl DelimiterOpenClose for typst::math::Delimiter { 9 | /// The delimiter's opening character. 10 | fn open(self) -> char { 11 | match self { 12 | Self::Paren => '(', 13 | Self::Bracket => '[', 14 | Self::Brace => '{', 15 | Self::Bar => '|', 16 | Self::DoubleBar => '‖', 17 | } 18 | } 19 | 20 | /// The delimiter's closing character. 21 | fn close(self) -> char { 22 | match self { 23 | Self::Paren => ')', 24 | Self::Bracket => ']', 25 | Self::Brace => '}', 26 | Self::Bar => '|', 27 | Self::DoubleBar => '‖', 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import ParseError from '../lib/katex/src/ParseError'; 2 | import Settings from '../lib/katex/src/Settings'; 3 | import parseTree from '../lib/katex/src/parseTree'; 4 | import buildTree from '../lib/katex/src/buildTree'; 5 | import buildCommon from '../lib/katex/src/buildCommon'; 6 | import { SymbolNode } from '../lib/katex/src/domTree'; 7 | 8 | function renderError(error, expression, settings) { 9 | if (settings.throwOnError) { 10 | throw error; 11 | } 12 | const node = buildCommon.makeSpan(["katex-error"], [new SymbolNode(expression)]); 13 | node.setAttribute("title", error.toString()); 14 | node.setAttribute("style", `color:${settings.errorColor}`); 15 | return node; 16 | } 17 | 18 | export default { 19 | ParseError, 20 | Settings, 21 | parseTree, 22 | buildTree, 23 | renderError, 24 | }; 25 | -------------------------------------------------------------------------------- /src/core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["cdylib", "rlib"] 8 | 9 | [features] 10 | default = ["console_error_panic_hook"] 11 | 12 | [dependencies] 13 | typst = { git = "https://github.com/typst/typst.git", tag = "v0.10.0" } 14 | typst-syntax = { git = "https://github.com/typst/typst.git", tag = "v0.10.0" } 15 | wasm-bindgen = "0.2.84" 16 | serde = { version = "1.0", features = ["derive"] } 17 | serde-wasm-bindgen = "0.4" 18 | console_error_panic_hook = { version = "0.1.7", optional = true } 19 | js-sys = "0.3.64" 20 | phf = "0.11.2" 21 | serde_json = "1.0.108" 22 | comemo = "0.3.1" 23 | derive_builder = "0.12.0" 24 | log = "0.4.20" 25 | 26 | [dev-dependencies] 27 | wasm-bindgen-test = "0.3.34" 28 | 29 | [profile.release] 30 | opt-level = "z" 31 | lto = true 32 | 33 | [lib.metadata.wasm-pack.profile.release] 34 | wasm-opt = ["-Oz"] 35 | -------------------------------------------------------------------------------- /lib/katex/contrib/mathtex-script-type/mathtex-script-type.js: -------------------------------------------------------------------------------- 1 | import katex from "katex"; 2 | 3 | let scripts = document.body.getElementsByTagName("script"); 4 | scripts = Array.prototype.slice.call(scripts); 5 | scripts.forEach(function(script) { 6 | if (!script.type || !script.type.match(/math\/tex/i)) { 7 | return -1; 8 | } 9 | const display = 10 | (script.type.match(/mode\s*=\s*display(;|\s|\n|$)/) != null); 11 | 12 | const katexElement = document.createElement(display ? "div" : "span"); 13 | katexElement.setAttribute("class", 14 | display ? "equation" : "inline-equation"); 15 | try { 16 | katex.render(script.text, katexElement, {displayMode: display}); 17 | } catch (err) { 18 | //console.error(err); linter doesn't like this 19 | katexElement.textContent = script.text; 20 | } 21 | script.parentNode.replaceChild(katexElement, script); 22 | }); 23 | -------------------------------------------------------------------------------- /test/webpack.config.js: -------------------------------------------------------------------------------- 1 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: './index.js', 6 | output: { 7 | filename: 'bundle.js', 8 | path: path.resolve(__dirname, 'dist'), 9 | }, 10 | devServer: { 11 | port: 8080, 12 | }, 13 | plugins: [ 14 | new HtmlWebpackPlugin({ 15 | template: 'index.html', 16 | }), 17 | ], 18 | experiments: { 19 | asyncWebAssembly: true, 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.js$/, 25 | use: { 26 | loader: 'babel-loader', 27 | options: { 28 | presets: ['@babel/preset-flow'], 29 | }, 30 | }, 31 | }, 32 | ] 33 | }, 34 | devtool: 'source-map', 35 | mode: 'development', 36 | }; 37 | -------------------------------------------------------------------------------- /lib/katex/src/functions/htmlmathml.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction, {ordargument} from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | 5 | import * as html from "../buildHTML"; 6 | import * as mml from "../buildMathML"; 7 | 8 | defineFunction({ 9 | type: "htmlmathml", 10 | names: ["\\html@mathml"], 11 | props: { 12 | numArgs: 2, 13 | allowedInText: true, 14 | }, 15 | handler: ({parser}, args) => { 16 | return { 17 | type: "htmlmathml", 18 | mode: parser.mode, 19 | html: ordargument(args[0]), 20 | mathml: ordargument(args[1]), 21 | }; 22 | }, 23 | htmlBuilder: (group, options) => { 24 | const elements = html.buildExpression( 25 | group.html, 26 | options, 27 | false 28 | ); 29 | return buildCommon.makeFragment(elements); 30 | }, 31 | mathmlBuilder: (group, options) => { 32 | return mml.buildExpressionRow(group.mathml, options); 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from 'esbuild'; 2 | import fs from 'fs'; 3 | 4 | const entryPoint = './wypst.js'; 5 | const isDevelopment = process.env.NODE_ENV === 'development'; 6 | 7 | // Inline wasm build 8 | esbuild.build({ 9 | entryPoints: [entryPoint], 10 | bundle: true, 11 | minify: !isDevelopment, 12 | sourcemap: isDevelopment, 13 | target: ['es6'], 14 | outfile: './dist/wypst.min.js', 15 | format: 'iife', 16 | globalName: 'wypst', 17 | loader: { 18 | '.wasm': 'binary' 19 | }, 20 | }); 21 | 22 | // Main build 23 | esbuild.build({ 24 | entryPoints: [entryPoint], 25 | bundle: true, 26 | minify: false, 27 | sourcemap: isDevelopment, 28 | target: ['es6'], 29 | outfile: './dist/wypst.js', 30 | format: 'esm', 31 | globalName: 'wypst', 32 | loader: { 33 | '.wasm': 'file' 34 | }, 35 | metafile: true, 36 | assetNames: 'wypst', 37 | }); 38 | 39 | // Copy CSS files 40 | fs.copyFileSync('./node_modules/katex/dist/katex.css', './dist/wypst.css'); 41 | fs.copyFileSync('./node_modules/katex/dist/katex.min.css', './dist/wypst.min.css'); 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 0xpapercut 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/katex/src/functions/math.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction from "../defineFunction"; 3 | import ParseError from "../ParseError"; 4 | 5 | // Switching from text mode back to math mode 6 | defineFunction({ 7 | type: "styling", 8 | names: ["\\(", "$"], 9 | props: { 10 | numArgs: 0, 11 | allowedInText: true, 12 | allowedInMath: false, 13 | }, 14 | handler({funcName, parser}, args) { 15 | const outerMode = parser.mode; 16 | parser.switchMode("math"); 17 | const close = (funcName === "\\(" ? "\\)" : "$"); 18 | const body = parser.parseExpression(false, close); 19 | parser.expect(close); 20 | parser.switchMode(outerMode); 21 | return { 22 | type: "styling", 23 | mode: parser.mode, 24 | style: "text", 25 | body, 26 | }; 27 | }, 28 | }); 29 | 30 | // Check for extra closing math delimiters 31 | defineFunction({ 32 | type: "text", // Doesn't matter what this is. 33 | names: ["\\)", "\\]"], 34 | props: { 35 | numArgs: 0, 36 | allowedInText: true, 37 | allowedInMath: false, 38 | }, 39 | handler(context, args) { 40 | throw new ParseError(`Mismatched ${context.funcName}`); 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Debug wypst 7 | 8 | 27 | 28 | 29 |
30 |
31 |
32 | 33 |

34 |
35 |
36 | 37 |

38 |
39 |
40 |

41 |
42 |
43 | 44 | 45 | -------------------------------------------------------------------------------- /src/core/src/lib.rs: -------------------------------------------------------------------------------- 1 | use wasm_bindgen::prelude::*; 2 | use serde_wasm_bindgen::to_value; 3 | use serde_json; 4 | use typst; 5 | 6 | mod converter; 7 | mod katex; 8 | mod utils; 9 | mod node; 10 | mod ext; 11 | mod content; 12 | mod symbol; 13 | 14 | fn content_tree(expression: &str) -> Result { 15 | let world = utils::FakeWorld::new(); 16 | utils::eval(&world, expression) 17 | } 18 | 19 | pub fn convert(content: &typst::foundations::Content) -> serde_json::Value { 20 | let katex_tree = converter::convert(content); 21 | serde_json::to_value(&katex_tree).unwrap() 22 | } 23 | 24 | #[wasm_bindgen(js_name = "parseTree")] 25 | pub fn parse_tree(expression: &str) -> Result { 26 | #[cfg(debug_assertions)] 27 | console_error_panic_hook::set_once(); 28 | let content = content_tree(expression); 29 | content.map(|c| { 30 | let katex_tree = converter::convert(&c); 31 | to_value(&katex_tree).unwrap() 32 | }) 33 | } 34 | 35 | #[wasm_bindgen(js_name = "typstContentTree")] 36 | pub fn typst_content_tree(expression: &str) -> Result { 37 | let content = content_tree(expression); 38 | match content { 39 | Ok(tree) => Ok(format!("{:#?}", tree).into()), 40 | Err(err) => Err(err), 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/katex/src/functions/hbox.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction, {ordargument} from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | 6 | import * as html from "../buildHTML"; 7 | import * as mml from "../buildMathML"; 8 | 9 | // \hbox is provided for compatibility with LaTeX \vcenter. 10 | // In LaTeX, \vcenter can act only on a box, as in 11 | // \vcenter{\hbox{$\frac{a+b}{\dfrac{c}{d}}$}} 12 | // This function by itself doesn't do anything but prevent a soft line break. 13 | 14 | defineFunction({ 15 | type: "hbox", 16 | names: ["\\hbox"], 17 | props: { 18 | numArgs: 1, 19 | argTypes: ["text"], 20 | allowedInText: true, 21 | primitive: true, 22 | }, 23 | handler({parser}, args) { 24 | return { 25 | type: "hbox", 26 | mode: parser.mode, 27 | body: ordargument(args[0]), 28 | }; 29 | }, 30 | htmlBuilder(group, options) { 31 | const elements = html.buildExpression(group.body, options, false); 32 | return buildCommon.makeFragment(elements); 33 | }, 34 | mathmlBuilder(group, options) { 35 | return new mathMLTree.MathNode( 36 | "mrow", mml.buildExpression(group.body, options) 37 | ); 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /lib/katex/src/functions/symbolsOp.js: -------------------------------------------------------------------------------- 1 | // 2 | import {defineFunctionBuilders} from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | 6 | import * as mml from "../buildMathML"; 7 | 8 | // Operator ParseNodes created in Parser.js from symbol Groups in src/symbols.js. 9 | 10 | defineFunctionBuilders({ 11 | type: "atom", 12 | htmlBuilder(group, options) { 13 | return buildCommon.mathsym( 14 | group.text, group.mode, options, ["m" + group.family]); 15 | }, 16 | mathmlBuilder(group, options) { 17 | const node = new mathMLTree.MathNode( 18 | "mo", [mml.makeText(group.text, group.mode)]); 19 | if (group.family === "bin") { 20 | const variant = mml.getVariant(group, options); 21 | if (variant === "bold-italic") { 22 | node.setAttribute("mathvariant", variant); 23 | } 24 | } else if (group.family === "punct") { 25 | node.setAttribute("separator", "true"); 26 | } else if (group.family === "open" || group.family === "close") { 27 | // Delims built here should not stretch vertically. 28 | // See delimsizing.js for stretchy delims. 29 | node.setAttribute("stretchy", "false"); 30 | } 31 | return node; 32 | }, 33 | }); 34 | 35 | -------------------------------------------------------------------------------- /lib/katex/src/unicodeSymbols.js: -------------------------------------------------------------------------------- 1 | // 2 | // This is an internal module, not part of the KaTeX distribution, 3 | // whose purpose is to generate `unicodeSymbols` in Parser.js 4 | // In this way, only this module, and not the distribution/browser, 5 | // needs String's normalize function. As this file is not transpiled, 6 | // Flow comment types syntax is used. 7 | const accents = require('./unicodeAccents'); 8 | 9 | const result /*: {[string]: string}*/ = {}; 10 | const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + 11 | "αβγδεϵζηθϑικλμνξοπϖρϱςστυφϕχψωΓΔΘΛΞΠΣΥΦΨΩ"; 12 | for (const letter of letters) { 13 | for (const accent of Object.getOwnPropertyNames(accents)) { 14 | const combined = letter + accent; 15 | const normalized = combined.normalize('NFC'); 16 | if (normalized.length === 1) { 17 | result[normalized] = combined; 18 | } 19 | for (const accent2 of Object.getOwnPropertyNames(accents)) { 20 | if (accent === accent2) { 21 | continue; 22 | } 23 | const combined2 = combined + accent2; 24 | const normalized2 = combined2.normalize('NFC'); 25 | if (normalized2.length === 1) { 26 | result[normalized2] = combined2; 27 | } 28 | } 29 | } 30 | } 31 | 32 | module.exports = result; 33 | -------------------------------------------------------------------------------- /src/core/src/symbol.rs: -------------------------------------------------------------------------------- 1 | use crate::node::Node; 2 | use crate::katex::{self, LapBuilder, MClassBuilder}; 3 | 4 | pub fn not() -> Node { 5 | Node::Node(katex::AtomBuilder::default() 6 | .family(katex::AtomGroup::Rel) 7 | .text("\\@not".to_string()) 8 | .build().unwrap().into_node()) 9 | } 10 | 11 | pub fn equals() -> Node { 12 | Node::Node(katex::AtomBuilder::default() 13 | .family(katex::AtomGroup::Rel) 14 | .text("=".to_string()) 15 | .build().unwrap().into_node()) 16 | } 17 | 18 | pub fn neq() -> Node { 19 | let not = MClassBuilder::default() 20 | .mclass("rel".to_string()) 21 | .body([ 22 | LapBuilder::default() 23 | .alignment("rlap".to_string()) 24 | .body(Box::new(not().into_ordgroup(katex::Mode::Math).into_node())) 25 | .build().unwrap().into_node() 26 | ].to_vec()) 27 | .is_character_box(false) 28 | .build().unwrap().into_node(); 29 | Node::Node(MClassBuilder::default() 30 | .mclass("mrel".to_string()) 31 | .is_character_box(false) 32 | .body([not, equals().into_node().unwrap()].to_vec()) 33 | .build().unwrap().into_node()) 34 | } 35 | 36 | pub fn define() -> Node { 37 | Node::Array([ 38 | katex::Symbol::get(katex::Mode::Math, ':').create_node(), 39 | katex::Symbol::get(katex::Mode::Math, '=').create_node(), 40 | ].to_vec()) 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | - name: Setup Pages 35 | uses: actions/configure-pages@v4 36 | - name: Upload artifact 37 | uses: actions/upload-pages-artifact@v3 38 | with: 39 | # Upload entire repository 40 | path: './test/dist' 41 | - name: Deploy to GitHub Pages 42 | id: deployment 43 | uses: actions/deploy-pages@v4 44 | -------------------------------------------------------------------------------- /lib/katex/src/functions/tag.js: -------------------------------------------------------------------------------- 1 | // 2 | import {defineFunctionBuilders} from "../defineFunction"; 3 | import mathMLTree from "../mathMLTree"; 4 | 5 | import * as mml from "../buildMathML"; 6 | 7 | const pad = () => { 8 | const padNode = new mathMLTree.MathNode("mtd", []); 9 | padNode.setAttribute("width", "50%"); 10 | return padNode; 11 | }; 12 | 13 | defineFunctionBuilders({ 14 | type: "tag", 15 | mathmlBuilder(group, options) { 16 | const table = new mathMLTree.MathNode("mtable", [ 17 | new mathMLTree.MathNode("mtr", [ 18 | pad(), 19 | new mathMLTree.MathNode("mtd", [ 20 | mml.buildExpressionRow(group.body, options), 21 | ]), 22 | pad(), 23 | new mathMLTree.MathNode("mtd", [ 24 | mml.buildExpressionRow(group.tag, options), 25 | ]), 26 | ]), 27 | ]); 28 | table.setAttribute("width", "100%"); 29 | return table; 30 | 31 | // TODO: Left-aligned tags. 32 | // Currently, the group and options passed here do not contain 33 | // enough info to set tag alignment. `leqno` is in Settings but it is 34 | // not passed to Options. On the HTML side, leqno is 35 | // set by a CSS class applied in buildTree.js. That would have worked 36 | // in MathML if browsers supported . Since they don't, we 37 | // need to rewrite the way this function is called. 38 | }, 39 | }); 40 | 41 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | export function deleteFields(obj, fields) { 2 | if (Array.isArray(obj)) { 3 | for (let i = 0; i < obj.length; i++) { 4 | deleteFields(obj[i], fields); 5 | } 6 | } else if (typeof obj === 'object' && obj !== null) { 7 | for (let field of fields) { 8 | delete obj[field]; 9 | } 10 | for (let key in obj) { 11 | if (obj.hasOwnProperty(key)) { 12 | deleteFields(obj[key], fields); 13 | } 14 | } 15 | } 16 | } 17 | 18 | export function deleteIfFieldEquals(obj, value) { 19 | if (typeof obj === 'object' && obj !== null) { 20 | for (let key in obj) { 21 | if (!obj.hasOwnProperty(key)) 22 | continue; 23 | if (obj[key] === value) { 24 | delete obj[key]; 25 | } else { 26 | deleteIfFieldEquals(obj[key], value); 27 | } 28 | } 29 | } 30 | } 31 | 32 | export function diff(obj1, obj2) { 33 | const result = {}; 34 | for (const key in obj1) { 35 | if (!obj2.hasOwnProperty(key)) { 36 | result[key] = obj1[key]; 37 | } else if (typeof obj1[key] === 'object' && typeof obj2[key] === 'object') { 38 | const value = diff(obj1[key], obj2[key]); 39 | if (Object.keys(value).length !== 0) { 40 | result[key] = value; 41 | } 42 | } else if (obj1[key] !== obj2[key]) { 43 | result[key] = obj1[key]; 44 | } 45 | } 46 | return result; 47 | } 48 | -------------------------------------------------------------------------------- /lib/katex/src/functions/raisebox.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | import {assertNodeType} from "../parseNode"; 6 | import {calculateSize} from "../units"; 7 | 8 | import * as html from "../buildHTML"; 9 | import * as mml from "../buildMathML"; 10 | 11 | // Box manipulation 12 | defineFunction({ 13 | type: "raisebox", 14 | names: ["\\raisebox"], 15 | props: { 16 | numArgs: 2, 17 | argTypes: ["size", "hbox"], 18 | allowedInText: true, 19 | }, 20 | handler({parser}, args) { 21 | const amount = assertNodeType(args[0], "size").value; 22 | const body = args[1]; 23 | return { 24 | type: "raisebox", 25 | mode: parser.mode, 26 | dy: amount, 27 | body, 28 | }; 29 | }, 30 | htmlBuilder(group, options) { 31 | const body = html.buildGroup(group.body, options); 32 | const dy = calculateSize(group.dy, options); 33 | return buildCommon.makeVList({ 34 | positionType: "shift", 35 | positionData: -dy, 36 | children: [{type: "elem", elem: body}], 37 | }, options); 38 | }, 39 | mathmlBuilder(group, options) { 40 | const node = new mathMLTree.MathNode( 41 | "mpadded", [mml.buildGroup(group.body, options)]); 42 | const dy = group.dy.number + group.dy.unit; 43 | node.setAttribute("voffset", dy); 44 | return node; 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /scripts.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const ShellPlugin = require('webpack-shell-plugin-next'); 3 | 4 | const scriptsDir = path.resolve(__dirname, 'scripts'); 5 | const symbolGenFilename = 'symbol_gen.js'; 6 | 7 | const symbolSourceConfig = { 8 | mode: 'development', 9 | target: 'node', 10 | entry: path.resolve(__dirname, 'node_modules/katex/src/symbols.js'), 11 | output: { 12 | path: path.resolve(scriptsDir, 'dist'), 13 | filename: 'symbols.js' 14 | }, 15 | devtool: 'inline-source-map', 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | exclude: /node_modules\/(?!katex)/, 21 | use: { 22 | loader: 'babel-loader', 23 | options: { 24 | presets: ['@babel/preset-flow'], 25 | }, 26 | }, 27 | } 28 | ], 29 | }, 30 | } 31 | 32 | const symbolGenConfig = { 33 | mode: 'development', 34 | target: 'node', 35 | entry: path.resolve(scriptsDir, symbolGenFilename), 36 | output: { 37 | path: path.resolve(scriptsDir, 'dist'), 38 | filename: symbolGenFilename 39 | }, 40 | plugins: [ 41 | new ShellPlugin({ 42 | onBuildEnd: { 43 | scripts: [`node ${path.resolve(scriptsDir, 'dist', symbolGenFilename)}`], 44 | blocking: true, 45 | parallel: false 46 | } 47 | }) 48 | ] 49 | } 50 | 51 | module.exports = [symbolSourceConfig, symbolGenConfig] 52 | -------------------------------------------------------------------------------- /lib/katex/src/functions/vcenter.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | 6 | import * as html from "../buildHTML"; 7 | import * as mml from "../buildMathML"; 8 | 9 | // \vcenter: Vertically center the argument group on the math axis. 10 | 11 | defineFunction({ 12 | type: "vcenter", 13 | names: ["\\vcenter"], 14 | props: { 15 | numArgs: 1, 16 | argTypes: ["original"], // In LaTeX, \vcenter can act only on a box. 17 | allowedInText: false, 18 | }, 19 | handler({parser}, args) { 20 | return { 21 | type: "vcenter", 22 | mode: parser.mode, 23 | body: args[0], 24 | }; 25 | }, 26 | htmlBuilder(group, options) { 27 | const body = html.buildGroup(group.body, options); 28 | const axisHeight = options.fontMetrics().axisHeight; 29 | const dy = 0.5 * ((body.height - axisHeight) - (body.depth + axisHeight)); 30 | return buildCommon.makeVList({ 31 | positionType: "shift", 32 | positionData: dy, 33 | children: [{type: "elem", elem: body}], 34 | }, options); 35 | }, 36 | mathmlBuilder(group, options) { 37 | // There is no way to do this in MathML. 38 | // Write a class as a breadcrumb in case some post-processor wants 39 | // to perform a vcenter adjustment. 40 | return new mathMLTree.MathNode( 41 | "mpadded", [mml.buildGroup(group.body, options)], ["vcenter"]); 42 | }, 43 | }); 44 | 45 | -------------------------------------------------------------------------------- /lib/katex/src/types.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | /** 4 | * This file consists only of basic flow types used in multiple places. 5 | * For types with javascript, create separate files by themselves. 6 | */ 7 | 8 | 9 | 10 | // LaTeX argument type. 11 | // - "size": A size-like thing, such as "1em" or "5ex" 12 | // - "color": An html color, like "#abc" or "blue" 13 | // - "url": An url string, in which "\" will be ignored 14 | // - if it precedes [#$%&~_^\{}] 15 | // - "raw": A string, allowing single character, percent sign, 16 | // and nested braces 17 | // - "original": The same type as the environment that the 18 | // function being parsed is in (e.g. used for the 19 | // bodies of functions like \textcolor where the 20 | // first argument is special and the second 21 | // argument is parsed normally) 22 | // - Mode: Node group parsed in given mode. 23 | 24 | 25 | 26 | // LaTeX display style. 27 | 28 | 29 | // Allowable token text for "break" arguments in parser. 30 | 31 | 32 | 33 | // Math font variants. 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /lib/katex/src/SourceLocation.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | 4 | /** 5 | * Lexing or parsing positional information for error reporting. 6 | * This object is immutable. 7 | */ 8 | export default class SourceLocation { 9 | // The + prefix indicates that these fields aren't writeable 10 | lexer ; // Lexer holding the input string. 11 | start ; // Start offset, zero-based inclusive. 12 | end ; // End offset, zero-based exclusive. 13 | 14 | constructor(lexer , start , end ) { 15 | this.lexer = lexer; 16 | this.start = start; 17 | this.end = end; 18 | } 19 | 20 | /** 21 | * Merges two `SourceLocation`s from location providers, given they are 22 | * provided in order of appearance. 23 | * - Returns the first one's location if only the first is provided. 24 | * - Returns a merged range of the first and the last if both are provided 25 | * and their lexers match. 26 | * - Otherwise, returns null. 27 | */ 28 | static range( 29 | first , 30 | second , 31 | ) { 32 | if (!second) { 33 | return first && first.loc; 34 | } else if (!first || !first.loc || !second.loc || 35 | first.loc.lexer !== second.loc.lexer) { 36 | return null; 37 | } else { 38 | return new SourceLocation( 39 | first.loc.lexer, first.loc.start, second.loc.end); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/katex/src/functions/pmb.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction, {ordargument} from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | import * as html from "../buildHTML"; 6 | import * as mml from "../buildMathML"; 7 | import {binrelClass} from "./mclass"; 8 | 9 | 10 | 11 | // \pmb is a simulation of bold font. 12 | // The version of \pmb in ambsy.sty works by typesetting three copies 13 | // with small offsets. We use CSS text-shadow. 14 | // It's a hack. Not as good as a real bold font. Better than nothing. 15 | 16 | defineFunction({ 17 | type: "pmb", 18 | names: ["\\pmb"], 19 | props: { 20 | numArgs: 1, 21 | allowedInText: true, 22 | }, 23 | handler({parser}, args) { 24 | return { 25 | type: "pmb", 26 | mode: parser.mode, 27 | mclass: binrelClass(args[0]), 28 | body: ordargument(args[0]), 29 | }; 30 | }, 31 | htmlBuilder(group , options) { 32 | const elements = html.buildExpression(group.body, options, true); 33 | const node = buildCommon.makeSpan([group.mclass], elements, options); 34 | node.style.textShadow = "0.02em 0.01em 0.04px"; 35 | return node; 36 | }, 37 | mathmlBuilder(group , style) { 38 | const inner = mml.buildExpression(group.body, style); 39 | // Wrap with an element. 40 | const node = new mathMLTree.MathNode("mstyle", inner); 41 | node.setAttribute("style", "text-shadow: 0.02em 0.01em 0.04px"); 42 | return node; 43 | }, 44 | }); 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wypst", 3 | "version": "0.0.8", 4 | "description": "Typst math typesetting for the web.", 5 | "scripts": { 6 | "scripts": "webpack --config=scripts.config.js" 7 | }, 8 | "main": "dist/wypst.js", 9 | "files": [ 10 | "wypst.js", 11 | "src/utils.js", 12 | "src/core/pkg/", 13 | "dist/" 14 | ], 15 | "keywords": [ 16 | "typst", 17 | "katex", 18 | "transpiler", 19 | "math" 20 | ], 21 | "type": "module", 22 | "author": "0xpapercut", 23 | "license": "MIT", 24 | "devDependencies": { 25 | "@babel/generator": "^7.23.4", 26 | "@babel/parser": "^7.23.4", 27 | "@babel/preset-flow": "^7.23.3", 28 | "@babel/preset-typescript": "^7.23.3", 29 | "@babel/traverse": "^7.23.4", 30 | "@rollup/plugin-babel": "^6.0.4", 31 | "@rollup/plugin-commonjs": "^25.0.7", 32 | "@rollup/plugin-node-resolve": "^15.2.3", 33 | "@rollup/plugin-wasm": "^6.2.2", 34 | "@wasm-tool/wasm-pack-plugin": "^1.7.0", 35 | "babel-loader": "^9.1.3", 36 | "chai": "^4.3.10", 37 | "css-loader": "^6.9.0", 38 | "esbuild": "^0.20.2", 39 | "esprima": "^4.0.1", 40 | "estraverse": "^5.3.0", 41 | "express": "^4.18.2", 42 | "flow-remove-types": "^2.234.0", 43 | "fs": "^0.0.1-security", 44 | "html-webpack-plugin": "^5.5.3", 45 | "isolated-vm": "^4.6.0", 46 | "json-formatter-js": "^2.3.4", 47 | "katex": "^0.16.10", 48 | "puppeteer": "^21.5.1", 49 | "rollup": "^4.14.3", 50 | "style-loader": "^3.3.4", 51 | "webpack": "^5.89.0", 52 | "webpack-cli": "^5.1.4", 53 | "webpack-dev-server": "^4.15.1", 54 | "webpack-shell-plugin-next": "^2.3.1", 55 | "esbuild-plugin-copy": "^2.1.1" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/katex/src/Token.js: -------------------------------------------------------------------------------- 1 | // 2 | import SourceLocation from "./SourceLocation"; 3 | 4 | /** 5 | * Interface required to break circular dependency between Token, Lexer, and 6 | * ParseError. 7 | */ 8 | 9 | 10 | /** 11 | * The resulting token returned from `lex`. 12 | * 13 | * It consists of the token text plus some position information. 14 | * The position information is essentially a range in an input string, 15 | * but instead of referencing the bare input string, we refer to the lexer. 16 | * That way it is possible to attach extra metadata to the input string, 17 | * like for example a file name or similar. 18 | * 19 | * The position information is optional, so it is OK to construct synthetic 20 | * tokens if appropriate. Not providing available position information may 21 | * lead to degraded error reporting, though. 22 | */ 23 | export class Token { 24 | text ; 25 | loc ; 26 | noexpand ; // don't expand the token 27 | treatAsRelax ; // used in \noexpand 28 | 29 | constructor( 30 | text , // the text of this token 31 | loc , 32 | ) { 33 | this.text = text; 34 | this.loc = loc; 35 | } 36 | 37 | /** 38 | * Given a pair of tokens (this and endToken), compute a `Token` encompassing 39 | * the whole input range enclosed by these two. 40 | */ 41 | range( 42 | endToken , // last token of the range, inclusive 43 | text , // the text of the newly constructed token 44 | ) { 45 | return new Token(text, SourceLocation.range(this, endToken)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/katex/src/functions/mathchoice.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction, {ordargument} from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import Style from "../Style"; 5 | 6 | import * as html from "../buildHTML"; 7 | import * as mml from "../buildMathML"; 8 | 9 | 10 | 11 | const chooseMathStyle = (group , options) => { 12 | switch (options.style.size) { 13 | case Style.DISPLAY.size: return group.display; 14 | case Style.TEXT.size: return group.text; 15 | case Style.SCRIPT.size: return group.script; 16 | case Style.SCRIPTSCRIPT.size: return group.scriptscript; 17 | default: return group.text; 18 | } 19 | }; 20 | 21 | defineFunction({ 22 | type: "mathchoice", 23 | names: ["\\mathchoice"], 24 | props: { 25 | numArgs: 4, 26 | primitive: true, 27 | }, 28 | handler: ({parser}, args) => { 29 | return { 30 | type: "mathchoice", 31 | mode: parser.mode, 32 | display: ordargument(args[0]), 33 | text: ordargument(args[1]), 34 | script: ordargument(args[2]), 35 | scriptscript: ordargument(args[3]), 36 | }; 37 | }, 38 | htmlBuilder: (group, options) => { 39 | const body = chooseMathStyle(group, options); 40 | const elements = html.buildExpression( 41 | body, 42 | options, 43 | false 44 | ); 45 | return buildCommon.makeFragment(elements); 46 | }, 47 | mathmlBuilder: (group, options) => { 48 | const body = chooseMathStyle(group, options); 49 | return mml.buildExpressionRow(body, options); 50 | }, 51 | }); 52 | -------------------------------------------------------------------------------- /lib/katex/src/parseTree.js: -------------------------------------------------------------------------------- 1 | // 2 | /** 3 | * Provides a single function for parsing an expression using a Parser 4 | * TODO(emily): Remove this 5 | */ 6 | 7 | import Parser from "./Parser"; 8 | import ParseError from "./ParseError"; 9 | import {Token} from "./Token"; 10 | 11 | 12 | 13 | 14 | /** 15 | * Parses an expression using a Parser, then returns the parsed result. 16 | */ 17 | const parseTree = function(toParse , settings ) { 18 | if (!(typeof toParse === 'string' || toParse instanceof String)) { 19 | throw new TypeError('KaTeX can only parse string typed expression'); 20 | } 21 | const parser = new Parser(toParse, settings); 22 | 23 | // Blank out any \df@tag to avoid spurious "Duplicate \tag" errors 24 | delete parser.gullet.macros.current["\\df@tag"]; 25 | 26 | let tree = parser.parse(); 27 | 28 | // Prevent a color definition from persisting between calls to katex.render(). 29 | delete parser.gullet.macros.current["\\current@color"]; 30 | delete parser.gullet.macros.current["\\color"]; 31 | 32 | // If the input used \tag, it will set the \df@tag macro to the tag. 33 | // In this case, we separately parse the tag and wrap the tree. 34 | if (parser.gullet.macros.get("\\df@tag")) { 35 | if (!settings.displayMode) { 36 | throw new ParseError("\\tag works only in display equations"); 37 | } 38 | tree = [{ 39 | type: "tag", 40 | mode: "text", 41 | body: tree, 42 | tag: parser.subparse([new Token("\\df@tag")]), 43 | }]; 44 | } 45 | 46 | return tree; 47 | }; 48 | 49 | export default parseTree; 50 | -------------------------------------------------------------------------------- /lib/katex/src/functions/char.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction from "../defineFunction"; 3 | import ParseError from "../ParseError"; 4 | import {assertNodeType} from "../parseNode"; 5 | 6 | // \@char is an internal function that takes a grouped decimal argument like 7 | // {123} and converts into symbol with code 123. It is used by the *macro* 8 | // \char defined in macros.js. 9 | defineFunction({ 10 | type: "textord", 11 | names: ["\\@char"], 12 | props: { 13 | numArgs: 1, 14 | allowedInText: true, 15 | }, 16 | handler({parser}, args) { 17 | const arg = assertNodeType(args[0], "ordgroup"); 18 | const group = arg.body; 19 | let number = ""; 20 | for (let i = 0; i < group.length; i++) { 21 | const node = assertNodeType(group[i], "textord"); 22 | number += node.text; 23 | } 24 | let code = parseInt(number); 25 | let text; 26 | if (isNaN(code)) { 27 | throw new ParseError(`\\@char has non-numeric argument ${number}`); 28 | // If we drop IE support, the following code could be replaced with 29 | // text = String.fromCodePoint(code) 30 | } else if (code < 0 || code >= 0x10ffff) { 31 | throw new ParseError(`\\@char with invalid code point ${number}`); 32 | } else if (code <= 0xffff) { 33 | text = String.fromCharCode(code); 34 | } else { // Astral code point; split into surrogate halves 35 | code -= 0x10000; 36 | text = String.fromCharCode((code >> 10) + 0xd800, 37 | (code & 0x3ff) + 0xdc00); 38 | } 39 | return { 40 | type: "textord", 41 | mode: parser.mode, 42 | text: text, 43 | }; 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /lib/katex/src/functions.js: -------------------------------------------------------------------------------- 1 | // 2 | /** Include this to ensure that all functions are defined. */ 3 | import {_functions} from "./defineFunction"; 4 | 5 | const functions = _functions; 6 | export default functions; 7 | 8 | // TODO(kevinb): have functions return an object and call defineFunction with 9 | // that object in this file instead of relying on side-effects. 10 | import "./functions/accent"; 11 | import "./functions/accentunder"; 12 | import "./functions/arrow"; 13 | import "./functions/pmb"; 14 | import "./environments/cd"; 15 | import "./functions/char"; 16 | import "./functions/color"; 17 | import "./functions/cr"; 18 | import "./functions/def"; 19 | import "./functions/delimsizing"; 20 | import "./functions/enclose"; 21 | import "./functions/environment"; 22 | import "./functions/font"; 23 | import "./functions/genfrac"; 24 | import "./functions/horizBrace"; 25 | import "./functions/href"; 26 | import "./functions/hbox"; 27 | import "./functions/html"; 28 | import "./functions/htmlmathml"; 29 | import "./functions/includegraphics"; 30 | import "./functions/kern"; 31 | import "./functions/lap"; 32 | import "./functions/math"; 33 | import "./functions/mathchoice"; 34 | import "./functions/mclass"; 35 | import "./functions/op"; 36 | import "./functions/operatorname"; 37 | import "./functions/ordgroup"; 38 | import "./functions/overline"; 39 | import "./functions/phantom"; 40 | import "./functions/raisebox"; 41 | import "./functions/relax"; 42 | import "./functions/rule"; 43 | import "./functions/sizing"; 44 | import "./functions/smash"; 45 | import "./functions/sqrt"; 46 | import "./functions/styling"; 47 | import "./functions/supsub"; 48 | import "./functions/symbolsOp"; 49 | import "./functions/symbolsOrd"; 50 | import "./functions/symbolsSpacing"; 51 | import "./functions/tag"; 52 | import "./functions/text"; 53 | import "./functions/underline"; 54 | import "./functions/vcenter"; 55 | import "./functions/verb"; 56 | -------------------------------------------------------------------------------- /wypst.js: -------------------------------------------------------------------------------- 1 | import init, { parseTree as _parseTree, typstContentTree } from './src/core/pkg'; 2 | import utils from './src/utils'; 3 | 4 | import wasm from './src/core/pkg/core_bg.wasm'; 5 | 6 | function parseTree(expression, settings) { 7 | expression = expression.trim().replace(/\n/g, ' '); 8 | return _parseTree(expression, settings); 9 | } 10 | 11 | function renderToDomTree(expression, options) { 12 | let settings = new utils.Settings(options); 13 | try { 14 | const tree = parseTree(expression, settings); 15 | return utils.buildTree(tree, expression, settings); 16 | } catch (error) { 17 | // Temporary fix so that we actually see errors like "unknown variable: ..." 18 | return utils.renderError(error, error, settings); 19 | } 20 | } 21 | 22 | /** 23 | * Renders a Typst expression into the specified DOM element 24 | * @param expression A Typst expression 25 | * @param element The DOM element to render into 26 | * @param options Render options 27 | */ 28 | function render(expression, baseNode, options) { 29 | baseNode.textContent = ""; 30 | const node = renderToDomTree(expression, options).toNode(); 31 | baseNode.appendChild(node); 32 | }; 33 | 34 | /** 35 | * Renders a Typst expression into an HTML string 36 | * @param expression A Typst expression 37 | * @param options Render options 38 | */ 39 | function renderToString(expression, options) { 40 | const markup = renderToDomTree(expression, options).toMarkup(); 41 | return markup; 42 | } 43 | 44 | async function initialize(path) { 45 | if (path) { 46 | await init(path); 47 | } else { 48 | await init(wasm); 49 | } 50 | } 51 | 52 | export default { 53 | render, 54 | renderToString, 55 | parseTree, 56 | __typstContentTree: typstContentTree, 57 | initialize, 58 | }; 59 | 60 | export { 61 | render, 62 | renderToString, 63 | parseTree, 64 | initialize, 65 | }; 66 | -------------------------------------------------------------------------------- /src/core/src/node.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use crate::katex; 3 | 4 | #[derive(Clone, Serialize)] 5 | #[serde(untagged)] 6 | pub enum Node { 7 | Node(katex::Node), 8 | Array(katex::NodeArray), 9 | } 10 | 11 | impl std::fmt::Debug for Node { 12 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 13 | match self { 14 | Node::Node(node) => write!(f, "{:?}", node), 15 | Node::Array(array) => write!(f, "{:?}", array), 16 | } 17 | } 18 | } 19 | 20 | impl Node { 21 | pub fn into_node(self) -> Result { 22 | match self { 23 | Node::Node(node) => Ok(node), 24 | Node::Array(array) => { 25 | if array.len() == 1 { 26 | // TODO: Must check if this code makes sense 27 | Ok(array.iter().next().cloned().unwrap()) 28 | } else { 29 | Err("Cannot convert an array with more than one element to a single node") 30 | } 31 | }, 32 | } 33 | } 34 | 35 | pub fn into_ordgroup(&self, mode: katex::Mode) -> katex::OrdGroup { 36 | katex::OrdGroupBuilder::default() 37 | .mode(mode) 38 | .body(self.clone().into_array()) 39 | .build().unwrap() 40 | } 41 | 42 | pub fn into_node_fallback_ordgroup(&self, mode: katex::Mode) -> katex::Node { 43 | match self.clone().into_node() { 44 | Ok(node) => node, 45 | Err(_) => self.into_ordgroup(mode).into_node(), 46 | } 47 | } 48 | 49 | pub fn into_array(self) -> katex::NodeArray { 50 | match self { 51 | Node::Node(node) => vec![node.clone()], 52 | Node::Array(array) => array, 53 | } 54 | } 55 | 56 | // pub fn join(&mut self, node: Node) { 57 | // let mut arr = self.clone().as_array(); 58 | // arr.append(&mut node.as_array()); 59 | // *self = Node::Array(arr); 60 | // } 61 | } 62 | -------------------------------------------------------------------------------- /lib/katex/contrib/copy-tex/copy-tex.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import katexReplaceWithTex from './katex2tex'; 4 | 5 | // Return
element containing node, or null if not found. 6 | function closestKatex(node ) { 7 | // If node is a Text Node, for example, go up to containing Element, 8 | // where we can apply the `closest` method. 9 | const element = 10 | (node instanceof Element ? node : node.parentElement); 11 | return element && element.closest('.katex'); 12 | } 13 | 14 | // Global copy handler to modify behavior on/within .katex elements. 15 | document.addEventListener('copy', function(event ) { 16 | const selection = window.getSelection(); 17 | if (selection.isCollapsed || !event.clipboardData) { 18 | return; // default action OK if selection is empty or unchangeable 19 | } 20 | const clipboardData = event.clipboardData; 21 | const range = selection.getRangeAt(0); 22 | 23 | // When start point is within a formula, expand to entire formula. 24 | const startKatex = closestKatex(range.startContainer); 25 | if (startKatex) { 26 | range.setStartBefore(startKatex); 27 | } 28 | 29 | // Similarly, when end point is within a formula, expand to entire formula. 30 | const endKatex = closestKatex(range.endContainer); 31 | if (endKatex) { 32 | range.setEndAfter(endKatex); 33 | } 34 | 35 | const fragment = range.cloneContents(); 36 | if (!fragment.querySelector('.katex-mathml')) { 37 | return; // default action OK if no .katex-mathml elements 38 | } 39 | 40 | const htmlContents = Array.prototype.map.call(fragment.childNodes, 41 | (el) => (el instanceof Text ? el.textContent : el.outerHTML) 42 | ).join(''); 43 | 44 | // Preserve usual HTML copy/paste behavior. 45 | clipboardData.setData('text/html', htmlContents); 46 | // Rewrite plain-text version. 47 | clipboardData.setData('text/plain', 48 | katexReplaceWithTex(fragment).textContent); 49 | // Prevent normal copy handling. 50 | event.preventDefault(); 51 | }); 52 | -------------------------------------------------------------------------------- /lib/katex/src/functions/overline.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | 6 | import * as html from "../buildHTML"; 7 | import * as mml from "../buildMathML"; 8 | 9 | defineFunction({ 10 | type: "overline", 11 | names: ["\\overline"], 12 | props: { 13 | numArgs: 1, 14 | }, 15 | handler({parser}, args) { 16 | const body = args[0]; 17 | return { 18 | type: "overline", 19 | mode: parser.mode, 20 | body, 21 | }; 22 | }, 23 | htmlBuilder(group, options) { 24 | // Overlines are handled in the TeXbook pg 443, Rule 9. 25 | 26 | // Build the inner group in the cramped style. 27 | const innerGroup = html.buildGroup(group.body, 28 | options.havingCrampedStyle()); 29 | 30 | // Create the line above the body 31 | const line = buildCommon.makeLineSpan("overline-line", options); 32 | 33 | // Generate the vlist, with the appropriate kerns 34 | const defaultRuleThickness = options.fontMetrics().defaultRuleThickness; 35 | const vlist = buildCommon.makeVList({ 36 | positionType: "firstBaseline", 37 | children: [ 38 | {type: "elem", elem: innerGroup}, 39 | {type: "kern", size: 3 * defaultRuleThickness}, 40 | {type: "elem", elem: line}, 41 | {type: "kern", size: defaultRuleThickness}, 42 | ], 43 | }, options); 44 | 45 | return buildCommon.makeSpan(["mord", "overline"], [vlist], options); 46 | }, 47 | mathmlBuilder(group, options) { 48 | const operator = new mathMLTree.MathNode( 49 | "mo", [new mathMLTree.TextNode("\u203e")]); 50 | operator.setAttribute("stretchy", "true"); 51 | 52 | const node = new mathMLTree.MathNode( 53 | "mover", 54 | [mml.buildGroup(group.body, options), operator]); 55 | node.setAttribute("accent", "true"); 56 | 57 | return node; 58 | }, 59 | }); 60 | -------------------------------------------------------------------------------- /lib/katex/src/functions/underline.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | 6 | import * as html from "../buildHTML"; 7 | import * as mml from "../buildMathML"; 8 | 9 | defineFunction({ 10 | type: "underline", 11 | names: ["\\underline"], 12 | props: { 13 | numArgs: 1, 14 | allowedInText: true, 15 | }, 16 | handler({parser}, args) { 17 | return { 18 | type: "underline", 19 | mode: parser.mode, 20 | body: args[0], 21 | }; 22 | }, 23 | htmlBuilder(group, options) { 24 | // Underlines are handled in the TeXbook pg 443, Rule 10. 25 | // Build the inner group. 26 | const innerGroup = html.buildGroup(group.body, options); 27 | 28 | // Create the line to go below the body 29 | const line = buildCommon.makeLineSpan("underline-line", options); 30 | 31 | // Generate the vlist, with the appropriate kerns 32 | const defaultRuleThickness = options.fontMetrics().defaultRuleThickness; 33 | const vlist = buildCommon.makeVList({ 34 | positionType: "top", 35 | positionData: innerGroup.height, 36 | children: [ 37 | {type: "kern", size: defaultRuleThickness}, 38 | {type: "elem", elem: line}, 39 | {type: "kern", size: 3 * defaultRuleThickness}, 40 | {type: "elem", elem: innerGroup}, 41 | ], 42 | }, options); 43 | 44 | return buildCommon.makeSpan(["mord", "underline"], [vlist], options); 45 | }, 46 | mathmlBuilder(group, options) { 47 | const operator = new mathMLTree.MathNode( 48 | "mo", [new mathMLTree.TextNode("\u203e")]); 49 | operator.setAttribute("stretchy", "true"); 50 | 51 | const node = new mathMLTree.MathNode( 52 | "munder", 53 | [mml.buildGroup(group.body, options), operator]); 54 | node.setAttribute("accentunder", "true"); 55 | 56 | return node; 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /lib/katex/src/functions/cr.js: -------------------------------------------------------------------------------- 1 | // 2 | // Row breaks within tabular environments, and line breaks at top level 3 | 4 | import defineFunction from "../defineFunction"; 5 | import buildCommon from "../buildCommon"; 6 | import mathMLTree from "../mathMLTree"; 7 | import {calculateSize, makeEm} from "../units"; 8 | import {assertNodeType} from "../parseNode"; 9 | 10 | // \DeclareRobustCommand\\{...\@xnewline} 11 | defineFunction({ 12 | type: "cr", 13 | names: ["\\\\"], 14 | props: { 15 | numArgs: 0, 16 | numOptionalArgs: 0, 17 | allowedInText: true, 18 | }, 19 | 20 | handler({parser}, args, optArgs) { 21 | const size = parser.gullet.future().text === "[" ? 22 | parser.parseSizeGroup(true) : null; 23 | const newLine = !parser.settings.displayMode || 24 | !parser.settings.useStrictBehavior( 25 | "newLineInDisplayMode", "In LaTeX, \\\\ or \\newline " + 26 | "does nothing in display mode"); 27 | return { 28 | type: "cr", 29 | mode: parser.mode, 30 | newLine, 31 | size: size && assertNodeType(size, "size").value, 32 | }; 33 | }, 34 | 35 | // The following builders are called only at the top level, 36 | // not within tabular/array environments. 37 | 38 | htmlBuilder(group, options) { 39 | const span = buildCommon.makeSpan(["mspace"], [], options); 40 | if (group.newLine) { 41 | span.classes.push("newline"); 42 | if (group.size) { 43 | span.style.marginTop = 44 | makeEm(calculateSize(group.size, options)); 45 | } 46 | } 47 | return span; 48 | }, 49 | 50 | mathmlBuilder(group, options) { 51 | const node = new mathMLTree.MathNode("mspace"); 52 | if (group.newLine) { 53 | node.setAttribute("linebreak", "newline"); 54 | if (group.size) { 55 | node.setAttribute("height", 56 | makeEm(calculateSize(group.size, options))); 57 | } 58 | } 59 | return node; 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /lib/katex/src/functions/kern.js: -------------------------------------------------------------------------------- 1 | // 2 | // Horizontal spacing commands 3 | 4 | import defineFunction from "../defineFunction"; 5 | import buildCommon from "../buildCommon"; 6 | import mathMLTree from "../mathMLTree"; 7 | import {calculateSize} from "../units"; 8 | import {assertNodeType} from "../parseNode"; 9 | 10 | // TODO: \hskip and \mskip should support plus and minus in lengths 11 | 12 | defineFunction({ 13 | type: "kern", 14 | names: ["\\kern", "\\mkern", "\\hskip", "\\mskip"], 15 | props: { 16 | numArgs: 1, 17 | argTypes: ["size"], 18 | primitive: true, 19 | allowedInText: true, 20 | }, 21 | handler({parser, funcName}, args) { 22 | const size = assertNodeType(args[0], "size"); 23 | if (parser.settings.strict) { 24 | const mathFunction = (funcName[1] === 'm'); // \mkern, \mskip 25 | const muUnit = (size.value.unit === 'mu'); 26 | if (mathFunction) { 27 | if (!muUnit) { 28 | parser.settings.reportNonstrict("mathVsTextUnits", 29 | `LaTeX's ${funcName} supports only mu units, ` + 30 | `not ${size.value.unit} units`); 31 | } 32 | if (parser.mode !== "math") { 33 | parser.settings.reportNonstrict("mathVsTextUnits", 34 | `LaTeX's ${funcName} works only in math mode`); 35 | } 36 | } else { // !mathFunction 37 | if (muUnit) { 38 | parser.settings.reportNonstrict("mathVsTextUnits", 39 | `LaTeX's ${funcName} doesn't support mu units`); 40 | } 41 | } 42 | } 43 | return { 44 | type: "kern", 45 | mode: parser.mode, 46 | dimension: size.value, 47 | }; 48 | }, 49 | htmlBuilder(group, options) { 50 | return buildCommon.makeGlue(group.dimension, options); 51 | }, 52 | mathmlBuilder(group, options) { 53 | const dimension = calculateSize(group.dimension, options); 54 | return new mathMLTree.SpaceNode(dimension); 55 | }, 56 | }); 57 | -------------------------------------------------------------------------------- /src/core/src/katex/types.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use crate::katex::NodeArray; 3 | 4 | use super::SourceLocation; 5 | 6 | #[derive(Clone, Serialize, PartialEq, Copy)] 7 | #[serde(rename_all = "lowercase")] 8 | pub enum Mode { 9 | Math, 10 | Text, 11 | } 12 | 13 | // Refernece: array.js 14 | #[derive(Clone, Serialize, PartialEq)] 15 | #[serde(rename_all = "lowercase")] 16 | pub enum ColSeparationType { // TODO 17 | Align, 18 | AlignAt, 19 | Gather, 20 | Small, 21 | #[serde(rename = "CD")] 22 | CD, 23 | } 24 | 25 | // Reference: array.js 26 | #[derive(Clone, Serialize)] 27 | #[serde(tag = "type", rename_all = "lowercase")] 28 | pub enum AlignSpec { 29 | Separator(Separator), 30 | Align(Align), 31 | } 32 | 33 | #[derive(Clone, Serialize)] 34 | pub struct Separator { 35 | pub separator: String, 36 | } 37 | 38 | #[derive(Clone, Serialize)] 39 | pub struct Align { 40 | pub align: String, 41 | pub pregap: Option, 42 | pub postgap: Option, 43 | } 44 | 45 | // Reference: units.js 46 | #[derive(Clone, Serialize)] 47 | pub struct Measurement { 48 | pub number: f32, 49 | pub unit: String, 50 | } 51 | 52 | #[derive(Clone, Serialize)] 53 | pub enum TagType { 54 | Bool(bool), 55 | NodeArray(NodeArray), 56 | } 57 | 58 | // Reference: types.js 59 | #[derive(Clone, Serialize)] 60 | #[serde(rename_all = "lowercase")] 61 | pub enum StyleStr { 62 | Text, 63 | Display, 64 | Script, 65 | ScriptScript, 66 | } 67 | 68 | #[derive(Clone, Serialize)] 69 | pub enum SizeType { // TODO: Check serialization 70 | One = 1, 71 | Two = 2, 72 | Three = 3, 73 | Four = 4, 74 | } 75 | 76 | #[derive(Clone, Serialize)] 77 | pub enum MClassType { 78 | MOpen, 79 | MClose, 80 | MRel, 81 | MOrd, 82 | } 83 | 84 | #[derive(Clone, Serialize)] 85 | #[serde(rename_all = "lowercase")] 86 | pub enum GenFracSizeType { 87 | StyleStr(StyleStr), 88 | Auto, 89 | } 90 | 91 | // Reference: Token.js 92 | #[derive(Clone, Serialize)] 93 | pub struct Token { 94 | pub text: String, 95 | pub loc: Option, 96 | pub noexpand: Option, 97 | pub treat_as_relax: Option, 98 | } 99 | -------------------------------------------------------------------------------- /lib/katex/src/functions/symbolsOrd.js: -------------------------------------------------------------------------------- 1 | // 2 | import {defineFunctionBuilders} from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | 6 | import * as mml from "../buildMathML"; 7 | 8 | 9 | 10 | // "mathord" and "textord" ParseNodes created in Parser.js from symbol Groups in 11 | // src/symbols.js. 12 | 13 | const defaultVariant = { 14 | "mi": "italic", 15 | "mn": "normal", 16 | "mtext": "normal", 17 | }; 18 | 19 | defineFunctionBuilders({ 20 | type: "mathord", 21 | htmlBuilder(group, options) { 22 | return buildCommon.makeOrd(group, options, "mathord"); 23 | }, 24 | mathmlBuilder(group , options) { 25 | const node = new mathMLTree.MathNode( 26 | "mi", 27 | [mml.makeText(group.text, group.mode, options)]); 28 | 29 | const variant = mml.getVariant(group, options) || "italic"; 30 | if (variant !== defaultVariant[node.type]) { 31 | node.setAttribute("mathvariant", variant); 32 | } 33 | return node; 34 | }, 35 | }); 36 | 37 | defineFunctionBuilders({ 38 | type: "textord", 39 | htmlBuilder(group, options) { 40 | return buildCommon.makeOrd(group, options, "textord"); 41 | }, 42 | mathmlBuilder(group , options) { 43 | const text = mml.makeText(group.text, group.mode, options); 44 | const variant = mml.getVariant(group, options) || "normal"; 45 | 46 | let node; 47 | if (group.mode === 'text') { 48 | node = new mathMLTree.MathNode("mtext", [text]); 49 | } else if (/[0-9]/.test(group.text)) { 50 | node = new mathMLTree.MathNode("mn", [text]); 51 | } else if (group.text === "\\prime") { 52 | node = new mathMLTree.MathNode("mo", [text]); 53 | } else { 54 | node = new mathMLTree.MathNode("mi", [text]); 55 | } 56 | if (variant !== defaultVariant[node.type]) { 57 | node.setAttribute("mathvariant", variant); 58 | } 59 | 60 | return node; 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /lib/katex/src/functions/accentunder.js: -------------------------------------------------------------------------------- 1 | // 2 | // Horizontal overlap functions 3 | import defineFunction from "../defineFunction"; 4 | import buildCommon from "../buildCommon"; 5 | import mathMLTree from "../mathMLTree"; 6 | import stretchy from "../stretchy"; 7 | 8 | import * as html from "../buildHTML"; 9 | import * as mml from "../buildMathML"; 10 | 11 | 12 | 13 | defineFunction({ 14 | type: "accentUnder", 15 | names: [ 16 | "\\underleftarrow", "\\underrightarrow", "\\underleftrightarrow", 17 | "\\undergroup", "\\underlinesegment", "\\utilde", 18 | ], 19 | props: { 20 | numArgs: 1, 21 | }, 22 | handler: ({parser, funcName}, args) => { 23 | const base = args[0]; 24 | return { 25 | type: "accentUnder", 26 | mode: parser.mode, 27 | label: funcName, 28 | base: base, 29 | }; 30 | }, 31 | htmlBuilder: (group , options) => { 32 | // Treat under accents much like underlines. 33 | const innerGroup = html.buildGroup(group.base, options); 34 | 35 | const accentBody = stretchy.svgSpan(group, options); 36 | const kern = group.label === "\\utilde" ? 0.12 : 0; 37 | 38 | // Generate the vlist, with the appropriate kerns 39 | const vlist = buildCommon.makeVList({ 40 | positionType: "top", 41 | positionData: innerGroup.height, 42 | children: [ 43 | {type: "elem", elem: accentBody, wrapperClasses: ["svg-align"]}, 44 | {type: "kern", size: kern}, 45 | {type: "elem", elem: innerGroup}, 46 | ], 47 | }, options); 48 | 49 | return buildCommon.makeSpan(["mord", "accentunder"], [vlist], options); 50 | }, 51 | mathmlBuilder: (group, options) => { 52 | const accentNode = stretchy.mathMLnode(group.label); 53 | const node = new mathMLTree.MathNode( 54 | "munder", 55 | [mml.buildGroup(group.base, options), accentNode] 56 | ); 57 | node.setAttribute("accentunder", "true"); 58 | return node; 59 | }, 60 | }); 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wypst 2 | Typst math typesetting for the web. 3 | 4 | ## Current Project Status 5 | I feel like there's something like 20% of work left to get Wypst in a more stable state, but for some months now I haven't been able to allocate the necessary time to do it. 6 | 7 | There's still no conclusive roadmap for native HTML export in Typst, and because this project gained some traction, I'll make a commitment to complete all unimplemented functionality, fix issues, and add whatever is needed to achieve equivalency of `obsidian-wypst` to `obsidian-typst`. 8 | 9 | ## Usage 10 | You can load this library either by using a script tag, or installing it with npm. 11 | 12 | ### Script tag (simple usage) 13 | ```html 14 | 15 | 16 | 17 | 22 | ``` 23 | 24 | Keep in mind that the javascript file is 17M, so if your internet is slow it might take some seconds to load. 25 | 26 | ### npm package (advanced usage) 27 | If having the wasm inlined directly is an incovenience, install the npm package 28 | ```bash 29 | npm install wypst 30 | ``` 31 | 32 | You may then load the wasm binary 33 | ```javascript 34 | import wypst from 'wypst'; 35 | import wasm from 'wypst/dist/wypst.wasm'; 36 | 37 | await wypst.initialize(wasm); 38 | wypst.renderToString("x + y"); // Test it out! 39 | ``` 40 | 41 | Keep in mind that you will probably need to tell your bundler how to load a `.wasm` file. If you have difficulties you can open an issue. 42 | 43 | ### Rendering Typst Math 44 | To render a Typst math expression, you can use either `render` or `renderToString`, as the example below shows: 45 | ```javascript 46 | wypst.render('sum_(n >= 1) 1/n^2 = pi^2/6', element); // Renders into the HTML element 47 | wypst.renderToString('sum_(n >= 1) 1/n^2 = pi^2/6'); // Renders into an HTML string 48 | ``` 49 | 50 | ## Contributing 51 | All help is welcome. Please see [CONTRIBUTING](CONTRIBUTING.md). 52 | -------------------------------------------------------------------------------- /lib/katex/src/functions/verb.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | import ParseError from "../ParseError"; 6 | 7 | 8 | 9 | defineFunction({ 10 | type: "verb", 11 | names: ["\\verb"], 12 | props: { 13 | numArgs: 0, 14 | allowedInText: true, 15 | }, 16 | handler(context, args, optArgs) { 17 | // \verb and \verb* are dealt with directly in Parser.js. 18 | // If we end up here, it's because of a failure to match the two delimiters 19 | // in the regex in Lexer.js. LaTeX raises the following error when \verb is 20 | // terminated by end of line (or file). 21 | throw new ParseError( 22 | "\\verb ended by end of line instead of matching delimiter"); 23 | }, 24 | htmlBuilder(group, options) { 25 | const text = makeVerb(group); 26 | const body = []; 27 | // \verb enters text mode and therefore is sized like \textstyle 28 | const newOptions = options.havingStyle(options.style.text()); 29 | for (let i = 0; i < text.length; i++) { 30 | let c = text[i]; 31 | if (c === '~') { 32 | c = '\\textasciitilde'; 33 | } 34 | body.push(buildCommon.makeSymbol(c, "Typewriter-Regular", 35 | group.mode, newOptions, ["mord", "texttt"])); 36 | } 37 | return buildCommon.makeSpan( 38 | ["mord", "text"].concat(newOptions.sizingClasses(options)), 39 | buildCommon.tryCombineChars(body), 40 | newOptions, 41 | ); 42 | }, 43 | mathmlBuilder(group, options) { 44 | const text = new mathMLTree.TextNode(makeVerb(group)); 45 | const node = new mathMLTree.MathNode("mtext", [text]); 46 | node.setAttribute("mathvariant", "monospace"); 47 | return node; 48 | }, 49 | }); 50 | 51 | /** 52 | * Converts verb group into body string. 53 | * 54 | * \verb* replaces each space with an open box \u2423 55 | * \verb replaces each space with a no-break space \xA0 56 | */ 57 | const makeVerb = (group ) => 58 | group.body.replace(/ /g, group.star ? '\u2423' : '\xA0'); 59 | -------------------------------------------------------------------------------- /lib/katex/src/buildTree.js: -------------------------------------------------------------------------------- 1 | // 2 | import buildHTML from "./buildHTML"; 3 | import buildMathML from "./buildMathML"; 4 | import buildCommon from "./buildCommon"; 5 | import Options from "./Options"; 6 | import Settings from "./Settings"; 7 | import Style from "./Style"; 8 | 9 | 10 | 11 | 12 | const optionsFromSettings = function(settings ) { 13 | return new Options({ 14 | style: (settings.displayMode ? Style.DISPLAY : Style.TEXT), 15 | maxSize: settings.maxSize, 16 | minRuleThickness: settings.minRuleThickness, 17 | }); 18 | }; 19 | 20 | const displayWrap = function(node , settings ) { 21 | if (settings.displayMode) { 22 | const classes = ["katex-display"]; 23 | if (settings.leqno) { 24 | classes.push("leqno"); 25 | } 26 | if (settings.fleqn) { 27 | classes.push("fleqn"); 28 | } 29 | node = buildCommon.makeSpan(classes, [node]); 30 | } 31 | return node; 32 | }; 33 | 34 | export const buildTree = function( 35 | tree , 36 | expression , 37 | settings , 38 | ) { 39 | const options = optionsFromSettings(settings); 40 | let katexNode; 41 | if (settings.output === "mathml") { 42 | return buildMathML(tree, expression, options, settings.displayMode, true); 43 | } else if (settings.output === "html") { 44 | const htmlNode = buildHTML(tree, options); 45 | katexNode = buildCommon.makeSpan(["katex"], [htmlNode]); 46 | } else { 47 | const mathMLNode = buildMathML(tree, expression, options, 48 | settings.displayMode, false); 49 | const htmlNode = buildHTML(tree, options); 50 | katexNode = buildCommon.makeSpan(["katex"], [mathMLNode, htmlNode]); 51 | } 52 | 53 | return displayWrap(katexNode, settings); 54 | }; 55 | 56 | export const buildHTMLTree = function( 57 | tree , 58 | expression , 59 | settings , 60 | ) { 61 | const options = optionsFromSettings(settings); 62 | const htmlNode = buildHTML(tree, options); 63 | const katexNode = buildCommon.makeSpan(["katex"], [htmlNode]); 64 | return displayWrap(katexNode, settings); 65 | }; 66 | 67 | export default buildTree; 68 | -------------------------------------------------------------------------------- /lib/katex/src/functions/text.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction, {ordargument} from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | 5 | import * as html from "../buildHTML"; 6 | import * as mml from "../buildMathML"; 7 | 8 | // Non-mathy text, possibly in a font 9 | const textFontFamilies = { 10 | "\\text": undefined, "\\textrm": "textrm", "\\textsf": "textsf", 11 | "\\texttt": "texttt", "\\textnormal": "textrm", 12 | }; 13 | 14 | const textFontWeights = { 15 | "\\textbf": "textbf", 16 | "\\textmd": "textmd", 17 | }; 18 | 19 | const textFontShapes = { 20 | "\\textit": "textit", 21 | "\\textup": "textup", 22 | }; 23 | 24 | const optionsWithFont = (group, options) => { 25 | const font = group.font; 26 | // Checks if the argument is a font family or a font style. 27 | if (!font) { 28 | return options; 29 | } else if (textFontFamilies[font]) { 30 | return options.withTextFontFamily(textFontFamilies[font]); 31 | } else if (textFontWeights[font]) { 32 | return options.withTextFontWeight(textFontWeights[font]); 33 | } else { 34 | return options.withTextFontShape(textFontShapes[font]); 35 | } 36 | }; 37 | 38 | defineFunction({ 39 | type: "text", 40 | names: [ 41 | // Font families 42 | "\\text", "\\textrm", "\\textsf", "\\texttt", "\\textnormal", 43 | // Font weights 44 | "\\textbf", "\\textmd", 45 | // Font Shapes 46 | "\\textit", "\\textup", 47 | ], 48 | props: { 49 | numArgs: 1, 50 | argTypes: ["text"], 51 | allowedInArgument: true, 52 | allowedInText: true, 53 | }, 54 | handler({parser, funcName}, args) { 55 | const body = args[0]; 56 | return { 57 | type: "text", 58 | mode: parser.mode, 59 | body: ordargument(body), 60 | font: funcName, 61 | }; 62 | }, 63 | htmlBuilder(group, options) { 64 | const newOptions = optionsWithFont(group, options); 65 | const inner = html.buildExpression(group.body, newOptions, true); 66 | return buildCommon.makeSpan(["mord", "text"], inner, newOptions); 67 | }, 68 | mathmlBuilder(group, options) { 69 | const newOptions = optionsWithFont(group, options); 70 | return mml.buildExpressionRow(group.body, newOptions); 71 | }, 72 | }); 73 | -------------------------------------------------------------------------------- /src/core/src/katex/constructor.rs: -------------------------------------------------------------------------------- 1 | use crate::katex::*; 2 | 3 | pub struct ArrayConstructor { 4 | pub body: NodeArray2D, 5 | pub h_lines_before_row: Vec>, 6 | pub cols: Option>, 7 | } 8 | 9 | impl ArrayConstructor { 10 | pub fn default() -> Self { 11 | Self { 12 | body: vec![], 13 | h_lines_before_row: vec![vec![]], 14 | cols: None 15 | } 16 | } 17 | 18 | pub fn next_row(&mut self) { 19 | self.body.push(Vec::new()); 20 | self.h_lines_before_row.push(Vec::new()); 21 | } 22 | 23 | pub fn push_node(&mut self, node: Node) { 24 | self.body.last_mut().unwrap().push(node); 25 | } 26 | 27 | pub fn map_body(&mut self, f: &dyn Fn(&mut Node) -> Node) { 28 | for row in self.body.iter_mut() { 29 | for node in row.iter_mut() { 30 | *node = f(node); 31 | } 32 | } 33 | } 34 | 35 | pub fn count_columns(&self) -> usize { 36 | self.body.iter().map(|row| row.len()).max().unwrap_or(0) 37 | } 38 | 39 | pub fn count_rows(&self) -> usize { 40 | self.body.iter().count() 41 | } 42 | 43 | pub fn cols_leftright_align(&mut self) -> &mut Self { 44 | let mut cols: Vec = Vec::new(); 45 | for i in 0..self.count_columns() { 46 | let align = Align { 47 | align: if i % 2 == 0 { "r".to_string() } else { "l".to_string() }, 48 | pregap: if i > 1 && i % 2 == 0 { Some(1f32) } else { Some(0f32) }, 49 | postgap: Some(0f32), 50 | }; 51 | cols.push(AlignSpec::Align(align)); 52 | } 53 | self.cols = Some(cols); 54 | self 55 | } 56 | 57 | pub fn cols_center_align(&mut self) -> &mut Self { 58 | let mut cols: Vec = Vec::new(); 59 | for i in 0..self.count_columns() { 60 | let align = Align { 61 | align: "c".to_string(), 62 | pregap: None, 63 | postgap: None 64 | }; 65 | cols.push(AlignSpec::Align(align)); 66 | } 67 | self.cols = Some(cols); 68 | self 69 | } 70 | 71 | pub fn builder(&self) -> ArrayBuilder { 72 | ArrayBuilder::default() 73 | .body(self.body.clone()) 74 | .h_lines_before_row(self.h_lines_before_row.clone()) 75 | .cols(self.cols.clone()).clone() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/katex/src/functions/environment.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction from "../defineFunction"; 3 | import ParseError from "../ParseError"; 4 | import {assertNodeType} from "../parseNode"; 5 | import environments from "../environments"; 6 | 7 | // Environment delimiters. HTML/MathML rendering is defined in the corresponding 8 | // defineEnvironment definitions. 9 | defineFunction({ 10 | type: "environment", 11 | names: ["\\begin", "\\end"], 12 | props: { 13 | numArgs: 1, 14 | argTypes: ["text"], 15 | }, 16 | handler({parser, funcName}, args) { 17 | const nameGroup = args[0]; 18 | if (nameGroup.type !== "ordgroup") { 19 | throw new ParseError("Invalid environment name", nameGroup); 20 | } 21 | let envName = ""; 22 | for (let i = 0; i < nameGroup.body.length; ++i) { 23 | envName += assertNodeType(nameGroup.body[i], "textord").text; 24 | } 25 | 26 | if (funcName === "\\begin") { 27 | // begin...end is similar to left...right 28 | if (!environments.hasOwnProperty(envName)) { 29 | throw new ParseError( 30 | "No such environment: " + envName, nameGroup); 31 | } 32 | // Build the environment object. Arguments and other information will 33 | // be made available to the begin and end methods using properties. 34 | const env = environments[envName]; 35 | const {args, optArgs} = 36 | parser.parseArguments("\\begin{" + envName + "}", env); 37 | const context = { 38 | mode: parser.mode, 39 | envName, 40 | parser, 41 | }; 42 | const result = env.handler(context, args, optArgs); 43 | parser.expect("\\end", false); 44 | const endNameToken = parser.nextToken; 45 | const end = assertNodeType(parser.parseFunction(), "environment"); 46 | if (end.name !== envName) { 47 | throw new ParseError( 48 | `Mismatch: \\begin{${envName}} matched by \\end{${end.name}}`, 49 | endNameToken); 50 | } 51 | // $FlowFixMe, "environment" handler returns an environment ParseNode 52 | return result; 53 | } 54 | 55 | return { 56 | type: "environment", 57 | mode: parser.mode, 58 | name: envName, 59 | nameGroup, 60 | }; 61 | }, 62 | }); 63 | -------------------------------------------------------------------------------- /lib/katex/src/tree.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import utils from "./utils"; 4 | 5 | 6 | 7 | 8 | 9 | // To ensure that all nodes have compatible signatures for these methods. 10 | 11 | 12 | 13 | 14 | 15 | 16 | /** 17 | * This node represents a document fragment, which contains elements, but when 18 | * placed into the DOM doesn't have any representation itself. It only contains 19 | * children and doesn't have any DOM node properties. 20 | */ 21 | export class DocumentFragment 22 | { 23 | children ; 24 | // HtmlDomNode 25 | classes ; 26 | height ; 27 | depth ; 28 | maxFontSize ; 29 | style ; // Never used; needed for satisfying interface. 30 | 31 | constructor(children ) { 32 | this.children = children; 33 | this.classes = []; 34 | this.height = 0; 35 | this.depth = 0; 36 | this.maxFontSize = 0; 37 | this.style = {}; 38 | } 39 | 40 | hasClass(className ) { 41 | return utils.contains(this.classes, className); 42 | } 43 | 44 | /** Convert the fragment into a node. */ 45 | toNode() { 46 | const frag = document.createDocumentFragment(); 47 | 48 | for (let i = 0; i < this.children.length; i++) { 49 | frag.appendChild(this.children[i].toNode()); 50 | } 51 | 52 | return frag; 53 | } 54 | 55 | /** Convert the fragment into HTML markup. */ 56 | toMarkup() { 57 | let markup = ""; 58 | 59 | // Simply concatenate the markup for the children together. 60 | for (let i = 0; i < this.children.length; i++) { 61 | markup += this.children[i].toMarkup(); 62 | } 63 | 64 | return markup; 65 | } 66 | 67 | /** 68 | * Converts the math node into a string, similar to innerText. Applies to 69 | * MathDomNode's only. 70 | */ 71 | toText() { 72 | // To avoid this, we would subclass documentFragment separately for 73 | // MathML, but polyfills for subclassing is expensive per PR 1469. 74 | // $FlowFixMe: Only works for ChildType = MathDomNode. 75 | const toText = (child ) => child.toText(); 76 | return this.children.map(toText).join(""); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/katex/src/unicodeSupOrSub.js: -------------------------------------------------------------------------------- 1 | // Helpers for Parser.js handling of Unicode (sub|super)script characters. 2 | 3 | export const unicodeSubRegEx = /^[₊₋₌₍₎₀₁₂₃₄₅₆₇₈₉ₐₑₕᵢⱼₖₗₘₙₒₚᵣₛₜᵤᵥₓᵦᵧᵨᵩᵪ]/; 4 | 5 | export const uSubsAndSups = Object.freeze({ 6 | '₊': '+', 7 | '₋': '-', 8 | '₌': '=', 9 | '₍': '(', 10 | '₎': ')', 11 | '₀': '0', 12 | '₁': '1', 13 | '₂': '2', 14 | '₃': '3', 15 | '₄': '4', 16 | '₅': '5', 17 | '₆': '6', 18 | '₇': '7', 19 | '₈': '8', 20 | '₉': '9', 21 | '\u2090': 'a', 22 | '\u2091': 'e', 23 | '\u2095': 'h', 24 | '\u1D62': 'i', 25 | '\u2C7C': 'j', 26 | '\u2096': 'k', 27 | '\u2097': 'l', 28 | '\u2098': 'm', 29 | '\u2099': 'n', 30 | '\u2092': 'o', 31 | '\u209A': 'p', 32 | '\u1D63': 'r', 33 | '\u209B': 's', 34 | '\u209C': 't', 35 | '\u1D64': 'u', 36 | '\u1D65': 'v', 37 | '\u2093': 'x', 38 | '\u1D66': 'β', 39 | '\u1D67': 'γ', 40 | '\u1D68': 'ρ', 41 | '\u1D69': '\u03d5', 42 | '\u1D6A': 'χ', 43 | '⁺': '+', 44 | '⁻': '-', 45 | '⁼': '=', 46 | '⁽': '(', 47 | '⁾': ')', 48 | '⁰': '0', 49 | '¹': '1', 50 | '²': '2', 51 | '³': '3', 52 | '⁴': '4', 53 | '⁵': '5', 54 | '⁶': '6', 55 | '⁷': '7', 56 | '⁸': '8', 57 | '⁹': '9', 58 | '\u1D2C': 'A', 59 | '\u1D2E': 'B', 60 | '\u1D30': 'D', 61 | '\u1D31': 'E', 62 | '\u1D33': 'G', 63 | '\u1D34': 'H', 64 | '\u1D35': 'I', 65 | '\u1D36': 'J', 66 | '\u1D37': 'K', 67 | '\u1D38': 'L', 68 | '\u1D39': 'M', 69 | '\u1D3A': 'N', 70 | '\u1D3C': 'O', 71 | '\u1D3E': 'P', 72 | '\u1D3F': 'R', 73 | '\u1D40': 'T', 74 | '\u1D41': 'U', 75 | '\u2C7D': 'V', 76 | '\u1D42': 'W', 77 | '\u1D43': 'a', 78 | '\u1D47': 'b', 79 | '\u1D9C': 'c', 80 | '\u1D48': 'd', 81 | '\u1D49': 'e', 82 | '\u1DA0': 'f', 83 | '\u1D4D': 'g', 84 | '\u02B0': 'h', 85 | '\u2071': 'i', 86 | '\u02B2': 'j', 87 | '\u1D4F': 'k', 88 | '\u02E1': 'l', 89 | '\u1D50': 'm', 90 | '\u207F': 'n', 91 | '\u1D52': 'o', 92 | '\u1D56': 'p', 93 | '\u02B3': 'r', 94 | '\u02E2': 's', 95 | '\u1D57': 't', 96 | '\u1D58': 'u', 97 | '\u1D5B': 'v', 98 | '\u02B7': 'w', 99 | '\u02E3': 'x', 100 | '\u02B8': 'y', 101 | '\u1DBB': 'z', 102 | '\u1D5D': 'β', 103 | '\u1D5E': 'γ', 104 | '\u1D5F': 'δ', 105 | '\u1D60': '\u03d5', 106 | '\u1D61': 'χ', 107 | '\u1DBF': 'θ', 108 | }); 109 | -------------------------------------------------------------------------------- /lib/katex/src/functions/styling.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction from "../defineFunction"; 3 | import mathMLTree from "../mathMLTree"; 4 | import Style from "../Style"; 5 | import {sizingGroup} from "./sizing"; 6 | 7 | import * as mml from "../buildMathML"; 8 | 9 | const styleMap = { 10 | "display": Style.DISPLAY, 11 | "text": Style.TEXT, 12 | "script": Style.SCRIPT, 13 | "scriptscript": Style.SCRIPTSCRIPT, 14 | }; 15 | 16 | defineFunction({ 17 | type: "styling", 18 | names: [ 19 | "\\displaystyle", "\\textstyle", "\\scriptstyle", 20 | "\\scriptscriptstyle", 21 | ], 22 | props: { 23 | numArgs: 0, 24 | allowedInText: true, 25 | primitive: true, 26 | }, 27 | handler({breakOnTokenText, funcName, parser}, args) { 28 | // parse out the implicit body 29 | const body = parser.parseExpression(true, breakOnTokenText); 30 | 31 | // TODO: Refactor to avoid duplicating styleMap in multiple places (e.g. 32 | // here and in buildHTML and de-dupe the enumeration of all the styles). 33 | // $FlowFixMe: The names above exactly match the styles. 34 | const style = funcName.slice(1, funcName.length - 5); 35 | return { 36 | type: "styling", 37 | mode: parser.mode, 38 | // Figure out what style to use by pulling out the style from 39 | // the function name 40 | style, 41 | body, 42 | }; 43 | }, 44 | htmlBuilder(group, options) { 45 | // Style changes are handled in the TeXbook on pg. 442, Rule 3. 46 | const newStyle = styleMap[group.style]; 47 | const newOptions = options.havingStyle(newStyle).withFont(''); 48 | return sizingGroup(group.body, newOptions, options); 49 | }, 50 | mathmlBuilder(group, options) { 51 | // Figure out what style we're changing to. 52 | const newStyle = styleMap[group.style]; 53 | const newOptions = options.havingStyle(newStyle); 54 | 55 | const inner = mml.buildExpression(group.body, newOptions); 56 | 57 | const node = new mathMLTree.MathNode("mstyle", inner); 58 | 59 | const styleAttributes = { 60 | "display": ["0", "true"], 61 | "text": ["0", "false"], 62 | "script": ["1", "false"], 63 | "scriptscript": ["2", "false"], 64 | }; 65 | 66 | const attr = styleAttributes[group.style]; 67 | 68 | node.setAttribute("scriptlevel", attr[0]); 69 | node.setAttribute("displaystyle", attr[1]); 70 | 71 | return node; 72 | }, 73 | }); 74 | -------------------------------------------------------------------------------- /lib/katex/contrib/auto-render/splitAtDelimiters.js: -------------------------------------------------------------------------------- 1 | /* eslint no-constant-condition:0 */ 2 | const findEndOfMath = function(delimiter, text, startIndex) { 3 | // Adapted from 4 | // https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx 5 | let index = startIndex; 6 | let braceLevel = 0; 7 | 8 | const delimLength = delimiter.length; 9 | 10 | while (index < text.length) { 11 | const character = text[index]; 12 | 13 | if (braceLevel <= 0 && 14 | text.slice(index, index + delimLength) === delimiter) { 15 | return index; 16 | } else if (character === "\\") { 17 | index++; 18 | } else if (character === "{") { 19 | braceLevel++; 20 | } else if (character === "}") { 21 | braceLevel--; 22 | } 23 | 24 | index++; 25 | } 26 | 27 | return -1; 28 | }; 29 | 30 | const escapeRegex = function(string) { 31 | return string.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); 32 | }; 33 | 34 | const amsRegex = /^\\begin{/; 35 | 36 | const splitAtDelimiters = function(text, delimiters) { 37 | let index; 38 | const data = []; 39 | 40 | const regexLeft = new RegExp( 41 | "(" + delimiters.map((x) => escapeRegex(x.left)).join("|") + ")" 42 | ); 43 | 44 | while (true) { 45 | index = text.search(regexLeft); 46 | if (index === -1) { 47 | break; 48 | } 49 | if (index > 0) { 50 | data.push({ 51 | type: "text", 52 | data: text.slice(0, index), 53 | }); 54 | text = text.slice(index); // now text starts with delimiter 55 | } 56 | // ... so this always succeeds: 57 | const i = delimiters.findIndex((delim) => text.startsWith(delim.left)); 58 | index = findEndOfMath(delimiters[i].right, text, delimiters[i].left.length); 59 | if (index === -1) { 60 | break; 61 | } 62 | const rawData = text.slice(0, index + delimiters[i].right.length); 63 | const math = amsRegex.test(rawData) 64 | ? rawData 65 | : text.slice(delimiters[i].left.length, index); 66 | data.push({ 67 | type: "math", 68 | data: math, 69 | rawData, 70 | display: delimiters[i].display, 71 | }); 72 | text = text.slice(index + delimiters[i].right.length); 73 | } 74 | 75 | if (text !== "") { 76 | data.push({ 77 | type: "text", 78 | data: text, 79 | }); 80 | } 81 | 82 | return data; 83 | }; 84 | 85 | export default splitAtDelimiters; 86 | -------------------------------------------------------------------------------- /lib/katex/contrib/copy-tex/katex2tex.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | 4 | 5 | 6 | 7 | 8 | // Set these to how you want inline and display math to be delimited. 9 | export const defaultCopyDelimiters = { 10 | inline: ['$', '$'], // alternative: ['\(', '\)'] 11 | display: ['$$', '$$'], // alternative: ['\[', '\]'] 12 | }; 13 | 14 | // Replace .katex elements with their TeX source ( element). 15 | // Modifies fragment in-place. Useful for writing your own 'copy' handler, 16 | // as in copy-tex.js. 17 | export function katexReplaceWithTex( 18 | fragment , 19 | copyDelimiters = defaultCopyDelimiters 20 | ) { 21 | // Remove .katex-html blocks that are preceded by .katex-mathml blocks 22 | // (which will get replaced below). 23 | const katexHtml = fragment.querySelectorAll('.katex-mathml + .katex-html'); 24 | for (let i = 0; i < katexHtml.length; i++) { 25 | const element = katexHtml[i]; 26 | if (element.remove) { 27 | element.remove(); 28 | } else if (element.parentNode) { 29 | element.parentNode.removeChild(element); 30 | } 31 | } 32 | // Replace .katex-mathml elements with their annotation (TeX source) 33 | // descendant, with inline delimiters. 34 | const katexMathml = fragment.querySelectorAll('.katex-mathml'); 35 | for (let i = 0; i < katexMathml.length; i++) { 36 | const element = katexMathml[i]; 37 | const texSource = element.querySelector('annotation'); 38 | if (texSource) { 39 | if (element.replaceWith) { 40 | element.replaceWith(texSource); 41 | } else if (element.parentNode) { 42 | element.parentNode.replaceChild(texSource, element); 43 | } 44 | texSource.innerHTML = copyDelimiters.inline[0] + 45 | texSource.innerHTML + copyDelimiters.inline[1]; 46 | } 47 | } 48 | // Switch display math to display delimiters. 49 | const displays = fragment.querySelectorAll('.katex-display annotation'); 50 | for (let i = 0; i < displays.length; i++) { 51 | const element = displays[i]; 52 | element.innerHTML = copyDelimiters.display[0] + 53 | element.innerHTML.substr(copyDelimiters.inline[0].length, 54 | element.innerHTML.length - copyDelimiters.inline[0].length 55 | - copyDelimiters.inline[1].length) 56 | + copyDelimiters.display[1]; 57 | } 58 | return fragment; 59 | } 60 | 61 | export default katexReplaceWithTex; 62 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import parseTree from 'katex/src/parseTree'; 2 | import Settings from 'katex/src/Settings'; 3 | import katex from 'katex'; 4 | import wypst from 'wypst'; 5 | import { deleteFields } from './utils'; 6 | 7 | let renderDiv = document.getElementById('render'); 8 | 9 | function katexRender() { 10 | let katexDiv = document.getElementById('katex'); 11 | 12 | let input = katexDiv.querySelector('#input'); 13 | let output = katexDiv.querySelector('#output'); 14 | 15 | input.addEventListener('input', function() { 16 | // Print KaTeX parse tree 17 | try { 18 | let tree = parseTree(input.value, new Settings({displayMode: true, strict: "ignore"})); 19 | deleteFields(tree, ['loc']); 20 | 21 | let treeHTML = prettyJsonToHtml(tree); 22 | output.innerHTML = treeHTML; 23 | } catch (error) { 24 | output.innerHTML = error; 25 | } 26 | 27 | // Render the equation 28 | try { 29 | katex.render(input.value, renderDiv, {displayMode: true}); 30 | } catch (error) { console.log(error); } 31 | }); 32 | } 33 | katexRender(); 34 | 35 | async function wypstRender() { 36 | let wypstDiv = document.getElementById('wypst'); 37 | let typstDiv = document.getElementById('typst'); 38 | 39 | let input = wypstDiv.querySelector('#input'); 40 | let wypstOutput = wypstDiv.querySelector('#output'); 41 | let typstOutput = typstDiv.querySelector('#output'); 42 | 43 | await wypst.init(); 44 | input.addEventListener('input', async function() { 45 | // Print Typst parse tree (in KaTeX representation) 46 | try { 47 | let tree = wypst.parseTree(input.value); 48 | let treeHTML = prettyJsonToHtml(tree); 49 | 50 | wypstOutput.innerHTML = treeHTML; 51 | } catch (error) { 52 | wypstOutput.innerHTML = error; 53 | } 54 | 55 | // Print Typst content tree 56 | try { 57 | let tree = wypst.__typstContentTree(input.value); 58 | let treeHTML = prettyStringToHtml(tree); 59 | typstOutput.innerHTML = treeHTML; 60 | } catch (error) { 61 | typstOutput.innerHTML = ''; 62 | } 63 | 64 | // Render the equation 65 | try { 66 | wypst.render(input.value, renderDiv, {displayMode: true}); 67 | } catch (error) { } 68 | }); 69 | } 70 | wypstRender() 71 | 72 | function prettyJsonToHtml(json) { 73 | let s = JSON.stringify(json, null, 4); 74 | return prettyStringToHtml(s); 75 | } 76 | 77 | function prettyStringToHtml(s) { 78 | return s.replace(/\n/g, '
').replace(/ /g, ' '); 79 | } 80 | -------------------------------------------------------------------------------- /src/core/src/utils.rs: -------------------------------------------------------------------------------- 1 | use comemo::Prehashed; 2 | use comemo::Track; 3 | use typst; 4 | use typst::World; 5 | 6 | pub struct FakeWorld { 7 | library: Prehashed, 8 | } 9 | 10 | impl FakeWorld { 11 | pub fn new() -> Self { 12 | FakeWorld { 13 | library: Prehashed::new(typst::Library::build()), 14 | } 15 | } 16 | } 17 | 18 | impl World for FakeWorld { 19 | fn library(&self) -> &Prehashed { 20 | &self.library 21 | } 22 | fn book(&self) -> &Prehashed { 23 | unimplemented!(); 24 | } 25 | fn file(&self,id:typst_syntax::FileId) -> typst::diag::FileResult { 26 | unimplemented!(); 27 | } 28 | fn font(&self,index:usize) -> Option { 29 | unimplemented!(); 30 | } 31 | fn main(&self) -> typst_syntax::Source { 32 | unimplemented!(); 33 | } 34 | fn packages(&self) -> &[(typst_syntax::PackageSpec,Option)] { 35 | unimplemented!(); 36 | } 37 | fn source(&self,id:typst_syntax::FileId) -> typst::diag::FileResult { 38 | unimplemented!(); 39 | } 40 | fn today(&self,offset:Option) -> Option { 41 | unimplemented!(); 42 | } 43 | } 44 | 45 | pub fn eval(world: &dyn typst::World, string: &str) -> Result { 46 | // Make engine 47 | let introspector = typst::introspection::Introspector::default(); 48 | let mut locator = typst::introspection::Locator::default(); 49 | let mut tracer = typst::eval::Tracer::default(); 50 | 51 | let engine = typst::engine::Engine { 52 | world: world.track(), 53 | introspector: introspector.track(), 54 | route: typst::engine::Route::default(), 55 | locator: &mut locator, 56 | tracer: tracer.track_mut(), 57 | }; 58 | 59 | let result = typst::eval::eval_string( 60 | world.track(), 61 | string, 62 | typst::syntax::Span::detached(), 63 | typst::eval::EvalMode::Math, 64 | world.library().math.scope().clone() 65 | ); 66 | 67 | match result { 68 | Ok(value) => match value { 69 | typst::foundations::Value::Content(content) => Ok(content), 70 | _ => Err("Expected content result.".to_string()), 71 | } 72 | Err(err) => Err(err[0].message.to_string()) 73 | } 74 | } 75 | 76 | pub fn insert_separator(list: &[T], separator: T) -> Vec { 77 | list.iter() 78 | .flat_map(|x| vec![x.clone(), separator.clone()]) 79 | .take(list.len() * 2 - 1) 80 | .collect() 81 | } 82 | -------------------------------------------------------------------------------- /lib/katex/src/functions/href.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction, {ordargument} from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import {assertNodeType} from "../parseNode"; 5 | import {MathNode} from "../mathMLTree"; 6 | 7 | import * as html from "../buildHTML"; 8 | import * as mml from "../buildMathML"; 9 | 10 | defineFunction({ 11 | type: "href", 12 | names: ["\\href"], 13 | props: { 14 | numArgs: 2, 15 | argTypes: ["url", "original"], 16 | allowedInText: true, 17 | }, 18 | handler: ({parser}, args) => { 19 | const body = args[1]; 20 | const href = assertNodeType(args[0], "url").url; 21 | 22 | if (!parser.settings.isTrusted({ 23 | command: "\\href", 24 | url: href, 25 | })) { 26 | return parser.formatUnsupportedCmd("\\href"); 27 | } 28 | 29 | return { 30 | type: "href", 31 | mode: parser.mode, 32 | href, 33 | body: ordargument(body), 34 | }; 35 | }, 36 | htmlBuilder: (group, options) => { 37 | const elements = html.buildExpression(group.body, options, false); 38 | return buildCommon.makeAnchor(group.href, [], elements, options); 39 | }, 40 | mathmlBuilder: (group, options) => { 41 | let math = mml.buildExpressionRow(group.body, options); 42 | if (!(math instanceof MathNode)) { 43 | math = new MathNode("mrow", [math]); 44 | } 45 | math.setAttribute("href", group.href); 46 | return math; 47 | }, 48 | }); 49 | 50 | defineFunction({ 51 | type: "href", 52 | names: ["\\url"], 53 | props: { 54 | numArgs: 1, 55 | argTypes: ["url"], 56 | allowedInText: true, 57 | }, 58 | handler: ({parser}, args) => { 59 | const href = assertNodeType(args[0], "url").url; 60 | 61 | if (!parser.settings.isTrusted({ 62 | command: "\\url", 63 | url: href, 64 | })) { 65 | return parser.formatUnsupportedCmd("\\url"); 66 | } 67 | 68 | const chars = []; 69 | for (let i = 0; i < href.length; i++) { 70 | let c = href[i]; 71 | if (c === "~") { 72 | c = "\\textasciitilde"; 73 | } 74 | chars.push({ 75 | type: "textord", 76 | mode: "text", 77 | text: c, 78 | }); 79 | } 80 | const body = { 81 | type: "text", 82 | mode: parser.mode, 83 | font: "\\texttt", 84 | body: chars, 85 | }; 86 | return { 87 | type: "href", 88 | mode: parser.mode, 89 | href, 90 | body: ordargument(body), 91 | }; 92 | }, 93 | }); 94 | -------------------------------------------------------------------------------- /lib/katex/src/functions/color.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction, {ordargument} from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | import {assertNodeType} from "../parseNode"; 6 | 7 | 8 | 9 | import * as html from "../buildHTML"; 10 | import * as mml from "../buildMathML"; 11 | 12 | const htmlBuilder = (group, options) => { 13 | const elements = html.buildExpression( 14 | group.body, 15 | options.withColor(group.color), 16 | false 17 | ); 18 | 19 | // \color isn't supposed to affect the type of the elements it contains. 20 | // To accomplish this, we wrap the results in a fragment, so the inner 21 | // elements will be able to directly interact with their neighbors. For 22 | // example, `\color{red}{2 +} 3` has the same spacing as `2 + 3` 23 | return buildCommon.makeFragment(elements); 24 | }; 25 | 26 | const mathmlBuilder = (group, options) => { 27 | const inner = mml.buildExpression(group.body, 28 | options.withColor(group.color)); 29 | 30 | const node = new mathMLTree.MathNode("mstyle", inner); 31 | 32 | node.setAttribute("mathcolor", group.color); 33 | 34 | return node; 35 | }; 36 | 37 | defineFunction({ 38 | type: "color", 39 | names: ["\\textcolor"], 40 | props: { 41 | numArgs: 2, 42 | allowedInText: true, 43 | argTypes: ["color", "original"], 44 | }, 45 | handler({parser}, args) { 46 | const color = assertNodeType(args[0], "color-token").color; 47 | const body = args[1]; 48 | return { 49 | type: "color", 50 | mode: parser.mode, 51 | color, 52 | body: (ordargument(body) ), 53 | }; 54 | }, 55 | htmlBuilder, 56 | mathmlBuilder, 57 | }); 58 | 59 | defineFunction({ 60 | type: "color", 61 | names: ["\\color"], 62 | props: { 63 | numArgs: 1, 64 | allowedInText: true, 65 | argTypes: ["color"], 66 | }, 67 | handler({parser, breakOnTokenText}, args) { 68 | const color = assertNodeType(args[0], "color-token").color; 69 | 70 | // Set macro \current@color in current namespace to store the current 71 | // color, mimicking the behavior of color.sty. 72 | // This is currently used just to correctly color a \right 73 | // that follows a \color command. 74 | parser.gullet.macros.set("\\current@color", color); 75 | 76 | // Parse out the implicit body that should be colored. 77 | const body = parser.parseExpression(true, breakOnTokenText); 78 | 79 | return { 80 | type: "color", 81 | mode: parser.mode, 82 | color, 83 | body, 84 | }; 85 | }, 86 | htmlBuilder, 87 | mathmlBuilder, 88 | }); 89 | -------------------------------------------------------------------------------- /lib/katex/src/spacingData.js: -------------------------------------------------------------------------------- 1 | // 2 | /** 3 | * Describes spaces between different classes of atoms. 4 | */ 5 | 6 | 7 | const thinspace = { 8 | number: 3, 9 | unit: "mu", 10 | }; 11 | const mediumspace = { 12 | number: 4, 13 | unit: "mu", 14 | }; 15 | const thickspace = { 16 | number: 5, 17 | unit: "mu", 18 | }; 19 | 20 | // Making the type below exact with all optional fields doesn't work due to 21 | // - https://github.com/facebook/flow/issues/4582 22 | // - https://github.com/facebook/flow/issues/5688 23 | // However, since *all* fields are optional, $Shape<> works as suggested in 5688 24 | // above. 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | // Spacing relationships for display and text styles 37 | export const spacings = { 38 | mord: { 39 | mop: thinspace, 40 | mbin: mediumspace, 41 | mrel: thickspace, 42 | minner: thinspace, 43 | }, 44 | mop: { 45 | mord: thinspace, 46 | mop: thinspace, 47 | mrel: thickspace, 48 | minner: thinspace, 49 | }, 50 | mbin: { 51 | mord: mediumspace, 52 | mop: mediumspace, 53 | mopen: mediumspace, 54 | minner: mediumspace, 55 | }, 56 | mrel: { 57 | mord: thickspace, 58 | mop: thickspace, 59 | mopen: thickspace, 60 | minner: thickspace, 61 | }, 62 | mopen: {}, 63 | mclose: { 64 | mop: thinspace, 65 | mbin: mediumspace, 66 | mrel: thickspace, 67 | minner: thinspace, 68 | }, 69 | mpunct: { 70 | mord: thinspace, 71 | mop: thinspace, 72 | mrel: thickspace, 73 | mopen: thinspace, 74 | mclose: thinspace, 75 | mpunct: thinspace, 76 | minner: thinspace, 77 | }, 78 | minner: { 79 | mord: thinspace, 80 | mop: thinspace, 81 | mbin: mediumspace, 82 | mrel: thickspace, 83 | mopen: thinspace, 84 | mpunct: thinspace, 85 | minner: thinspace, 86 | }, 87 | }; 88 | 89 | // Spacing relationships for script and scriptscript styles 90 | export const tightSpacings = { 91 | mord: { 92 | mop: thinspace, 93 | }, 94 | mop: { 95 | mord: thinspace, 96 | mop: thinspace, 97 | }, 98 | mbin: {}, 99 | mrel: {}, 100 | mopen: {}, 101 | mclose: { 102 | mop: thinspace, 103 | }, 104 | mpunct: {}, 105 | minner: { 106 | mop: thinspace, 107 | }, 108 | }; 109 | -------------------------------------------------------------------------------- /lib/katex/src/functions/lap.js: -------------------------------------------------------------------------------- 1 | // 2 | // Horizontal overlap functions 3 | import defineFunction from "../defineFunction"; 4 | import buildCommon from "../buildCommon"; 5 | import mathMLTree from "../mathMLTree"; 6 | import {makeEm} from "../units"; 7 | 8 | import * as html from "../buildHTML"; 9 | import * as mml from "../buildMathML"; 10 | 11 | defineFunction({ 12 | type: "lap", 13 | names: ["\\mathllap", "\\mathrlap", "\\mathclap"], 14 | props: { 15 | numArgs: 1, 16 | allowedInText: true, 17 | }, 18 | handler: ({parser, funcName}, args) => { 19 | const body = args[0]; 20 | return { 21 | type: "lap", 22 | mode: parser.mode, 23 | alignment: funcName.slice(5), 24 | body, 25 | }; 26 | }, 27 | htmlBuilder: (group, options) => { 28 | // mathllap, mathrlap, mathclap 29 | let inner; 30 | if (group.alignment === "clap") { 31 | // ref: https://www.math.lsu.edu/~aperlis/publications/mathclap/ 32 | inner = buildCommon.makeSpan( 33 | [], [html.buildGroup(group.body, options)]); 34 | // wrap, since CSS will center a .clap > .inner > span 35 | inner = buildCommon.makeSpan(["inner"], [inner], options); 36 | } else { 37 | inner = buildCommon.makeSpan( 38 | ["inner"], [html.buildGroup(group.body, options)]); 39 | } 40 | const fix = buildCommon.makeSpan(["fix"], []); 41 | let node = buildCommon.makeSpan( 42 | [group.alignment], [inner, fix], options); 43 | 44 | // At this point, we have correctly set horizontal alignment of the 45 | // two items involved in the lap. 46 | // Next, use a strut to set the height of the HTML bounding box. 47 | // Otherwise, a tall argument may be misplaced. 48 | // This code resolved issue #1153 49 | const strut = buildCommon.makeSpan(["strut"]); 50 | strut.style.height = makeEm(node.height + node.depth); 51 | if (node.depth) { 52 | strut.style.verticalAlign = makeEm(-node.depth); 53 | } 54 | node.children.unshift(strut); 55 | 56 | // Next, prevent vertical misplacement when next to something tall. 57 | // This code resolves issue #1234 58 | node = buildCommon.makeSpan(["thinbox"], [node], options); 59 | return buildCommon.makeSpan(["mord", "vbox"], [node], options); 60 | }, 61 | mathmlBuilder: (group, options) => { 62 | // mathllap, mathrlap, mathclap 63 | const node = new mathMLTree.MathNode( 64 | "mpadded", [mml.buildGroup(group.body, options)]); 65 | 66 | if (group.alignment !== "rlap") { 67 | const offset = (group.alignment === "llap" ? "-1" : "-0.5"); 68 | node.setAttribute("lspace", offset + "width"); 69 | } 70 | node.setAttribute("width", "0px"); 71 | 72 | return node; 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /lib/katex/src/functions/symbolsSpacing.js: -------------------------------------------------------------------------------- 1 | // 2 | import {defineFunctionBuilders} from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | import ParseError from "../ParseError"; 6 | 7 | // A map of CSS-based spacing functions to their CSS class. 8 | const cssSpace = { 9 | "\\nobreak": "nobreak", 10 | "\\allowbreak": "allowbreak", 11 | }; 12 | 13 | // A lookup table to determine whether a spacing function/symbol should be 14 | // treated like a regular space character. If a symbol or command is a key 15 | // in this table, then it should be a regular space character. Furthermore, 16 | // the associated value may have a `className` specifying an extra CSS class 17 | // to add to the created `span`. 18 | const regularSpace = { 19 | " ": {}, 20 | "\\ ": {}, 21 | "~": { 22 | className: "nobreak", 23 | }, 24 | "\\space": {}, 25 | "\\nobreakspace": { 26 | className: "nobreak", 27 | }, 28 | }; 29 | 30 | // ParseNode<"spacing"> created in Parser.js from the "spacing" symbol Groups in 31 | // src/symbols.js. 32 | defineFunctionBuilders({ 33 | type: "spacing", 34 | htmlBuilder(group, options) { 35 | if (regularSpace.hasOwnProperty(group.text)) { 36 | const className = regularSpace[group.text].className || ""; 37 | // Spaces are generated by adding an actual space. Each of these 38 | // things has an entry in the symbols table, so these will be turned 39 | // into appropriate outputs. 40 | if (group.mode === "text") { 41 | const ord = buildCommon.makeOrd(group, options, "textord"); 42 | ord.classes.push(className); 43 | return ord; 44 | } else { 45 | return buildCommon.makeSpan(["mspace", className], 46 | [buildCommon.mathsym(group.text, group.mode, options)], 47 | options); 48 | } 49 | } else if (cssSpace.hasOwnProperty(group.text)) { 50 | // Spaces based on just a CSS class. 51 | return buildCommon.makeSpan( 52 | ["mspace", cssSpace[group.text]], 53 | [], options); 54 | } else { 55 | throw new ParseError(`Unknown type of space "${group.text}"`); 56 | } 57 | }, 58 | mathmlBuilder(group, options) { 59 | let node; 60 | 61 | if (regularSpace.hasOwnProperty(group.text)) { 62 | node = new mathMLTree.MathNode( 63 | "mtext", [new mathMLTree.TextNode("\u00a0")]); 64 | } else if (cssSpace.hasOwnProperty(group.text)) { 65 | // CSS-based MathML spaces (\nobreak, \allowbreak) are ignored 66 | return new mathMLTree.MathNode("mspace"); 67 | } else { 68 | throw new ParseError(`Unknown type of space "${group.text}"`); 69 | } 70 | 71 | return node; 72 | }, 73 | }); 74 | -------------------------------------------------------------------------------- /lib/katex/src/functions/rule.js: -------------------------------------------------------------------------------- 1 | // 2 | import buildCommon from "../buildCommon"; 3 | import defineFunction from "../defineFunction"; 4 | import mathMLTree from "../mathMLTree"; 5 | import {assertNodeType} from "../parseNode"; 6 | import {calculateSize, makeEm} from "../units"; 7 | 8 | defineFunction({ 9 | type: "rule", 10 | names: ["\\rule"], 11 | props: { 12 | numArgs: 2, 13 | numOptionalArgs: 1, 14 | argTypes: ["size", "size", "size"], 15 | }, 16 | handler({parser}, args, optArgs) { 17 | const shift = optArgs[0]; 18 | const width = assertNodeType(args[0], "size"); 19 | const height = assertNodeType(args[1], "size"); 20 | return { 21 | type: "rule", 22 | mode: parser.mode, 23 | shift: shift && assertNodeType(shift, "size").value, 24 | width: width.value, 25 | height: height.value, 26 | }; 27 | }, 28 | htmlBuilder(group, options) { 29 | // Make an empty span for the rule 30 | const rule = buildCommon.makeSpan(["mord", "rule"], [], options); 31 | 32 | // Calculate the shift, width, and height of the rule, and account for units 33 | const width = calculateSize(group.width, options); 34 | const height = calculateSize(group.height, options); 35 | const shift = (group.shift) ? calculateSize(group.shift, options) : 0; 36 | 37 | // Style the rule to the right size 38 | rule.style.borderRightWidth = makeEm(width); 39 | rule.style.borderTopWidth = makeEm(height); 40 | rule.style.bottom = makeEm(shift); 41 | 42 | // Record the height and width 43 | rule.width = width; 44 | rule.height = height + shift; 45 | rule.depth = -shift; 46 | // Font size is the number large enough that the browser will 47 | // reserve at least `absHeight` space above the baseline. 48 | // The 1.125 factor was empirically determined 49 | rule.maxFontSize = height * 1.125 * options.sizeMultiplier; 50 | 51 | return rule; 52 | }, 53 | mathmlBuilder(group, options) { 54 | const width = calculateSize(group.width, options); 55 | const height = calculateSize(group.height, options); 56 | const shift = (group.shift) ? calculateSize(group.shift, options) : 0; 57 | const color = options.color && options.getColor() || "black"; 58 | 59 | const rule = new mathMLTree.MathNode("mspace"); 60 | rule.setAttribute("mathbackground", color); 61 | rule.setAttribute("width", makeEm(width)); 62 | rule.setAttribute("height", makeEm(height)); 63 | 64 | const wrapper = new mathMLTree.MathNode("mpadded", [rule]); 65 | if (shift >= 0) { 66 | wrapper.setAttribute("height", makeEm(shift)); 67 | } else { 68 | wrapper.setAttribute("height", makeEm(shift)); 69 | wrapper.setAttribute("depth", makeEm(-shift)); 70 | } 71 | wrapper.setAttribute("voffset", makeEm(shift)); 72 | 73 | return wrapper; 74 | }, 75 | }); 76 | -------------------------------------------------------------------------------- /lib/katex/src/ParseError.js: -------------------------------------------------------------------------------- 1 | // 2 | import {Token} from "./Token"; 3 | 4 | 5 | 6 | /** 7 | * This is the ParseError class, which is the main error thrown by KaTeX 8 | * functions when something has gone wrong. This is used to distinguish internal 9 | * errors from errors in the expression that the user provided. 10 | * 11 | * If possible, a caller should provide a Token or ParseNode with information 12 | * about where in the source string the problem occurred. 13 | */ 14 | class ParseError { 15 | name ; 16 | position ; 17 | // Error start position based on passed-in Token or ParseNode. 18 | length ; 19 | // Length of affected text based on passed-in Token or ParseNode. 20 | rawMessage ; 21 | // The underlying error message without any context added. 22 | 23 | constructor( 24 | message , // The error message 25 | token , // An object providing position information 26 | ) { 27 | let error = "KaTeX parse error: " + message; 28 | let start; 29 | let end; 30 | 31 | const loc = token && token.loc; 32 | if (loc && loc.start <= loc.end) { 33 | // If we have the input and a position, make the error a bit fancier 34 | 35 | // Get the input 36 | const input = loc.lexer.input; 37 | 38 | // Prepend some information 39 | start = loc.start; 40 | end = loc.end; 41 | if (start === input.length) { 42 | error += " at end of input: "; 43 | } else { 44 | error += " at position " + (start + 1) + ": "; 45 | } 46 | 47 | // Underline token in question using combining underscores 48 | const underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332"); 49 | 50 | // Extract some context from the input and add it to the error 51 | let left; 52 | if (start > 15) { 53 | left = "…" + input.slice(start - 15, start); 54 | } else { 55 | left = input.slice(0, start); 56 | } 57 | let right; 58 | if (end + 15 < input.length) { 59 | right = input.slice(end, end + 15) + "…"; 60 | } else { 61 | right = input.slice(end); 62 | } 63 | error += left + underlined + right; 64 | 65 | } 66 | 67 | // Some hackery to make ParseError a prototype of Error 68 | // See http://stackoverflow.com/a/8460753 69 | // $FlowFixMe 70 | const self = new Error(error); 71 | self.name = "ParseError"; 72 | // $FlowFixMe 73 | self.__proto__ = ParseError.prototype; 74 | self.position = start; 75 | if (start != null && end != null) { 76 | self.length = end - start; 77 | } 78 | self.rawMessage = message; 79 | return self; 80 | } 81 | } 82 | 83 | // $FlowFixMe More hackery 84 | ParseError.prototype.__proto__ = Error.prototype; 85 | 86 | export default ParseError; 87 | -------------------------------------------------------------------------------- /lib/katex/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // Simple CLI for KaTeX. 3 | // Reads TeX from stdin, outputs HTML to stdout. 4 | // To run this from the repository, you must first build KaTeX by running 5 | // `yarn` and `yarn build`. 6 | 7 | /* eslint no-console:0 */ 8 | 9 | let katex; 10 | try { 11 | katex = require("./"); 12 | } catch (e) { 13 | console.error( 14 | "KaTeX could not import, likely because dist/katex.js is missing."); 15 | console.error("Please run 'yarn' and 'yarn build' before running"); 16 | console.error("cli.js from the KaTeX repository."); 17 | console.error(); 18 | throw e; 19 | } 20 | const {version} = require("./package.json"); 21 | const fs = require("fs"); 22 | 23 | const program = require("commander").version(version); 24 | for (const prop in katex.SETTINGS_SCHEMA) { 25 | if (katex.SETTINGS_SCHEMA.hasOwnProperty(prop)) { 26 | const opt = katex.SETTINGS_SCHEMA[prop]; 27 | if (opt.cli !== false) { 28 | program.option(opt.cli || "--" + prop, opt.cliDescription || 29 | opt.description, opt.cliProcessor, opt.cliDefault); 30 | } 31 | } 32 | } 33 | program.option("-f, --macro-file ", 34 | "Read macro definitions, one per line, from the given file.") 35 | .option("-i, --input ", "Read LaTeX input from the given file.") 36 | .option("-o, --output ", "Write html output to the given file."); 37 | 38 | let options; 39 | 40 | function readMacros() { 41 | if (options.macroFile) { 42 | fs.readFile(options.macroFile, "utf-8", function(err, data) { 43 | if (err) {throw err;} 44 | splitMacros(data.toString().split('\n')); 45 | }); 46 | } else { 47 | splitMacros([]); 48 | } 49 | } 50 | 51 | function splitMacros(macroStrings) { 52 | // Override macros from macro file (if any) 53 | // with macros from command line (if any) 54 | macroStrings = macroStrings.concat(options.macro); 55 | 56 | const macros = {}; 57 | 58 | for (const m of macroStrings) { 59 | const i = m.search(":"); 60 | if (i !== -1) { 61 | macros[m.substring(0, i).trim()] = m.substring(i + 1).trim(); 62 | } 63 | } 64 | 65 | options.macros = macros; 66 | readInput(); 67 | } 68 | 69 | function readInput() { 70 | let input = ""; 71 | 72 | if (options.input) { 73 | fs.readFile(options.input, "utf-8", function(err, data) { 74 | if (err) {throw err;} 75 | input = data.toString(); 76 | writeOutput(input); 77 | }); 78 | } else { 79 | process.stdin.on("data", function(chunk) { 80 | input += chunk.toString(); 81 | }); 82 | 83 | process.stdin.on("end", function() { 84 | writeOutput(input); 85 | }); 86 | } 87 | } 88 | 89 | function writeOutput(input) { 90 | // --format specifies the KaTeX output 91 | const outputFile = options.output; 92 | options.output = options.format; 93 | 94 | const output = katex.renderToString(input, options) + "\n"; 95 | 96 | if (outputFile) { 97 | fs.writeFile(outputFile, output, function(err) { 98 | if (err) { 99 | return console.log(err); 100 | } 101 | }); 102 | } else { 103 | console.log(output); 104 | } 105 | } 106 | 107 | if (require.main !== module) { 108 | module.exports = program; 109 | } else { 110 | options = program.parse(process.argv).opts(); 111 | readMacros(); 112 | } 113 | -------------------------------------------------------------------------------- /lib/katex/src/functions/sizing.js: -------------------------------------------------------------------------------- 1 | // 2 | import buildCommon from "../buildCommon"; 3 | import defineFunction from "../defineFunction"; 4 | import mathMLTree from "../mathMLTree"; 5 | import {makeEm} from "../units"; 6 | 7 | import * as html from "../buildHTML"; 8 | import * as mml from "../buildMathML"; 9 | 10 | 11 | 12 | 13 | 14 | 15 | export function sizingGroup( 16 | value , 17 | options , 18 | baseOptions , 19 | ) { 20 | const inner = html.buildExpression(value, options, false); 21 | const multiplier = options.sizeMultiplier / baseOptions.sizeMultiplier; 22 | 23 | // Add size-resetting classes to the inner list and set maxFontSize 24 | // manually. Handle nested size changes. 25 | for (let i = 0; i < inner.length; i++) { 26 | const pos = inner[i].classes.indexOf("sizing"); 27 | if (pos < 0) { 28 | Array.prototype.push.apply(inner[i].classes, 29 | options.sizingClasses(baseOptions)); 30 | } else if (inner[i].classes[pos + 1] === "reset-size" + options.size) { 31 | // This is a nested size change: e.g., inner[i] is the "b" in 32 | // `\Huge a \small b`. Override the old size (the `reset-` class) 33 | // but not the new size. 34 | inner[i].classes[pos + 1] = "reset-size" + baseOptions.size; 35 | } 36 | 37 | inner[i].height *= multiplier; 38 | inner[i].depth *= multiplier; 39 | } 40 | 41 | return buildCommon.makeFragment(inner); 42 | } 43 | 44 | const sizeFuncs = [ 45 | "\\tiny", "\\sixptsize", "\\scriptsize", "\\footnotesize", "\\small", 46 | "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge", 47 | ]; 48 | 49 | export const htmlBuilder = (group, options) => { 50 | // Handle sizing operators like \Huge. Real TeX doesn't actually allow 51 | // these functions inside of math expressions, so we do some special 52 | // handling. 53 | const newOptions = options.havingSize(group.size); 54 | return sizingGroup(group.body, newOptions, options); 55 | }; 56 | 57 | defineFunction({ 58 | type: "sizing", 59 | names: sizeFuncs, 60 | props: { 61 | numArgs: 0, 62 | allowedInText: true, 63 | }, 64 | handler: ({breakOnTokenText, funcName, parser}, args) => { 65 | const body = parser.parseExpression(false, breakOnTokenText); 66 | 67 | return { 68 | type: "sizing", 69 | mode: parser.mode, 70 | // Figure out what size to use based on the list of functions above 71 | size: sizeFuncs.indexOf(funcName) + 1, 72 | body, 73 | }; 74 | }, 75 | htmlBuilder, 76 | mathmlBuilder: (group, options) => { 77 | const newOptions = options.havingSize(group.size); 78 | const inner = mml.buildExpression(group.body, newOptions); 79 | 80 | const node = new mathMLTree.MathNode("mstyle", inner); 81 | 82 | // TODO(emily): This doesn't produce the correct size for nested size 83 | // changes, because we don't keep state of what style we're currently 84 | // in, so we can't reset the size to normal before changing it. Now 85 | // that we're passing an options parameter we should be able to fix 86 | // this. 87 | node.setAttribute("mathsize", makeEm(newOptions.sizeMultiplier)); 88 | 89 | return node; 90 | }, 91 | }); 92 | -------------------------------------------------------------------------------- /lib/katex/src/functions/font.js: -------------------------------------------------------------------------------- 1 | // 2 | // TODO(kevinb): implement \\sl and \\sc 3 | 4 | import {binrelClass} from "./mclass"; 5 | import defineFunction, {normalizeArgument} from "../defineFunction"; 6 | import utils from "../utils"; 7 | 8 | import * as html from "../buildHTML"; 9 | import * as mml from "../buildMathML"; 10 | 11 | 12 | 13 | const htmlBuilder = (group , options) => { 14 | const font = group.font; 15 | const newOptions = options.withFont(font); 16 | return html.buildGroup(group.body, newOptions); 17 | }; 18 | 19 | const mathmlBuilder = (group , options) => { 20 | const font = group.font; 21 | const newOptions = options.withFont(font); 22 | return mml.buildGroup(group.body, newOptions); 23 | }; 24 | 25 | const fontAliases = { 26 | "\\Bbb": "\\mathbb", 27 | "\\bold": "\\mathbf", 28 | "\\frak": "\\mathfrak", 29 | "\\bm": "\\boldsymbol", 30 | }; 31 | 32 | defineFunction({ 33 | type: "font", 34 | names: [ 35 | // styles, except \boldsymbol defined below 36 | "\\mathrm", "\\mathit", "\\mathbf", "\\mathnormal", 37 | 38 | // families 39 | "\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf", 40 | "\\mathtt", 41 | 42 | // aliases, except \bm defined below 43 | "\\Bbb", "\\bold", "\\frak", 44 | ], 45 | props: { 46 | numArgs: 1, 47 | allowedInArgument: true, 48 | }, 49 | handler: ({parser, funcName}, args) => { 50 | const body = normalizeArgument(args[0]); 51 | let func = funcName; 52 | if (func in fontAliases) { 53 | func = fontAliases[func]; 54 | } 55 | return { 56 | type: "font", 57 | mode: parser.mode, 58 | font: func.slice(1), 59 | body, 60 | }; 61 | }, 62 | htmlBuilder, 63 | mathmlBuilder, 64 | }); 65 | 66 | defineFunction({ 67 | type: "mclass", 68 | names: ["\\boldsymbol", "\\bm"], 69 | props: { 70 | numArgs: 1, 71 | }, 72 | handler: ({parser}, args) => { 73 | const body = args[0]; 74 | const isCharacterBox = utils.isCharacterBox(body); 75 | // amsbsy.sty's \boldsymbol uses \binrel spacing to inherit the 76 | // argument's bin|rel|ord status 77 | return { 78 | type: "mclass", 79 | mode: parser.mode, 80 | mclass: binrelClass(body), 81 | body: [ 82 | { 83 | type: "font", 84 | mode: parser.mode, 85 | font: "boldsymbol", 86 | body, 87 | }, 88 | ], 89 | isCharacterBox: isCharacterBox, 90 | }; 91 | }, 92 | }); 93 | 94 | // Old font changing functions 95 | defineFunction({ 96 | type: "font", 97 | names: ["\\rm", "\\sf", "\\tt", "\\bf", "\\it", "\\cal"], 98 | props: { 99 | numArgs: 0, 100 | allowedInText: true, 101 | }, 102 | handler: ({parser, funcName, breakOnTokenText}, args) => { 103 | const {mode} = parser; 104 | const body = parser.parseExpression(true, breakOnTokenText); 105 | const style = `math${funcName.slice(1)}`; 106 | 107 | return { 108 | type: "font", 109 | mode: mode, 110 | font: style, 111 | body: { 112 | type: "ordgroup", 113 | mode: parser.mode, 114 | body, 115 | }, 116 | }; 117 | }, 118 | htmlBuilder, 119 | mathmlBuilder, 120 | }); 121 | -------------------------------------------------------------------------------- /lib/katex/src/functions/html.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction, {ordargument} from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import {assertNodeType} from "../parseNode"; 5 | import ParseError from "../ParseError"; 6 | 7 | import * as html from "../buildHTML"; 8 | import * as mml from "../buildMathML"; 9 | 10 | defineFunction({ 11 | type: "html", 12 | names: ["\\htmlClass", "\\htmlId", "\\htmlStyle", "\\htmlData"], 13 | props: { 14 | numArgs: 2, 15 | argTypes: ["raw", "original"], 16 | allowedInText: true, 17 | }, 18 | handler: ({parser, funcName, token}, args) => { 19 | const value = assertNodeType(args[0], "raw").string; 20 | const body = args[1]; 21 | 22 | if (parser.settings.strict) { 23 | parser.settings.reportNonstrict("htmlExtension", 24 | "HTML extension is disabled on strict mode"); 25 | } 26 | 27 | let trustContext; 28 | const attributes = {}; 29 | 30 | switch (funcName) { 31 | case "\\htmlClass": 32 | attributes.class = value; 33 | trustContext = { 34 | command: "\\htmlClass", 35 | class: value, 36 | }; 37 | break; 38 | case "\\htmlId": 39 | attributes.id = value; 40 | trustContext = { 41 | command: "\\htmlId", 42 | id: value, 43 | }; 44 | break; 45 | case "\\htmlStyle": 46 | attributes.style = value; 47 | trustContext = { 48 | command: "\\htmlStyle", 49 | style: value, 50 | }; 51 | break; 52 | case "\\htmlData": { 53 | const data = value.split(","); 54 | for (let i = 0; i < data.length; i++) { 55 | const keyVal = data[i].split("="); 56 | if (keyVal.length !== 2) { 57 | throw new ParseError( 58 | "Error parsing key-value for \\htmlData"); 59 | } 60 | attributes["data-" + keyVal[0].trim()] = keyVal[1].trim(); 61 | } 62 | 63 | trustContext = { 64 | command: "\\htmlData", 65 | attributes, 66 | }; 67 | break; 68 | } 69 | default: 70 | throw new Error("Unrecognized html command"); 71 | } 72 | 73 | if (!parser.settings.isTrusted(trustContext)) { 74 | return parser.formatUnsupportedCmd(funcName); 75 | } 76 | return { 77 | type: "html", 78 | mode: parser.mode, 79 | attributes, 80 | body: ordargument(body), 81 | }; 82 | }, 83 | htmlBuilder: (group, options) => { 84 | const elements = html.buildExpression(group.body, options, false); 85 | 86 | const classes = ["enclosing"]; 87 | if (group.attributes.class) { 88 | classes.push(...group.attributes.class.trim().split(/\s+/)); 89 | } 90 | 91 | const span = buildCommon.makeSpan(classes, elements, options); 92 | for (const attr in group.attributes) { 93 | if (attr !== "class" && group.attributes.hasOwnProperty(attr)) { 94 | span.setAttribute(attr, group.attributes[attr]); 95 | } 96 | } 97 | return span; 98 | }, 99 | mathmlBuilder: (group, options) => { 100 | return mml.buildExpressionRow(group.body, options); 101 | }, 102 | }); 103 | -------------------------------------------------------------------------------- /lib/katex/src/Style.js: -------------------------------------------------------------------------------- 1 | // 2 | /** 3 | * This file contains information and classes for the various kinds of styles 4 | * used in TeX. It provides a generic `Style` class, which holds information 5 | * about a specific style. It then provides instances of all the different kinds 6 | * of styles possible, and provides functions to move between them and get 7 | * information about them. 8 | */ 9 | 10 | /** 11 | * The main style class. Contains a unique id for the style, a size (which is 12 | * the same for cramped and uncramped version of a style), and a cramped flag. 13 | */ 14 | class Style { 15 | id ; 16 | size ; 17 | cramped ; 18 | 19 | constructor(id , size , cramped ) { 20 | this.id = id; 21 | this.size = size; 22 | this.cramped = cramped; 23 | } 24 | 25 | /** 26 | * Get the style of a superscript given a base in the current style. 27 | */ 28 | sup() { 29 | return styles[sup[this.id]]; 30 | } 31 | 32 | /** 33 | * Get the style of a subscript given a base in the current style. 34 | */ 35 | sub() { 36 | return styles[sub[this.id]]; 37 | } 38 | 39 | /** 40 | * Get the style of a fraction numerator given the fraction in the current 41 | * style. 42 | */ 43 | fracNum() { 44 | return styles[fracNum[this.id]]; 45 | } 46 | 47 | /** 48 | * Get the style of a fraction denominator given the fraction in the current 49 | * style. 50 | */ 51 | fracDen() { 52 | return styles[fracDen[this.id]]; 53 | } 54 | 55 | /** 56 | * Get the cramped version of a style (in particular, cramping a cramped style 57 | * doesn't change the style). 58 | */ 59 | cramp() { 60 | return styles[cramp[this.id]]; 61 | } 62 | 63 | /** 64 | * Get a text or display version of this style. 65 | */ 66 | text() { 67 | return styles[text[this.id]]; 68 | } 69 | 70 | /** 71 | * Return true if this style is tightly spaced (scriptstyle/scriptscriptstyle) 72 | */ 73 | isTight() { 74 | return this.size >= 2; 75 | } 76 | } 77 | 78 | // Export an interface for type checking, but don't expose the implementation. 79 | // This way, no more styles can be generated. 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | // IDs of the different styles 95 | const D = 0; 96 | const Dc = 1; 97 | const T = 2; 98 | const Tc = 3; 99 | const S = 4; 100 | const Sc = 5; 101 | const SS = 6; 102 | const SSc = 7; 103 | 104 | // Instances of the different styles 105 | const styles = [ 106 | new Style(D, 0, false), 107 | new Style(Dc, 0, true), 108 | new Style(T, 1, false), 109 | new Style(Tc, 1, true), 110 | new Style(S, 2, false), 111 | new Style(Sc, 2, true), 112 | new Style(SS, 3, false), 113 | new Style(SSc, 3, true), 114 | ]; 115 | 116 | // Lookup tables for switching from one style to another 117 | const sup = [S, Sc, S, Sc, SS, SSc, SS, SSc]; 118 | const sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc]; 119 | const fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc]; 120 | const fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc]; 121 | const cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc]; 122 | const text = [D, Dc, T, Tc, T, Tc, T, Tc]; 123 | 124 | // We only export some of the styles. 125 | export default { 126 | DISPLAY: (styles[D] ), 127 | TEXT: (styles[T] ), 128 | SCRIPT: (styles[S] ), 129 | SCRIPTSCRIPT: (styles[SS] ), 130 | }; 131 | -------------------------------------------------------------------------------- /lib/katex/src/functions/smash.js: -------------------------------------------------------------------------------- 1 | // 2 | // smash, with optional [tb], as in AMS 3 | import defineFunction from "../defineFunction"; 4 | import buildCommon from "../buildCommon"; 5 | import mathMLTree from "../mathMLTree"; 6 | import {assertNodeType} from "../parseNode"; 7 | 8 | import * as html from "../buildHTML"; 9 | import * as mml from "../buildMathML"; 10 | 11 | defineFunction({ 12 | type: "smash", 13 | names: ["\\smash"], 14 | props: { 15 | numArgs: 1, 16 | numOptionalArgs: 1, 17 | allowedInText: true, 18 | }, 19 | handler: ({parser}, args, optArgs) => { 20 | let smashHeight = false; 21 | let smashDepth = false; 22 | const tbArg = optArgs[0] && assertNodeType(optArgs[0], "ordgroup"); 23 | if (tbArg) { 24 | // Optional [tb] argument is engaged. 25 | // ref: amsmath: \renewcommand{\smash}[1][tb]{% 26 | // def\mb@t{\ht}\def\mb@b{\dp}\def\mb@tb{\ht\z@\z@\dp}% 27 | let letter = ""; 28 | for (let i = 0; i < tbArg.body.length; ++i) { 29 | const node = tbArg.body[i]; 30 | // $FlowFixMe: Not every node type has a `text` property. 31 | letter = node.text; 32 | if (letter === "t") { 33 | smashHeight = true; 34 | } else if (letter === "b") { 35 | smashDepth = true; 36 | } else { 37 | smashHeight = false; 38 | smashDepth = false; 39 | break; 40 | } 41 | } 42 | } else { 43 | smashHeight = true; 44 | smashDepth = true; 45 | } 46 | 47 | const body = args[0]; 48 | return { 49 | type: "smash", 50 | mode: parser.mode, 51 | body, 52 | smashHeight, 53 | smashDepth, 54 | }; 55 | }, 56 | htmlBuilder: (group, options) => { 57 | const node = buildCommon.makeSpan( 58 | [], [html.buildGroup(group.body, options)]); 59 | 60 | if (!group.smashHeight && !group.smashDepth) { 61 | return node; 62 | } 63 | 64 | if (group.smashHeight) { 65 | node.height = 0; 66 | // In order to influence makeVList, we have to reset the children. 67 | if (node.children) { 68 | for (let i = 0; i < node.children.length; i++) { 69 | node.children[i].height = 0; 70 | } 71 | } 72 | } 73 | 74 | if (group.smashDepth) { 75 | node.depth = 0; 76 | if (node.children) { 77 | for (let i = 0; i < node.children.length; i++) { 78 | node.children[i].depth = 0; 79 | } 80 | } 81 | } 82 | 83 | // At this point, we've reset the TeX-like height and depth values. 84 | // But the span still has an HTML line height. 85 | // makeVList applies "display: table-cell", which prevents the browser 86 | // from acting on that line height. So we'll call makeVList now. 87 | 88 | const smashedNode = buildCommon.makeVList({ 89 | positionType: "firstBaseline", 90 | children: [{type: "elem", elem: node}], 91 | }, options); 92 | 93 | // For spacing, TeX treats \hphantom as a math group (same spacing as ord). 94 | return buildCommon.makeSpan(["mord"], [smashedNode], options); 95 | }, 96 | mathmlBuilder: (group, options) => { 97 | const node = new mathMLTree.MathNode( 98 | "mpadded", [mml.buildGroup(group.body, options)]); 99 | 100 | if (group.smashHeight) { 101 | node.setAttribute("height", "0px"); 102 | } 103 | 104 | if (group.smashDepth) { 105 | node.setAttribute("depth", "0px"); 106 | } 107 | 108 | return node; 109 | }, 110 | }); 111 | -------------------------------------------------------------------------------- /lib/katex/src/functions/phantom.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction, {ordargument} from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | 6 | import * as html from "../buildHTML"; 7 | import * as mml from "../buildMathML"; 8 | 9 | defineFunction({ 10 | type: "phantom", 11 | names: ["\\phantom"], 12 | props: { 13 | numArgs: 1, 14 | allowedInText: true, 15 | }, 16 | handler: ({parser}, args) => { 17 | const body = args[0]; 18 | return { 19 | type: "phantom", 20 | mode: parser.mode, 21 | body: ordargument(body), 22 | }; 23 | }, 24 | htmlBuilder: (group, options) => { 25 | const elements = html.buildExpression( 26 | group.body, 27 | options.withPhantom(), 28 | false 29 | ); 30 | 31 | // \phantom isn't supposed to affect the elements it contains. 32 | // See "color" for more details. 33 | return buildCommon.makeFragment(elements); 34 | }, 35 | mathmlBuilder: (group, options) => { 36 | const inner = mml.buildExpression(group.body, options); 37 | return new mathMLTree.MathNode("mphantom", inner); 38 | }, 39 | }); 40 | 41 | defineFunction({ 42 | type: "hphantom", 43 | names: ["\\hphantom"], 44 | props: { 45 | numArgs: 1, 46 | allowedInText: true, 47 | }, 48 | handler: ({parser}, args) => { 49 | const body = args[0]; 50 | return { 51 | type: "hphantom", 52 | mode: parser.mode, 53 | body, 54 | }; 55 | }, 56 | htmlBuilder: (group, options) => { 57 | let node = buildCommon.makeSpan( 58 | [], [html.buildGroup(group.body, options.withPhantom())]); 59 | node.height = 0; 60 | node.depth = 0; 61 | if (node.children) { 62 | for (let i = 0; i < node.children.length; i++) { 63 | node.children[i].height = 0; 64 | node.children[i].depth = 0; 65 | } 66 | } 67 | 68 | // See smash for comment re: use of makeVList 69 | node = buildCommon.makeVList({ 70 | positionType: "firstBaseline", 71 | children: [{type: "elem", elem: node}], 72 | }, options); 73 | 74 | // For spacing, TeX treats \smash as a math group (same spacing as ord). 75 | return buildCommon.makeSpan(["mord"], [node], options); 76 | }, 77 | mathmlBuilder: (group, options) => { 78 | const inner = mml.buildExpression(ordargument(group.body), options); 79 | const phantom = new mathMLTree.MathNode("mphantom", inner); 80 | const node = new mathMLTree.MathNode("mpadded", [phantom]); 81 | node.setAttribute("height", "0px"); 82 | node.setAttribute("depth", "0px"); 83 | return node; 84 | }, 85 | }); 86 | 87 | defineFunction({ 88 | type: "vphantom", 89 | names: ["\\vphantom"], 90 | props: { 91 | numArgs: 1, 92 | allowedInText: true, 93 | }, 94 | handler: ({parser}, args) => { 95 | const body = args[0]; 96 | return { 97 | type: "vphantom", 98 | mode: parser.mode, 99 | body, 100 | }; 101 | }, 102 | htmlBuilder: (group, options) => { 103 | const inner = buildCommon.makeSpan( 104 | ["inner"], 105 | [html.buildGroup(group.body, options.withPhantom())]); 106 | const fix = buildCommon.makeSpan(["fix"], []); 107 | return buildCommon.makeSpan( 108 | ["mord", "rlap"], [inner, fix], options); 109 | }, 110 | mathmlBuilder: (group, options) => { 111 | const inner = mml.buildExpression(ordargument(group.body), options); 112 | const phantom = new mathMLTree.MathNode("mphantom", inner); 113 | const node = new mathMLTree.MathNode("mpadded", [phantom]); 114 | node.setAttribute("width", "0px"); 115 | return node; 116 | }, 117 | }); 118 | -------------------------------------------------------------------------------- /lib/katex/src/defineEnvironment.js: -------------------------------------------------------------------------------- 1 | // 2 | import {_htmlGroupBuilders, _mathmlGroupBuilders} from "./defineFunction"; 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | /** 11 | * The context contains the following properties: 12 | * - mode: current parsing mode. 13 | * - envName: the name of the environment, one of the listed names. 14 | * - parser: the parser object. 15 | */ 16 | 17 | 18 | 19 | 20 | 21 | 22 | /** 23 | * - context: information and references provided by the parser 24 | * - args: an array of arguments passed to \begin{name} 25 | * - optArgs: an array of optional arguments passed to \begin{name} 26 | */ 27 | 28 | 29 | 30 | 31 | 32 | 33 | /** 34 | * - numArgs: (default 0) The number of arguments after the \begin{name} function. 35 | * - argTypes: (optional) Just like for a function 36 | * - allowedInText: (default false) Whether or not the environment is allowed 37 | * inside text mode (not enforced yet). 38 | * - numOptionalArgs: (default 0) Just like for a function 39 | */ 40 | 41 | 42 | 43 | 44 | /** 45 | * Final environment spec for use at parse time. 46 | * This is almost identical to `EnvDefSpec`, except it 47 | * 1. includes the function handler 48 | * 2. requires all arguments except argType 49 | * It is generated by `defineEnvironment()` below. 50 | */ 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | /** 61 | * All registered environments. 62 | * `environments.js` exports this same dictionary again and makes it public. 63 | * `Parser.js` requires this dictionary via `environments.js`. 64 | */ 65 | export const _environments = {}; 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | export default function defineEnvironment ({ 90 | type, 91 | names, 92 | props, 93 | handler, 94 | htmlBuilder, 95 | mathmlBuilder, 96 | } ) { 97 | // Set default values of environments. 98 | const data = { 99 | type, 100 | numArgs: props.numArgs || 0, 101 | allowedInText: false, 102 | numOptionalArgs: 0, 103 | handler, 104 | }; 105 | for (let i = 0; i < names.length; ++i) { 106 | // TODO: The value type of _environments should be a type union of all 107 | // possible `EnvSpec<>` possibilities instead of `EnvSpec<*>`, which is 108 | // an existential type. 109 | _environments[names[i]] = data; 110 | } 111 | if (htmlBuilder) { 112 | _htmlGroupBuilders[type] = htmlBuilder; 113 | } 114 | if (mathmlBuilder) { 115 | _mathmlGroupBuilders[type] = mathmlBuilder; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /lib/katex/src/defineMacro.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | import {Token} from "./Token"; 4 | 5 | 6 | 7 | /** 8 | * Provides context to macros defined by functions. Implemented by 9 | * MacroExpander. 10 | */ 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | /** Macro tokens (in reverse order). */ 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | /** 116 | * All registered global/built-in macros. 117 | * `macros.js` exports this same dictionary again and makes it public. 118 | * `Parser.js` requires this dictionary via `macros.js`. 119 | */ 120 | export const _macros = {}; 121 | 122 | // This function might one day accept an additional argument and do more things. 123 | export default function defineMacro(name , body ) { 124 | _macros[name] = body; 125 | } 126 | -------------------------------------------------------------------------------- /lib/katex/src/utils.js: -------------------------------------------------------------------------------- 1 | // 2 | /** 3 | * This file contains a list of utility functions which are useful in other 4 | * files. 5 | */ 6 | 7 | 8 | 9 | /** 10 | * Return whether an element is contained in a list 11 | */ 12 | const contains = function (list , elem ) { 13 | return list.indexOf(elem) !== -1; 14 | }; 15 | 16 | /** 17 | * Provide a default value if a setting is undefined 18 | * NOTE: Couldn't use `T` as the output type due to facebook/flow#5022. 19 | */ 20 | const deflt = function (setting , defaultIfUndefined ) { 21 | return setting === undefined ? defaultIfUndefined : setting; 22 | }; 23 | 24 | // hyphenate and escape adapted from Facebook's React under Apache 2 license 25 | 26 | const uppercase = /([A-Z])/g; 27 | const hyphenate = function(str ) { 28 | return str.replace(uppercase, "-$1").toLowerCase(); 29 | }; 30 | 31 | const ESCAPE_LOOKUP = { 32 | "&": "&", 33 | ">": ">", 34 | "<": "<", 35 | "\"": """, 36 | "'": "'", 37 | }; 38 | 39 | const ESCAPE_REGEX = /[&><"']/g; 40 | 41 | /** 42 | * Escapes text to prevent scripting attacks. 43 | */ 44 | function escape(text ) { 45 | return String(text).replace(ESCAPE_REGEX, match => ESCAPE_LOOKUP[match]); 46 | } 47 | 48 | /** 49 | * Sometimes we want to pull out the innermost element of a group. In most 50 | * cases, this will just be the group itself, but when ordgroups and colors have 51 | * a single element, we want to pull that out. 52 | */ 53 | const getBaseElem = function(group ) { 54 | if (group.type === "ordgroup") { 55 | if (group.body.length === 1) { 56 | return getBaseElem(group.body[0]); 57 | } else { 58 | return group; 59 | } 60 | } else if (group.type === "color") { 61 | if (group.body.length === 1) { 62 | return getBaseElem(group.body[0]); 63 | } else { 64 | return group; 65 | } 66 | } else if (group.type === "font") { 67 | return getBaseElem(group.body); 68 | } else { 69 | return group; 70 | } 71 | }; 72 | 73 | /** 74 | * TeXbook algorithms often reference "character boxes", which are simply groups 75 | * with a single character in them. To decide if something is a character box, 76 | * we find its innermost group, and see if it is a single character. 77 | */ 78 | const isCharacterBox = function(group ) { 79 | const baseElem = getBaseElem(group); 80 | 81 | // These are all they types of groups which hold single characters 82 | return baseElem.type === "mathord" || 83 | baseElem.type === "textord" || 84 | baseElem.type === "atom"; 85 | }; 86 | 87 | export const assert = function (value ) { 88 | if (!value) { 89 | throw new Error('Expected non-null, but got ' + String(value)); 90 | } 91 | return value; 92 | }; 93 | 94 | /** 95 | * Return the protocol of a URL, or "_relative" if the URL does not specify a 96 | * protocol (and thus is relative), or `null` if URL has invalid protocol 97 | * (so should be outright rejected). 98 | */ 99 | export const protocolFromUrl = function(url ) { 100 | // Check for possible leading protocol. 101 | // https://url.spec.whatwg.org/#url-parsing strips leading whitespace 102 | // (U+20) or C0 control (U+00-U+1F) characters. 103 | // eslint-disable-next-line no-control-regex 104 | const protocol = /^[\x00-\x20]*([^\\/#?]*?)(:|�*58|�*3a|&colon)/i 105 | .exec(url); 106 | if (!protocol) { 107 | return "_relative"; 108 | } 109 | // Reject weird colons 110 | if (protocol[2] !== ":") { 111 | return null; 112 | } 113 | // Reject invalid characters in scheme according to 114 | // https://datatracker.ietf.org/doc/html/rfc3986#section-3.1 115 | if (!/^[a-zA-Z][a-zA-Z0-9+\-.]*$/.test(protocol[1])) { 116 | return null; 117 | } 118 | // Lowercase the protocol 119 | return protocol[1].toLowerCase(); 120 | }; 121 | 122 | export default { 123 | contains, 124 | deflt, 125 | escape, 126 | hyphenate, 127 | getBaseElem, 128 | isCharacterBox, 129 | protocolFromUrl, 130 | }; 131 | -------------------------------------------------------------------------------- /lib/katex/src/unicodeScripts.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | /* 4 | * This file defines the Unicode scripts and script families that we 5 | * support. To add new scripts or families, just add a new entry to the 6 | * scriptData array below. Adding scripts to the scriptData array allows 7 | * characters from that script to appear in \text{} environments. 8 | */ 9 | 10 | /** 11 | * Each script or script family has a name and an array of blocks. 12 | * Each block is an array of two numbers which specify the start and 13 | * end points (inclusive) of a block of Unicode codepoints. 14 | */ 15 | 16 | 17 | 18 | 19 | 20 | /** 21 | * Unicode block data for the families of scripts we support in \text{}. 22 | * Scripts only need to appear here if they do not have font metrics. 23 | */ 24 | const scriptData = [ 25 | { 26 | // Latin characters beyond the Latin-1 characters we have metrics for. 27 | // Needed for Czech, Hungarian and Turkish text, for example. 28 | name: 'latin', 29 | blocks: [ 30 | [0x0100, 0x024f], // Latin Extended-A and Latin Extended-B 31 | [0x0300, 0x036f], // Combining Diacritical marks 32 | ], 33 | }, 34 | { 35 | // The Cyrillic script used by Russian and related languages. 36 | // A Cyrillic subset used to be supported as explicitly defined 37 | // symbols in symbols.js 38 | name: 'cyrillic', 39 | blocks: [[0x0400, 0x04ff]], 40 | }, 41 | { 42 | // Armenian 43 | name: 'armenian', 44 | blocks: [[0x0530, 0x058F]], 45 | }, 46 | { 47 | // The Brahmic scripts of South and Southeast Asia 48 | // Devanagari (0900–097F) 49 | // Bengali (0980–09FF) 50 | // Gurmukhi (0A00–0A7F) 51 | // Gujarati (0A80–0AFF) 52 | // Oriya (0B00–0B7F) 53 | // Tamil (0B80–0BFF) 54 | // Telugu (0C00–0C7F) 55 | // Kannada (0C80–0CFF) 56 | // Malayalam (0D00–0D7F) 57 | // Sinhala (0D80–0DFF) 58 | // Thai (0E00–0E7F) 59 | // Lao (0E80–0EFF) 60 | // Tibetan (0F00–0FFF) 61 | // Myanmar (1000–109F) 62 | name: 'brahmic', 63 | blocks: [[0x0900, 0x109F]], 64 | }, 65 | { 66 | name: 'georgian', 67 | blocks: [[0x10A0, 0x10ff]], 68 | }, 69 | { 70 | // Chinese and Japanese. 71 | // The "k" in cjk is for Korean, but we've separated Korean out 72 | name: "cjk", 73 | blocks: [ 74 | [0x3000, 0x30FF], // CJK symbols and punctuation, Hiragana, Katakana 75 | [0x4E00, 0x9FAF], // CJK ideograms 76 | [0xFF00, 0xFF60], // Fullwidth punctuation 77 | // TODO: add halfwidth Katakana and Romanji glyphs 78 | ], 79 | }, 80 | { 81 | // Korean 82 | name: 'hangul', 83 | blocks: [[0xAC00, 0xD7AF]], 84 | }, 85 | ]; 86 | 87 | /** 88 | * Given a codepoint, return the name of the script or script family 89 | * it is from, or null if it is not part of a known block 90 | */ 91 | export function scriptFromCodepoint(codepoint ) { 92 | for (let i = 0; i < scriptData.length; i++) { 93 | const script = scriptData[i]; 94 | for (let i = 0; i < script.blocks.length; i++) { 95 | const block = script.blocks[i]; 96 | if (codepoint >= block[0] && codepoint <= block[1]) { 97 | return script.name; 98 | } 99 | } 100 | } 101 | return null; 102 | } 103 | 104 | /** 105 | * A flattened version of all the supported blocks in a single array. 106 | * This is an optimization to make supportedCodepoint() fast. 107 | */ 108 | const allBlocks = []; 109 | scriptData.forEach(s => s.blocks.forEach(b => allBlocks.push(...b))); 110 | 111 | /** 112 | * Given a codepoint, return true if it falls within one of the 113 | * scripts or script families defined above and false otherwise. 114 | * 115 | * Micro benchmarks shows that this is faster than 116 | * /[\u3000-\u30FF\u4E00-\u9FAF\uFF00-\uFF60\uAC00-\uD7AF\u0900-\u109F]/.test() 117 | * in Firefox, Chrome and Node. 118 | */ 119 | export function supportedCodepoint(codepoint ) { 120 | for (let i = 0; i < allBlocks.length; i += 2) { 121 | if (codepoint >= allBlocks[i] && codepoint <= allBlocks[i + 1]) { 122 | return true; 123 | } 124 | } 125 | return false; 126 | } 127 | -------------------------------------------------------------------------------- /lib/katex/src/units.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | /** 4 | * This file does conversion between units. In particular, it provides 5 | * calculateSize to convert other units into ems. 6 | */ 7 | 8 | import ParseError from "./ParseError"; 9 | import Options from "./Options"; 10 | 11 | // This table gives the number of TeX pts in one of each *absolute* TeX unit. 12 | // Thus, multiplying a length by this number converts the length from units 13 | // into pts. Dividing the result by ptPerEm gives the number of ems 14 | // *assuming* a font size of ptPerEm (normal size, normal style). 15 | const ptPerUnit = { 16 | // https://en.wikibooks.org/wiki/LaTeX/Lengths and 17 | // https://tex.stackexchange.com/a/8263 18 | "pt": 1, // TeX point 19 | "mm": 7227 / 2540, // millimeter 20 | "cm": 7227 / 254, // centimeter 21 | "in": 72.27, // inch 22 | "bp": 803 / 800, // big (PostScript) points 23 | "pc": 12, // pica 24 | "dd": 1238 / 1157, // didot 25 | "cc": 14856 / 1157, // cicero (12 didot) 26 | "nd": 685 / 642, // new didot 27 | "nc": 1370 / 107, // new cicero (12 new didot) 28 | "sp": 1 / 65536, // scaled point (TeX's internal smallest unit) 29 | // https://tex.stackexchange.com/a/41371 30 | "px": 803 / 800, // \pdfpxdimen defaults to 1 bp in pdfTeX and LuaTeX 31 | }; 32 | 33 | // Dictionary of relative units, for fast validity testing. 34 | const relativeUnit = { 35 | "ex": true, 36 | "em": true, 37 | "mu": true, 38 | }; 39 | 40 | 41 | 42 | /** 43 | * Determine whether the specified unit (either a string defining the unit 44 | * or a "size" parse node containing a unit field) is valid. 45 | */ 46 | export const validUnit = function(unit ) { 47 | if (typeof unit !== "string") { 48 | unit = unit.unit; 49 | } 50 | return (unit in ptPerUnit || unit in relativeUnit || unit === "ex"); 51 | }; 52 | 53 | /* 54 | * Convert a "size" parse node (with numeric "number" and string "unit" fields, 55 | * as parsed by functions.js argType "size") into a CSS em value for the 56 | * current style/scale. `options` gives the current options. 57 | */ 58 | export const calculateSize = function( 59 | sizeValue , options ) { 60 | let scale; 61 | if (sizeValue.unit in ptPerUnit) { 62 | // Absolute units 63 | scale = ptPerUnit[sizeValue.unit] // Convert unit to pt 64 | / options.fontMetrics().ptPerEm // Convert pt to CSS em 65 | / options.sizeMultiplier; // Unscale to make absolute units 66 | } else if (sizeValue.unit === "mu") { 67 | // `mu` units scale with scriptstyle/scriptscriptstyle. 68 | scale = options.fontMetrics().cssEmPerMu; 69 | } else { 70 | // Other relative units always refer to the *textstyle* font 71 | // in the current size. 72 | let unitOptions; 73 | if (options.style.isTight()) { 74 | // isTight() means current style is script/scriptscript. 75 | unitOptions = options.havingStyle(options.style.text()); 76 | } else { 77 | unitOptions = options; 78 | } 79 | // TODO: In TeX these units are relative to the quad of the current 80 | // *text* font, e.g. cmr10. KaTeX instead uses values from the 81 | // comparably-sized *Computer Modern symbol* font. At 10pt, these 82 | // match. At 7pt and 5pt, they differ: cmr7=1.138894, cmsy7=1.170641; 83 | // cmr5=1.361133, cmsy5=1.472241. Consider $\scriptsize a\kern1emb$. 84 | // TeX \showlists shows a kern of 1.13889 * fontsize; 85 | // KaTeX shows a kern of 1.171 * fontsize. 86 | if (sizeValue.unit === "ex") { 87 | scale = unitOptions.fontMetrics().xHeight; 88 | } else if (sizeValue.unit === "em") { 89 | scale = unitOptions.fontMetrics().quad; 90 | } else { 91 | throw new ParseError("Invalid unit: '" + sizeValue.unit + "'"); 92 | } 93 | if (unitOptions !== options) { 94 | scale *= unitOptions.sizeMultiplier / options.sizeMultiplier; 95 | } 96 | } 97 | return Math.min(sizeValue.number * scale, options.maxSize); 98 | }; 99 | 100 | /** 101 | * Round `n` to 4 decimal places, or to the nearest 1/10,000th em. See 102 | * https://github.com/KaTeX/KaTeX/pull/2460. 103 | */ 104 | export const makeEm = function(n ) { 105 | return +n.toFixed(4) + "em"; 106 | }; 107 | -------------------------------------------------------------------------------- /src/core/src/main.rs: -------------------------------------------------------------------------------- 1 | // Mainly for debug purposes 2 | 3 | use comemo::Prehashed; 4 | use comemo::Track; 5 | use typst; 6 | use core::convert; 7 | use typst::World; 8 | 9 | struct FakeWorld { 10 | library: Prehashed, 11 | } 12 | 13 | impl FakeWorld { 14 | fn new() -> Self { 15 | FakeWorld { 16 | library: Prehashed::new(typst::Library::build()), 17 | } 18 | } 19 | } 20 | 21 | impl World for FakeWorld { 22 | fn library(&self) -> &Prehashed { 23 | &self.library 24 | } 25 | fn book(&self) -> &Prehashed { 26 | unimplemented!(); 27 | } 28 | fn file(&self, id: typst_syntax::FileId) -> typst::diag::FileResult { 29 | unimplemented!(); 30 | } 31 | fn font(&self, index: usize) -> Option { 32 | unimplemented!(); 33 | } 34 | fn main(&self) -> typst_syntax::Source { 35 | unimplemented!(); 36 | } 37 | fn packages(&self) -> &[(typst_syntax::PackageSpec,Option)] { 38 | unimplemented!(); 39 | } 40 | fn source(&self, id: typst_syntax::FileId) -> typst::diag::FileResult { 41 | unimplemented!(); 42 | } 43 | fn today(&self, offset: Option) -> Option { 44 | unimplemented!(); 45 | } 46 | } 47 | 48 | fn eval(world: &dyn World, string: &str) -> typst::foundations::Content { 49 | // Make engine 50 | let introspector = typst::introspection::Introspector::default(); 51 | let mut locator = typst::introspection::Locator::default(); 52 | let mut tracer = typst::eval::Tracer::default(); 53 | 54 | let engine = typst::engine::Engine { 55 | world: world.track(), 56 | introspector: introspector.track(), 57 | route: typst::engine::Route::default(), 58 | locator: &mut locator, 59 | tracer: tracer.track_mut(), 60 | }; 61 | 62 | let result = typst::eval::eval_string( 63 | world.track(), 64 | string, 65 | typst::syntax::Span::detached(), 66 | typst::eval::EvalMode::Math, 67 | world.library().math.scope().clone() 68 | ).unwrap(); 69 | match result { 70 | typst::foundations::Value::Content(content) => content, 71 | _ => panic!(), 72 | } 73 | } 74 | 75 | // Equations tested: 76 | // A = pi r^2 77 | // "area" = pi dot "radius"^2 78 | // cal(A) := { x in RR | x "is natural" } 79 | // x < y => x gt.eq.not y 80 | // sum_(k=0)^n k &= 1 + ... + n \ &= (n(n+1)) / 2 81 | // frac(a^2, 2) 82 | // vec(1, 2, delim: "[") 83 | // mat(1, 2; 3, 4) 84 | // lim_x = op("lim", limits: #true)_x 85 | // (3x + y) / 7 &= 9 && "given" \ 3x + y &= 63 & "multiply by 7" \ 3x &= 63 - y && "subtract y" \ x &= 21 - y/3 & "divide by 3" 86 | // sum_(i=0)^n a_i = 2^(1+i) 87 | // 1/2 < (x+1)/2 88 | // ((x+1)) / 2 = frac(a, b) 89 | // tan x = (sin x)/(cos x) 90 | // op("custom", limits: #true)_(n->oo) n 91 | // bb(b) 92 | // bb(N) = NN 93 | // f: NN -> RR 94 | // vec(a, b, c) dot vec(1, 2, 3) = a + 2b + 3c 95 | 96 | // Works partially 97 | // attach(Pi, t: alpha, b: beta, tl: 1, tr: 2+3, bl: 4+5, br: 6) 98 | // lr(]sum_(x=1)^n] x, size: #50%) 99 | // mat(1, 2, ..., 10; 2, 2, ..., 10; dots.v, dots.v, dots.down, dots.v; 10, 10, ..., 10) 100 | // upright(A) != A 101 | 102 | // Does not work 103 | // grave(a) = accent(a, `) 104 | // arrow(a) = accent(a, arrow) 105 | // tilde(a) = accent(a, \u{0303}) 106 | // scripts(sum)_1^2 != sum_1^2 107 | // limits(A)_1^2 != A_1^2 108 | // (a dot b dot cancel(x)) / cancel(x) 109 | // f(x, y) := cases(1 "if" (x dot y)/2 <= 0, 2 "if" x "is even", 3 "if" x in NN, 4 "else") 110 | // Class: https://typst.app/docs/reference/math/class/ 111 | // abs((x + y) / 2) 112 | // { x mid(|) sum_(i=1)^n w_i|f_i (x)| < 1 } 113 | // norm(x/2) 114 | // abs(x/2) 115 | // floor(x/2) 116 | // ceil(x/2) 117 | // round(x/2) 118 | // sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 119 | // root(3, x) 120 | // sum_i x_i/2 = inline(sum_i x_i/2) 121 | // sum_i x_i/2 = display(sum_i x_i/2) 122 | // sum_i x_i/2 = script(sum_i x_i/2) 123 | // sum_i x_i/2 = sscript(sum_i x_i/2) 124 | // upright(A) != A 125 | // underline(1 + 2 + ... + 5) 126 | // overline(1 + 2 + ... + 5) 127 | // underbrace(1 + 2 + ... + 5, "numbers") 128 | // overbrace(1 + 2 + ... + 5, "numbers") 129 | // underbracket(1 + 2 + ... + 5, "numbers") 130 | // overbracket(1 + 2 + ... + 5, "numbers") 131 | // sans(A B C) 132 | // frak(P) 133 | // mono(x + y = z) 134 | 135 | pub fn main() { 136 | // Try to construct a MathContext object. 137 | let mut world = FakeWorld::new(); 138 | let content = eval(&world, "||x||"); 139 | let math: &typst::math::EquationElem = content.to::().unwrap(); 140 | println!("{:#?}", math); 141 | println!("{:#?}", convert(&content)); 142 | } 143 | -------------------------------------------------------------------------------- /scripts/symbol_gen.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const vm = require('vm'); 3 | const parser = require('@babel/parser'); 4 | const traverse = require('@babel/traverse').default; 5 | const generator = require('@babel/generator').default; 6 | 7 | const data = fs.readFileSync('scripts/dist/symbols.js', 'utf8'); 8 | const ast = parser.parse(data); 9 | 10 | global.rustLines = [] 11 | global.atoms = ['bin', 'close', 'inner', 'open', 'punct', 'rel'] 12 | global.nodeMap = { 13 | 'mathord': 'MathOrd', 14 | 'textord': 'TextOrd', 15 | } 16 | 17 | global.toPascalCase = function(str) { 18 | return str.replace(/(^\w|-\w|_\w)/g, (group) => group.toUpperCase() 19 | .replace('-', '') 20 | .replace('_', '')); 21 | } 22 | 23 | global.getRustGroup = function(group) { 24 | if (atoms.includes(group)) { 25 | return `Group::Atom(AtomGroup::${toPascalCase(group)})`; 26 | } else { 27 | if (group === 'mathord') 28 | group = 'MathOrd'; 29 | else if (group === 'textord') 30 | group = 'TextOrd'; 31 | return `Group::NonAtom(NonAtomGroup::${toPascalCase(group)})`; 32 | } 33 | } 34 | 35 | global.combinations = new Object(); 36 | 37 | function newDefineSymbol(mode, font, group, replace, name, ...args) { 38 | let rustMode = mode === 'math' ? 'Mode::Math' : 'Mode::Text'; 39 | let rustFont = font === 'main' ? 'Font::Main' : 'Font::Ams'; 40 | let rustGroup = getRustGroup(group); 41 | if (replace === null) 42 | return 43 | let rustName = replace === '\\' ? '\\\\' : replace; 44 | if (replace === '0') { 45 | console.log('0'); 46 | } 47 | if (replace === 'A') { 48 | console.log(mode, font, group); 49 | } 50 | let rustLine = `if mode == ${rustMode} && name == '${rustName}' { return Symbol::new(${rustMode}, ${rustFont}, ${rustGroup}, '${rustName}'); }`; 51 | global.combinations[JSON.stringify([rustMode, rustName])] = rustLine; 52 | // global.combinations.add(JSON.stringify([rustMode, rustName])); 53 | // rustLines.push(rustLine); 54 | } 55 | 56 | console.log('Im here'); 57 | 58 | traverse(ast, { 59 | enter(path) { 60 | if (path.node.type === 'FunctionDeclaration' && path.node.id.name === 'defineSymbol') { 61 | path.replaceWith( 62 | parser.parse(newDefineSymbol.toString()).program.body[0] 63 | ) 64 | } 65 | if (path.node.type === 'CallExpression' && path.node.callee.name === 'defineSymbol') { 66 | path.node.callee.name = 'newDefineSymbol'; 67 | } 68 | } 69 | }) 70 | 71 | let { code } = generator(ast); 72 | vm.runInThisContext(code); 73 | 74 | const blockStart = '//// --- AUTO GENERATED CODE --- ////' 75 | const blockEnd = '//// --------------------------- ////' 76 | 77 | const rustSource = fs.readFileSync('src/core/src/katex/symbol.rs', 'utf8'); 78 | 79 | let lines = rustSource.split('\n'); 80 | const blockStartIndex = lines.findIndex(line => line.includes(blockStart)); 81 | const blockEndIndex = lines.findIndex(line => line.includes(blockEnd)); 82 | 83 | let indentationSpace = lines[blockStartIndex].slice(0, lines[blockStartIndex].indexOf(blockStart)); 84 | global.rustLines = Object.values(global.combinations); 85 | let rustLines = global.rustLines.map(s => indentationSpace + s); 86 | lines = lines.slice(0, blockStartIndex + 1).concat(rustLines).concat(lines.slice(blockEndIndex)); 87 | 88 | fs.writeFileSync('src/core/src/katex/symbol.rs', lines.join('\n'), 'utf8'); 89 | 90 | 91 | 92 | // console.log(data.slice(0, data.indexOf(blockStart)) + blockStart + '\n' + global.rustLines.slice(0, 3).join('\n') + '\n' + blockEnd + data.slice(data.indexOf(blockEnd) + blockEnd.length)) 93 | 94 | // const result = data.replace(start) 95 | // console.log(data) 96 | 97 | 98 | // fs.readFile('src/core/src/katex/symbol.rs', 'utf8', function(err, data) { 99 | // if (err) { 100 | // return console.log(err); 101 | // } 102 | 103 | // let lines = data.split('\n'); 104 | // const blockStartIndex = lines.findIndex(line => line.includes(blockStart)) 105 | // const blockEndIndex = lines.findIndex(line => line.includes(blockEnd)) 106 | 107 | // let indentationSpace = lines[blockStartIndex].slice(0, lines[blockStartIndex].indexOf(blockStart)) 108 | // let rustLines = global.rustLines.map(s => indentationSpace + s).slice(0, 3) 109 | // lines = lines.slice(0, blockStartIndex + 1).concat(rustLines).concat(lines.slice(blockEndIndex)) 110 | // fs.writeFileSync() 111 | 112 | 113 | // // console.log(data.slice(0, data.indexOf(blockStart)) + blockStart + '\n' + global.rustLines.slice(0, 3).join('\n') + '\n' + blockEnd + data.slice(data.indexOf(blockEnd) + blockEnd.length)) 114 | 115 | // // const result = data.replace(start) 116 | // // console.log(data) 117 | // }) 118 | -------------------------------------------------------------------------------- /lib/katex/src/functions/sqrt.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | import delimiter from "../delimiter"; 6 | import Style from "../Style"; 7 | import {makeEm} from "../units"; 8 | 9 | import * as html from "../buildHTML"; 10 | import * as mml from "../buildMathML"; 11 | 12 | defineFunction({ 13 | type: "sqrt", 14 | names: ["\\sqrt"], 15 | props: { 16 | numArgs: 1, 17 | numOptionalArgs: 1, 18 | }, 19 | handler({parser}, args, optArgs) { 20 | const index = optArgs[0]; 21 | const body = args[0]; 22 | return { 23 | type: "sqrt", 24 | mode: parser.mode, 25 | body, 26 | index, 27 | }; 28 | }, 29 | htmlBuilder(group, options) { 30 | // Square roots are handled in the TeXbook pg. 443, Rule 11. 31 | 32 | // First, we do the same steps as in overline to build the inner group 33 | // and line 34 | let inner = html.buildGroup(group.body, options.havingCrampedStyle()); 35 | if (inner.height === 0) { 36 | // Render a small surd. 37 | inner.height = options.fontMetrics().xHeight; 38 | } 39 | 40 | // Some groups can return document fragments. Handle those by wrapping 41 | // them in a span. 42 | inner = buildCommon.wrapFragment(inner, options); 43 | 44 | // Calculate the minimum size for the \surd delimiter 45 | const metrics = options.fontMetrics(); 46 | const theta = metrics.defaultRuleThickness; 47 | 48 | let phi = theta; 49 | if (options.style.id < Style.TEXT.id) { 50 | phi = options.fontMetrics().xHeight; 51 | } 52 | 53 | // Calculate the clearance between the body and line 54 | let lineClearance = theta + phi / 4; 55 | 56 | const minDelimiterHeight = (inner.height + inner.depth + 57 | lineClearance + theta); 58 | 59 | // Create a sqrt SVG of the required minimum size 60 | const {span: img, ruleWidth, advanceWidth} = 61 | delimiter.sqrtImage(minDelimiterHeight, options); 62 | 63 | const delimDepth = img.height - ruleWidth; 64 | 65 | // Adjust the clearance based on the delimiter size 66 | if (delimDepth > inner.height + inner.depth + lineClearance) { 67 | lineClearance = 68 | (lineClearance + delimDepth - inner.height - inner.depth) / 2; 69 | } 70 | 71 | // Shift the sqrt image 72 | const imgShift = img.height - inner.height - lineClearance - ruleWidth; 73 | 74 | inner.style.paddingLeft = makeEm(advanceWidth); 75 | 76 | // Overlay the image and the argument. 77 | const body = buildCommon.makeVList({ 78 | positionType: "firstBaseline", 79 | children: [ 80 | {type: "elem", elem: inner, wrapperClasses: ["svg-align"]}, 81 | {type: "kern", size: -(inner.height + imgShift)}, 82 | {type: "elem", elem: img}, 83 | {type: "kern", size: ruleWidth}, 84 | ], 85 | }, options); 86 | 87 | if (!group.index) { 88 | return buildCommon.makeSpan(["mord", "sqrt"], [body], options); 89 | } else { 90 | // Handle the optional root index 91 | 92 | // The index is always in scriptscript style 93 | const newOptions = options.havingStyle(Style.SCRIPTSCRIPT); 94 | const rootm = html.buildGroup(group.index, newOptions, options); 95 | 96 | // The amount the index is shifted by. This is taken from the TeX 97 | // source, in the definition of `\r@@t`. 98 | const toShift = 0.6 * (body.height - body.depth); 99 | 100 | // Build a VList with the superscript shifted up correctly 101 | const rootVList = buildCommon.makeVList({ 102 | positionType: "shift", 103 | positionData: -toShift, 104 | children: [{type: "elem", elem: rootm}], 105 | }, options); 106 | // Add a class surrounding it so we can add on the appropriate 107 | // kerning 108 | const rootVListWrap = buildCommon.makeSpan(["root"], [rootVList]); 109 | 110 | return buildCommon.makeSpan(["mord", "sqrt"], 111 | [rootVListWrap, body], options); 112 | } 113 | }, 114 | mathmlBuilder(group, options) { 115 | const {body, index} = group; 116 | return index ? 117 | new mathMLTree.MathNode( 118 | "mroot", [ 119 | mml.buildGroup(body, options), 120 | mml.buildGroup(index, options), 121 | ]) : 122 | new mathMLTree.MathNode( 123 | "msqrt", [mml.buildGroup(body, options)]); 124 | }, 125 | }); 126 | -------------------------------------------------------------------------------- /lib/katex/src/functions/utils/assembleSupSub.js: -------------------------------------------------------------------------------- 1 | // 2 | import buildCommon from "../../buildCommon"; 3 | import * as html from "../../buildHTML"; 4 | import utils from "../../utils"; 5 | 6 | 7 | 8 | 9 | import {makeEm} from "../../units"; 10 | 11 | // For an operator with limits, assemble the base, sup, and sub into a span. 12 | 13 | export const assembleSupSub = ( 14 | base , 15 | supGroup , 16 | subGroup , 17 | options , 18 | style , 19 | slant , 20 | baseShift , 21 | ) => { 22 | base = buildCommon.makeSpan([], [base]); 23 | const subIsSingleCharacter = subGroup && utils.isCharacterBox(subGroup); 24 | let sub; 25 | let sup; 26 | // We manually have to handle the superscripts and subscripts. This, 27 | // aside from the kern calculations, is copied from supsub. 28 | if (supGroup) { 29 | const elem = html.buildGroup( 30 | supGroup, options.havingStyle(style.sup()), options); 31 | 32 | sup = { 33 | elem, 34 | kern: Math.max( 35 | options.fontMetrics().bigOpSpacing1, 36 | options.fontMetrics().bigOpSpacing3 - elem.depth), 37 | }; 38 | } 39 | 40 | if (subGroup) { 41 | const elem = html.buildGroup( 42 | subGroup, options.havingStyle(style.sub()), options); 43 | 44 | sub = { 45 | elem, 46 | kern: Math.max( 47 | options.fontMetrics().bigOpSpacing2, 48 | options.fontMetrics().bigOpSpacing4 - elem.height), 49 | }; 50 | } 51 | 52 | // Build the final group as a vlist of the possible subscript, base, 53 | // and possible superscript. 54 | let finalGroup; 55 | if (sup && sub) { 56 | const bottom = options.fontMetrics().bigOpSpacing5 + 57 | sub.elem.height + sub.elem.depth + 58 | sub.kern + 59 | base.depth + baseShift; 60 | 61 | finalGroup = buildCommon.makeVList({ 62 | positionType: "bottom", 63 | positionData: bottom, 64 | children: [ 65 | {type: "kern", size: options.fontMetrics().bigOpSpacing5}, 66 | {type: "elem", elem: sub.elem, marginLeft: makeEm(-slant)}, 67 | {type: "kern", size: sub.kern}, 68 | {type: "elem", elem: base}, 69 | {type: "kern", size: sup.kern}, 70 | {type: "elem", elem: sup.elem, marginLeft: makeEm(slant)}, 71 | {type: "kern", size: options.fontMetrics().bigOpSpacing5}, 72 | ], 73 | }, options); 74 | } else if (sub) { 75 | const top = base.height - baseShift; 76 | 77 | // Shift the limits by the slant of the symbol. Note 78 | // that we are supposed to shift the limits by 1/2 of the slant, 79 | // but since we are centering the limits adding a full slant of 80 | // margin will shift by 1/2 that. 81 | finalGroup = buildCommon.makeVList({ 82 | positionType: "top", 83 | positionData: top, 84 | children: [ 85 | {type: "kern", size: options.fontMetrics().bigOpSpacing5}, 86 | {type: "elem", elem: sub.elem, marginLeft: makeEm(-slant)}, 87 | {type: "kern", size: sub.kern}, 88 | {type: "elem", elem: base}, 89 | ], 90 | }, options); 91 | } else if (sup) { 92 | const bottom = base.depth + baseShift; 93 | 94 | finalGroup = buildCommon.makeVList({ 95 | positionType: "bottom", 96 | positionData: bottom, 97 | children: [ 98 | {type: "elem", elem: base}, 99 | {type: "kern", size: sup.kern}, 100 | {type: "elem", elem: sup.elem, marginLeft: makeEm(slant)}, 101 | {type: "kern", size: options.fontMetrics().bigOpSpacing5}, 102 | ], 103 | }, options); 104 | } else { 105 | // This case probably shouldn't occur (this would mean the 106 | // supsub was sending us a group with no superscript or 107 | // subscript) but be safe. 108 | return base; 109 | } 110 | 111 | const parts = [finalGroup]; 112 | if (sub && slant !== 0 && !subIsSingleCharacter) { 113 | // A negative margin-left was applied to the lower limit. 114 | // Avoid an overlap by placing a spacer on the left on the group. 115 | const spacer = buildCommon.makeSpan(["mspace"], [], options); 116 | spacer.style.marginRight = makeEm(slant); 117 | parts.unshift(spacer); 118 | } 119 | return buildCommon.makeSpan(["mop", "op-limits"], parts, options); 120 | }; 121 | -------------------------------------------------------------------------------- /lib/katex/src/Namespace.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | /** 4 | * A `Namespace` refers to a space of nameable things like macros or lengths, 5 | * which can be `set` either globally or local to a nested group, using an 6 | * undo stack similar to how TeX implements this functionality. 7 | * Performance-wise, `get` and local `set` take constant time, while global 8 | * `set` takes time proportional to the depth of group nesting. 9 | */ 10 | 11 | import ParseError from "./ParseError"; 12 | 13 | 14 | 15 | export default class Namespace { 16 | current ; 17 | builtins ; 18 | undefStack ; 19 | 20 | /** 21 | * Both arguments are optional. The first argument is an object of 22 | * built-in mappings which never change. The second argument is an object 23 | * of initial (global-level) mappings, which will constantly change 24 | * according to any global/top-level `set`s done. 25 | */ 26 | constructor(builtins = {}, 27 | globalMacros = {}) { 28 | this.current = globalMacros; 29 | this.builtins = builtins; 30 | this.undefStack = []; 31 | } 32 | 33 | /** 34 | * Start a new nested group, affecting future local `set`s. 35 | */ 36 | beginGroup() { 37 | this.undefStack.push({}); 38 | } 39 | 40 | /** 41 | * End current nested group, restoring values before the group began. 42 | */ 43 | endGroup() { 44 | if (this.undefStack.length === 0) { 45 | throw new ParseError("Unbalanced namespace destruction: attempt " + 46 | "to pop global namespace; please report this as a bug"); 47 | } 48 | const undefs = this.undefStack.pop(); 49 | for (const undef in undefs) { 50 | if (undefs.hasOwnProperty(undef)) { 51 | if (undefs[undef] == null) { 52 | delete this.current[undef]; 53 | } else { 54 | this.current[undef] = undefs[undef]; 55 | } 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * Ends all currently nested groups (if any), restoring values before the 62 | * groups began. Useful in case of an error in the middle of parsing. 63 | */ 64 | endGroups() { 65 | while (this.undefStack.length > 0) { 66 | this.endGroup(); 67 | } 68 | } 69 | 70 | /** 71 | * Detect whether `name` has a definition. Equivalent to 72 | * `get(name) != null`. 73 | */ 74 | has(name ) { 75 | return this.current.hasOwnProperty(name) || 76 | this.builtins.hasOwnProperty(name); 77 | } 78 | 79 | /** 80 | * Get the current value of a name, or `undefined` if there is no value. 81 | * 82 | * Note: Do not use `if (namespace.get(...))` to detect whether a macro 83 | * is defined, as the definition may be the empty string which evaluates 84 | * to `false` in JavaScript. Use `if (namespace.get(...) != null)` or 85 | * `if (namespace.has(...))`. 86 | */ 87 | get(name ) { 88 | if (this.current.hasOwnProperty(name)) { 89 | return this.current[name]; 90 | } else { 91 | return this.builtins[name]; 92 | } 93 | } 94 | 95 | /** 96 | * Set the current value of a name, and optionally set it globally too. 97 | * Local set() sets the current value and (when appropriate) adds an undo 98 | * operation to the undo stack. Global set() may change the undo 99 | * operation at every level, so takes time linear in their number. 100 | * A value of undefined means to delete existing definitions. 101 | */ 102 | set(name , value , global = false) { 103 | if (global) { 104 | // Global set is equivalent to setting in all groups. Simulate this 105 | // by destroying any undos currently scheduled for this name, 106 | // and adding an undo with the *new* value (in case it later gets 107 | // locally reset within this environment). 108 | for (let i = 0; i < this.undefStack.length; i++) { 109 | delete this.undefStack[i][name]; 110 | } 111 | if (this.undefStack.length > 0) { 112 | this.undefStack[this.undefStack.length - 1][name] = value; 113 | } 114 | } else { 115 | // Undo this set at end of this group (possibly to `undefined`), 116 | // unless an undo is already in place, in which case that older 117 | // value is the correct one. 118 | const top = this.undefStack[this.undefStack.length - 1]; 119 | if (top && !top.hasOwnProperty(name)) { 120 | top[name] = this.current[name]; 121 | } 122 | } 123 | if (value == null) { 124 | delete this.current[name]; 125 | } else { 126 | this.current[name] = value; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /lib/katex/src/wide-character.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | /** 4 | * This file provides support for Unicode range U+1D400 to U+1D7FF, 5 | * Mathematical Alphanumeric Symbols. 6 | * 7 | * Function wideCharacterFont takes a wide character as input and returns 8 | * the font information necessary to render it properly. 9 | */ 10 | 11 | 12 | import ParseError from "./ParseError"; 13 | 14 | /** 15 | * Data below is from https://www.unicode.org/charts/PDF/U1D400.pdf 16 | * That document sorts characters into groups by font type, say bold or italic. 17 | * 18 | * In the arrays below, each subarray consists three elements: 19 | * * The CSS class of that group when in math mode. 20 | * * The CSS class of that group when in text mode. 21 | * * The font name, so that KaTeX can get font metrics. 22 | */ 23 | 24 | const wideLatinLetterData = [ 25 | ["mathbf", "textbf", "Main-Bold"], // A-Z bold upright 26 | ["mathbf", "textbf", "Main-Bold"], // a-z bold upright 27 | 28 | ["mathnormal", "textit", "Math-Italic"], // A-Z italic 29 | ["mathnormal", "textit", "Math-Italic"], // a-z italic 30 | 31 | ["boldsymbol", "boldsymbol", "Main-BoldItalic"], // A-Z bold italic 32 | ["boldsymbol", "boldsymbol", "Main-BoldItalic"], // a-z bold italic 33 | 34 | // Map fancy A-Z letters to script, not calligraphic. 35 | // This aligns with unicode-math and math fonts (except Cambria Math). 36 | ["mathscr", "textscr", "Script-Regular"], // A-Z script 37 | ["", "", ""], // a-z script. No font 38 | 39 | ["", "", ""], // A-Z bold script. No font 40 | ["", "", ""], // a-z bold script. No font 41 | 42 | ["mathfrak", "textfrak", "Fraktur-Regular"], // A-Z Fraktur 43 | ["mathfrak", "textfrak", "Fraktur-Regular"], // a-z Fraktur 44 | 45 | ["mathbb", "textbb", "AMS-Regular"], // A-Z double-struck 46 | ["mathbb", "textbb", "AMS-Regular"], // k double-struck 47 | 48 | // Note that we are using a bold font, but font metrics for regular Fraktur. 49 | ["mathboldfrak", "textboldfrak", "Fraktur-Regular"], // A-Z bold Fraktur 50 | ["mathboldfrak", "textboldfrak", "Fraktur-Regular"], // a-z bold Fraktur 51 | 52 | ["mathsf", "textsf", "SansSerif-Regular"], // A-Z sans-serif 53 | ["mathsf", "textsf", "SansSerif-Regular"], // a-z sans-serif 54 | 55 | ["mathboldsf", "textboldsf", "SansSerif-Bold"], // A-Z bold sans-serif 56 | ["mathboldsf", "textboldsf", "SansSerif-Bold"], // a-z bold sans-serif 57 | 58 | ["mathitsf", "textitsf", "SansSerif-Italic"], // A-Z italic sans-serif 59 | ["mathitsf", "textitsf", "SansSerif-Italic"], // a-z italic sans-serif 60 | 61 | ["", "", ""], // A-Z bold italic sans. No font 62 | ["", "", ""], // a-z bold italic sans. No font 63 | 64 | ["mathtt", "texttt", "Typewriter-Regular"], // A-Z monospace 65 | ["mathtt", "texttt", "Typewriter-Regular"], // a-z monospace 66 | ]; 67 | 68 | const wideNumeralData = [ 69 | ["mathbf", "textbf", "Main-Bold"], // 0-9 bold 70 | ["", "", ""], // 0-9 double-struck. No KaTeX font. 71 | ["mathsf", "textsf", "SansSerif-Regular"], // 0-9 sans-serif 72 | ["mathboldsf", "textboldsf", "SansSerif-Bold"], // 0-9 bold sans-serif 73 | ["mathtt", "texttt", "Typewriter-Regular"], // 0-9 monospace 74 | ]; 75 | 76 | export const wideCharacterFont = function( 77 | wideChar , 78 | mode , 79 | ) { 80 | 81 | // IE doesn't support codePointAt(). So work with the surrogate pair. 82 | const H = wideChar.charCodeAt(0); // high surrogate 83 | const L = wideChar.charCodeAt(1); // low surrogate 84 | const codePoint = ((H - 0xD800) * 0x400) + (L - 0xDC00) + 0x10000; 85 | 86 | const j = mode === "math" ? 0 : 1; // column index for CSS class. 87 | 88 | if (0x1D400 <= codePoint && codePoint < 0x1D6A4) { 89 | // wideLatinLetterData contains exactly 26 chars on each row. 90 | // So we can calculate the relevant row. No traverse necessary. 91 | const i = Math.floor((codePoint - 0x1D400) / 26); 92 | return [wideLatinLetterData[i][2], wideLatinLetterData[i][j]]; 93 | 94 | } else if (0x1D7CE <= codePoint && codePoint <= 0x1D7FF) { 95 | // Numerals, ten per row. 96 | const i = Math.floor((codePoint - 0x1D7CE) / 10); 97 | return [wideNumeralData[i][2], wideNumeralData[i][j]]; 98 | 99 | } else if (codePoint === 0x1D6A5 || codePoint === 0x1D6A6) { 100 | // dotless i or j 101 | return [wideLatinLetterData[0][2], wideLatinLetterData[0][j]]; 102 | 103 | } else if (0x1D6A6 < codePoint && codePoint < 0x1D7CE) { 104 | // Greek letters. Not supported, yet. 105 | return ["", ""]; 106 | 107 | } else { 108 | // We don't support any wide characters outside 1D400–1D7FF. 109 | throw new ParseError("Unsupported character: " + wideChar); 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /lib/katex/src/functions/horizBrace.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction from "../defineFunction"; 3 | import buildCommon from "../buildCommon"; 4 | import mathMLTree from "../mathMLTree"; 5 | import stretchy from "../stretchy"; 6 | import Style from "../Style"; 7 | import {assertNodeType} from "../parseNode"; 8 | 9 | import * as html from "../buildHTML"; 10 | import * as mml from "../buildMathML"; 11 | 12 | 13 | 14 | 15 | // NOTE: Unlike most `htmlBuilder`s, this one handles not only "horizBrace", but 16 | // also "supsub" since an over/underbrace can affect super/subscripting. 17 | export const htmlBuilder = (grp, options) => { 18 | const style = options.style; 19 | 20 | // Pull out the `ParseNode<"horizBrace">` if `grp` is a "supsub" node. 21 | let supSubGroup; 22 | let group ; 23 | if (grp.type === "supsub") { 24 | // Ref: LaTeX source2e: }}}}\limits} 25 | // i.e. LaTeX treats the brace similar to an op and passes it 26 | // with \limits, so we need to assign supsub style. 27 | supSubGroup = grp.sup ? 28 | html.buildGroup(grp.sup, options.havingStyle(style.sup()), options) : 29 | html.buildGroup(grp.sub, options.havingStyle(style.sub()), options); 30 | group = assertNodeType(grp.base, "horizBrace"); 31 | } else { 32 | group = assertNodeType(grp, "horizBrace"); 33 | } 34 | 35 | // Build the base group 36 | const body = html.buildGroup( 37 | group.base, options.havingBaseStyle(Style.DISPLAY)); 38 | 39 | // Create the stretchy element 40 | const braceBody = stretchy.svgSpan(group, options); 41 | 42 | // Generate the vlist, with the appropriate kerns ┏━━━━━━━━┓ 43 | // This first vlist contains the content and the brace: equation 44 | let vlist; 45 | if (group.isOver) { 46 | vlist = buildCommon.makeVList({ 47 | positionType: "firstBaseline", 48 | children: [ 49 | {type: "elem", elem: body}, 50 | {type: "kern", size: 0.1}, 51 | {type: "elem", elem: braceBody}, 52 | ], 53 | }, options); 54 | // $FlowFixMe: Replace this with passing "svg-align" into makeVList. 55 | vlist.children[0].children[0].children[1].classes.push("svg-align"); 56 | } else { 57 | vlist = buildCommon.makeVList({ 58 | positionType: "bottom", 59 | positionData: body.depth + 0.1 + braceBody.height, 60 | children: [ 61 | {type: "elem", elem: braceBody}, 62 | {type: "kern", size: 0.1}, 63 | {type: "elem", elem: body}, 64 | ], 65 | }, options); 66 | // $FlowFixMe: Replace this with passing "svg-align" into makeVList. 67 | vlist.children[0].children[0].children[0].classes.push("svg-align"); 68 | } 69 | 70 | if (supSubGroup) { 71 | // To write the supsub, wrap the first vlist in another vlist: 72 | // They can't all go in the same vlist, because the note might be 73 | // wider than the equation. We want the equation to control the 74 | // brace width. 75 | 76 | // note long note long note 77 | // ┏━━━━━━━━┓ or ┏━━━┓ not ┏━━━━━━━━━┓ 78 | // equation eqn eqn 79 | 80 | const vSpan = buildCommon.makeSpan( 81 | ["mord", (group.isOver ? "mover" : "munder")], 82 | [vlist], options); 83 | 84 | if (group.isOver) { 85 | vlist = buildCommon.makeVList({ 86 | positionType: "firstBaseline", 87 | children: [ 88 | {type: "elem", elem: vSpan}, 89 | {type: "kern", size: 0.2}, 90 | {type: "elem", elem: supSubGroup}, 91 | ], 92 | }, options); 93 | } else { 94 | vlist = buildCommon.makeVList({ 95 | positionType: "bottom", 96 | positionData: vSpan.depth + 0.2 + supSubGroup.height + 97 | supSubGroup.depth, 98 | children: [ 99 | {type: "elem", elem: supSubGroup}, 100 | {type: "kern", size: 0.2}, 101 | {type: "elem", elem: vSpan}, 102 | ], 103 | }, options); 104 | } 105 | } 106 | 107 | return buildCommon.makeSpan( 108 | ["mord", (group.isOver ? "mover" : "munder")], [vlist], options); 109 | }; 110 | 111 | const mathmlBuilder = (group, options) => { 112 | const accentNode = stretchy.mathMLnode(group.label); 113 | return new mathMLTree.MathNode( 114 | (group.isOver ? "mover" : "munder"), 115 | [mml.buildGroup(group.base, options), accentNode] 116 | ); 117 | }; 118 | 119 | // Horizontal stretchy braces 120 | defineFunction({ 121 | type: "horizBrace", 122 | names: ["\\overbrace", "\\underbrace"], 123 | props: { 124 | numArgs: 1, 125 | }, 126 | handler({parser, funcName}, args) { 127 | return { 128 | type: "horizBrace", 129 | mode: parser.mode, 130 | label: funcName, 131 | isOver: /^\\over/.test(funcName), 132 | base: args[0], 133 | }; 134 | }, 135 | htmlBuilder, 136 | mathmlBuilder, 137 | }); 138 | -------------------------------------------------------------------------------- /lib/katex/src/Lexer.js: -------------------------------------------------------------------------------- 1 | // 2 | /** 3 | * The Lexer class handles tokenizing the input in various ways. Since our 4 | * parser expects us to be able to backtrack, the lexer allows lexing from any 5 | * given starting point. 6 | * 7 | * Its main exposed function is the `lex` function, which takes a position to 8 | * lex from and a type of token to lex. It defers to the appropriate `_innerLex` 9 | * function. 10 | * 11 | * The various `_innerLex` functions perform the actual lexing of different 12 | * kinds. 13 | */ 14 | 15 | import ParseError from "./ParseError"; 16 | import SourceLocation from "./SourceLocation"; 17 | import {Token} from "./Token"; 18 | 19 | 20 | 21 | 22 | /* The following tokenRegex 23 | * - matches typical whitespace (but not NBSP etc.) using its first group 24 | * - does not match any control character \x00-\x1f except whitespace 25 | * - does not match a bare backslash 26 | * - matches any ASCII character except those just mentioned 27 | * - does not match the BMP private use area \uE000-\uF8FF 28 | * - does not match bare surrogate code units 29 | * - matches any BMP character except for those just described 30 | * - matches any valid Unicode surrogate pair 31 | * - matches a backslash followed by one or more whitespace characters 32 | * - matches a backslash followed by one or more letters then whitespace 33 | * - matches a backslash followed by any BMP character 34 | * Capturing groups: 35 | * [1] regular whitespace 36 | * [2] backslash followed by whitespace 37 | * [3] anything else, which may include: 38 | * [4] left character of \verb* 39 | * [5] left character of \verb 40 | * [6] backslash followed by word, excluding any trailing whitespace 41 | * Just because the Lexer matches something doesn't mean it's valid input: 42 | * If there is no matching function or symbol definition, the Parser will 43 | * still reject the input. 44 | */ 45 | const spaceRegexString = "[ \r\n\t]"; 46 | const controlWordRegexString = "\\\\[a-zA-Z@]+"; 47 | const controlSymbolRegexString = "\\\\[^\uD800-\uDFFF]"; 48 | const controlWordWhitespaceRegexString = 49 | `(${controlWordRegexString})${spaceRegexString}*`; 50 | const controlSpaceRegexString = "\\\\(\n|[ \r\t]+\n?)[ \r\t]*"; 51 | const combiningDiacriticalMarkString = "[\u0300-\u036f]"; 52 | export const combiningDiacriticalMarksEndRegex = 53 | new RegExp(`${combiningDiacriticalMarkString}+$`); 54 | const tokenRegexString = `(${spaceRegexString}+)|` + // whitespace 55 | `${controlSpaceRegexString}|` + // \whitespace 56 | "([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" + // single codepoint 57 | `${combiningDiacriticalMarkString}*` + // ...plus accents 58 | "|[\uD800-\uDBFF][\uDC00-\uDFFF]" + // surrogate pair 59 | `${combiningDiacriticalMarkString}*` + // ...plus accents 60 | "|\\\\verb\\*([^]).*?\\4" + // \verb* 61 | "|\\\\verb([^*a-zA-Z]).*?\\5" + // \verb unstarred 62 | `|${controlWordWhitespaceRegexString}` + // \macroName + spaces 63 | `|${controlSymbolRegexString})`; // \\, \', etc. 64 | 65 | /** Main Lexer class */ 66 | export default class Lexer { 67 | input ; 68 | settings ; 69 | tokenRegex ; 70 | // Category codes. The lexer only supports comment characters (14) for now. 71 | // MacroExpander additionally distinguishes active (13). 72 | catcodes ; 73 | 74 | constructor(input , settings ) { 75 | // Separate accents from characters 76 | this.input = input; 77 | this.settings = settings; 78 | this.tokenRegex = new RegExp(tokenRegexString, 'g'); 79 | this.catcodes = { 80 | "%": 14, // comment character 81 | "~": 13, // active character 82 | }; 83 | } 84 | 85 | setCatcode(char , code ) { 86 | this.catcodes[char] = code; 87 | } 88 | 89 | /** 90 | * This function lexes a single token. 91 | */ 92 | lex() { 93 | const input = this.input; 94 | const pos = this.tokenRegex.lastIndex; 95 | if (pos === input.length) { 96 | return new Token("EOF", new SourceLocation(this, pos, pos)); 97 | } 98 | const match = this.tokenRegex.exec(input); 99 | if (match === null || match.index !== pos) { 100 | throw new ParseError( 101 | `Unexpected character: '${input[pos]}'`, 102 | new Token(input[pos], new SourceLocation(this, pos, pos + 1))); 103 | } 104 | const text = match[6] || match[3] || (match[2] ? "\\ " : " "); 105 | 106 | if (this.catcodes[text] === 14) { // comment character 107 | const nlIndex = input.indexOf('\n', this.tokenRegex.lastIndex); 108 | if (nlIndex === -1) { 109 | this.tokenRegex.lastIndex = input.length; // EOF 110 | this.settings.reportNonstrict("commentAtEnd", 111 | "% comment has no terminating newline; LaTeX would " + 112 | "fail because of commenting the end of math mode (e.g. $)"); 113 | } else { 114 | this.tokenRegex.lastIndex = nlIndex + 1; 115 | } 116 | return this.lex(); 117 | } 118 | 119 | return new Token(text, new SourceLocation(this, pos, 120 | this.tokenRegex.lastIndex)); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/katex/src/functions/includegraphics.js: -------------------------------------------------------------------------------- 1 | // 2 | import defineFunction from "../defineFunction"; 3 | 4 | import {calculateSize, validUnit, makeEm} from "../units"; 5 | import ParseError from "../ParseError"; 6 | import {Img} from "../domTree"; 7 | import mathMLTree from "../mathMLTree"; 8 | import {assertNodeType} from "../parseNode"; 9 | 10 | 11 | const sizeData = function(str ) { 12 | if (/^[-+]? *(\d+(\.\d*)?|\.\d+)$/.test(str)) { 13 | // str is a number with no unit specified. 14 | // default unit is bp, per graphix package. 15 | return {number: +str, unit: "bp"}; 16 | } else { 17 | const match = (/([-+]?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(str); 18 | if (!match) { 19 | throw new ParseError("Invalid size: '" + str 20 | + "' in \\includegraphics"); 21 | } 22 | const data = { 23 | number: +(match[1] + match[2]), // sign + magnitude, cast to number 24 | unit: match[3], 25 | }; 26 | if (!validUnit(data)) { 27 | throw new ParseError("Invalid unit: '" + data.unit 28 | + "' in \\includegraphics."); 29 | } 30 | return data; 31 | } 32 | }; 33 | 34 | defineFunction({ 35 | type: "includegraphics", 36 | names: ["\\includegraphics"], 37 | props: { 38 | numArgs: 1, 39 | numOptionalArgs: 1, 40 | argTypes: ["raw", "url"], 41 | allowedInText: false, 42 | }, 43 | handler: ({parser}, args, optArgs) => { 44 | let width = {number: 0, unit: "em"}; 45 | let height = {number: 0.9, unit: "em"}; // sorta character sized. 46 | let totalheight = {number: 0, unit: "em"}; 47 | let alt = ""; 48 | 49 | if (optArgs[0]) { 50 | const attributeStr = assertNodeType(optArgs[0], "raw").string; 51 | 52 | // Parser.js does not parse key/value pairs. We get a string. 53 | const attributes = attributeStr.split(","); 54 | for (let i = 0; i < attributes.length; i++) { 55 | const keyVal = attributes[i].split("="); 56 | if (keyVal.length === 2) { 57 | const str = keyVal[1].trim(); 58 | switch (keyVal[0].trim()) { 59 | case "alt": 60 | alt = str; 61 | break; 62 | case "width": 63 | width = sizeData(str); 64 | break; 65 | case "height": 66 | height = sizeData(str); 67 | break; 68 | case "totalheight": 69 | totalheight = sizeData(str); 70 | break; 71 | default: 72 | throw new ParseError("Invalid key: '" + keyVal[0] + 73 | "' in \\includegraphics."); 74 | } 75 | } 76 | } 77 | } 78 | 79 | const src = assertNodeType(args[0], "url").url; 80 | 81 | if (alt === "") { 82 | // No alt given. Use the file name. Strip away the path. 83 | alt = src; 84 | alt = alt.replace(/^.*[\\/]/, ''); 85 | alt = alt.substring(0, alt.lastIndexOf('.')); 86 | } 87 | 88 | if (!parser.settings.isTrusted({ 89 | command: "\\includegraphics", 90 | url: src, 91 | })) { 92 | return parser.formatUnsupportedCmd("\\includegraphics"); 93 | } 94 | 95 | return { 96 | type: "includegraphics", 97 | mode: parser.mode, 98 | alt: alt, 99 | width: width, 100 | height: height, 101 | totalheight: totalheight, 102 | src: src, 103 | }; 104 | }, 105 | htmlBuilder: (group, options) => { 106 | const height = calculateSize(group.height, options); 107 | let depth = 0; 108 | 109 | if (group.totalheight.number > 0) { 110 | depth = calculateSize(group.totalheight, options) - height; 111 | } 112 | 113 | let width = 0; 114 | if (group.width.number > 0) { 115 | width = calculateSize(group.width, options); 116 | } 117 | 118 | const style = {height: makeEm(height + depth)}; 119 | if (width > 0) { 120 | style.width = makeEm(width); 121 | } 122 | if (depth > 0) { 123 | style.verticalAlign = makeEm(-depth); 124 | } 125 | 126 | const node = new Img(group.src, group.alt, style); 127 | node.height = height; 128 | node.depth = depth; 129 | 130 | return node; 131 | }, 132 | mathmlBuilder: (group, options) => { 133 | const node = new mathMLTree.MathNode("mglyph", []); 134 | node.setAttribute("alt", group.alt); 135 | 136 | const height = calculateSize(group.height, options); 137 | let depth = 0; 138 | if (group.totalheight.number > 0) { 139 | depth = calculateSize(group.totalheight, options) - height; 140 | node.setAttribute("valign", makeEm(-depth)); 141 | } 142 | node.setAttribute("height", makeEm(height + depth)); 143 | 144 | if (group.width.number > 0) { 145 | const width = calculateSize(group.width, options); 146 | node.setAttribute("width", makeEm(width)); 147 | } 148 | node.setAttribute("src", group.src); 149 | return node; 150 | }, 151 | }); 152 | -------------------------------------------------------------------------------- /lib/katex/contrib/auto-render/auto-render.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console:0 */ 2 | 3 | import katex from "katex"; 4 | import splitAtDelimiters from "./splitAtDelimiters"; 5 | 6 | /* Note: optionsCopy is mutated by this method. If it is ever exposed in the 7 | * API, we should copy it before mutating. 8 | */ 9 | const renderMathInText = function(text, optionsCopy) { 10 | const data = splitAtDelimiters(text, optionsCopy.delimiters); 11 | if (data.length === 1 && data[0].type === 'text') { 12 | // There is no formula in the text. 13 | // Let's return null which means there is no need to replace 14 | // the current text node with a new one. 15 | return null; 16 | } 17 | 18 | const fragment = document.createDocumentFragment(); 19 | 20 | for (let i = 0; i < data.length; i++) { 21 | if (data[i].type === "text") { 22 | fragment.appendChild(document.createTextNode(data[i].data)); 23 | } else { 24 | const span = document.createElement("span"); 25 | let math = data[i].data; 26 | // Override any display mode defined in the settings with that 27 | // defined by the text itself 28 | optionsCopy.displayMode = data[i].display; 29 | try { 30 | if (optionsCopy.preProcess) { 31 | math = optionsCopy.preProcess(math); 32 | } 33 | katex.render(math, span, optionsCopy); 34 | } catch (e) { 35 | if (!(e instanceof katex.ParseError)) { 36 | throw e; 37 | } 38 | optionsCopy.errorCallback( 39 | "KaTeX auto-render: Failed to parse `" + data[i].data + 40 | "` with ", 41 | e 42 | ); 43 | fragment.appendChild(document.createTextNode(data[i].rawData)); 44 | continue; 45 | } 46 | fragment.appendChild(span); 47 | } 48 | } 49 | 50 | return fragment; 51 | }; 52 | 53 | const renderElem = function(elem, optionsCopy) { 54 | for (let i = 0; i < elem.childNodes.length; i++) { 55 | const childNode = elem.childNodes[i]; 56 | if (childNode.nodeType === 3) { 57 | // Text node 58 | // Concatenate all sibling text nodes. 59 | // Webkit browsers split very large text nodes into smaller ones, 60 | // so the delimiters may be split across different nodes. 61 | let textContentConcat = childNode.textContent; 62 | let sibling = childNode.nextSibling; 63 | let nSiblings = 0; 64 | while (sibling && (sibling.nodeType === Node.TEXT_NODE)) { 65 | textContentConcat += sibling.textContent; 66 | sibling = sibling.nextSibling; 67 | nSiblings++; 68 | } 69 | const frag = renderMathInText(textContentConcat, optionsCopy); 70 | if (frag) { 71 | // Remove extra text nodes 72 | for (let j = 0; j < nSiblings; j++) { 73 | childNode.nextSibling.remove(); 74 | } 75 | i += frag.childNodes.length - 1; 76 | elem.replaceChild(frag, childNode); 77 | } else { 78 | // If the concatenated text does not contain math 79 | // the siblings will not either 80 | i += nSiblings; 81 | } 82 | } else if (childNode.nodeType === 1) { 83 | // Element node 84 | const className = ' ' + childNode.className + ' '; 85 | const shouldRender = optionsCopy.ignoredTags.indexOf( 86 | childNode.nodeName.toLowerCase()) === -1 && 87 | optionsCopy.ignoredClasses.every( 88 | x => className.indexOf(' ' + x + ' ') === -1); 89 | 90 | if (shouldRender) { 91 | renderElem(childNode, optionsCopy); 92 | } 93 | } 94 | // Otherwise, it's something else, and ignore it. 95 | } 96 | }; 97 | 98 | const renderMathInElement = function(elem, options) { 99 | if (!elem) { 100 | throw new Error("No element provided to render"); 101 | } 102 | 103 | const optionsCopy = {}; 104 | 105 | // Object.assign(optionsCopy, option) 106 | for (const option in options) { 107 | if (options.hasOwnProperty(option)) { 108 | optionsCopy[option] = options[option]; 109 | } 110 | } 111 | 112 | // default options 113 | optionsCopy.delimiters = optionsCopy.delimiters || [ 114 | {left: "$$", right: "$$", display: true}, 115 | {left: "\\(", right: "\\)", display: false}, 116 | // LaTeX uses $…$, but it ruins the display of normal `$` in text: 117 | // {left: "$", right: "$", display: false}, 118 | // $ must come after $$ 119 | 120 | // Render AMS environments even if outside $$…$$ delimiters. 121 | {left: "\\begin{equation}", right: "\\end{equation}", display: true}, 122 | {left: "\\begin{align}", right: "\\end{align}", display: true}, 123 | {left: "\\begin{alignat}", right: "\\end{alignat}", display: true}, 124 | {left: "\\begin{gather}", right: "\\end{gather}", display: true}, 125 | {left: "\\begin{CD}", right: "\\end{CD}", display: true}, 126 | 127 | {left: "\\[", right: "\\]", display: true}, 128 | ]; 129 | optionsCopy.ignoredTags = optionsCopy.ignoredTags || [ 130 | "script", "noscript", "style", "textarea", "pre", "code", "option", 131 | ]; 132 | optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || []; 133 | optionsCopy.errorCallback = optionsCopy.errorCallback || console.error; 134 | 135 | // Enable sharing of global macros defined via `\gdef` between different 136 | // math elements within a single call to `renderMathInElement`. 137 | optionsCopy.macros = optionsCopy.macros || {}; 138 | 139 | renderElem(elem, optionsCopy); 140 | }; 141 | 142 | export default renderMathInElement; 143 | --------------------------------------------------------------------------------