mirror of
https://github.com/ggml-org/llama.cpp.git
synced 2025-11-06 09:46:50 +00:00
feat(webui): improve LaTeX rendering with currency detection (#16508)
* webui : Revised LaTeX formula recognition * webui : Further examples containg amounts * webui : vitest for maskInlineLaTeX * webui: Moved preprocessLaTeX to lib/utils * webui: LaTeX in table-cells * chore: update webui build output (use theirs) * webui: backslash in LaTeX-preprocessing * chore: update webui build output * webui: look-behind backslash-check * chore: update webui build output * Apply suggestions from code review Code maintenance (variable names, code formatting, string handling) Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com> * webui: Moved constants to lib/constants. * webui: package woff2 inside base64 data * webui: LaTeX-line-break in display formula * chore: update webui build output * webui: Bugfix (font embedding) * webui: Bugfix (font embedding) * webui: vite embeds assets * webui: don't suppress 404 (fonts) * refactor: KaTeX integration with SCSS Moves KaTeX styling to SCSS for better customization and font embedding. This change includes: - Adding `sass` as a dev dependency. - Introducing a custom SCSS file to override KaTeX variables and disable TTF/WOFF fonts, relying solely on WOFF2 for embedding. - Adjusting the Vite configuration to resolve `katex-fonts` alias and inject SCSS variables. * fix: LaTeX processing within blockquotes * webui: update webui build output --------- Co-authored-by: Aleksander Grygier <aleksander.grygier@gmail.com>
This commit is contained in:
Binary file not shown.
361
tools/server/webui/package-lock.json
generated
361
tools/server/webui/package-lock.json
generated
@@ -59,6 +59,7 @@
|
|||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
|
"sass": "^1.93.3",
|
||||||
"storybook": "^9.0.17",
|
"storybook": "^9.0.17",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
@@ -1176,6 +1177,330 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@parcel/watcher": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"detect-libc": "^1.0.3",
|
||||||
|
"is-glob": "^4.0.3",
|
||||||
|
"micromatch": "^4.0.5",
|
||||||
|
"node-addon-api": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@parcel/watcher-android-arm64": "2.5.1",
|
||||||
|
"@parcel/watcher-darwin-arm64": "2.5.1",
|
||||||
|
"@parcel/watcher-darwin-x64": "2.5.1",
|
||||||
|
"@parcel/watcher-freebsd-x64": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-arm-glibc": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-arm-musl": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-arm64-glibc": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-arm64-musl": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-x64-glibc": "2.5.1",
|
||||||
|
"@parcel/watcher-linux-x64-musl": "2.5.1",
|
||||||
|
"@parcel/watcher-win32-arm64": "2.5.1",
|
||||||
|
"@parcel/watcher-win32-ia32": "2.5.1",
|
||||||
|
"@parcel/watcher-win32-x64": "2.5.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-android-arm64": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-darwin-arm64": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-darwin-x64": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-freebsd-x64": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-arm-glibc": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-arm-musl": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-arm64-glibc": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-arm64-musl": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-x64-glibc": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-linux-x64-musl": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-win32-arm64": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-win32-ia32": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher-win32-x64": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/parcel"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@parcel/watcher/node_modules/detect-libc": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"bin": {
|
||||||
|
"detect-libc": "bin/detect-libc.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@playwright/test": {
|
"node_modules/@playwright/test": {
|
||||||
"version": "1.54.1",
|
"version": "1.54.1",
|
||||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz",
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.54.1.tgz",
|
||||||
@@ -4697,6 +5022,13 @@
|
|||||||
"node": ">= 4"
|
"node": ">= 4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immutable": {
|
||||||
|
"version": "5.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz",
|
||||||
|
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/import-fresh": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@@ -6462,6 +6794,14 @@
|
|||||||
"tslib": "^2.0.3"
|
"tslib": "^2.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/node-addon-api": {
|
||||||
|
"version": "7.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||||
|
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/object-inspect": {
|
"node_modules/object-inspect": {
|
||||||
"version": "1.13.4",
|
"version": "1.13.4",
|
||||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
@@ -7484,6 +7824,27 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/sass": {
|
||||||
|
"version": "1.93.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/sass/-/sass-1.93.3.tgz",
|
||||||
|
"integrity": "sha512-elOcIZRTM76dvxNAjqYrucTSI0teAF/L2Lv0s6f6b7FOwcwIuA357bIE871580AjHJuSvLIRUosgV+lIWx6Rgg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chokidar": "^4.0.0",
|
||||||
|
"immutable": "^5.0.2",
|
||||||
|
"source-map-js": ">=0.6.2 <2.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"sass": "sass.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@parcel/watcher": "^2.4.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.26.0",
|
"version": "0.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
|
"sass": "^1.93.3",
|
||||||
"storybook": "^9.0.17",
|
"storybook": "^9.0.17",
|
||||||
"svelte": "^5.0.0",
|
"svelte": "^5.0.0",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
|
|||||||
@@ -8,8 +8,9 @@
|
|||||||
import rehypeKatex from 'rehype-katex';
|
import rehypeKatex from 'rehype-katex';
|
||||||
import rehypeStringify from 'rehype-stringify';
|
import rehypeStringify from 'rehype-stringify';
|
||||||
import { copyCodeToClipboard } from '$lib/utils/copy';
|
import { copyCodeToClipboard } from '$lib/utils/copy';
|
||||||
|
import { preprocessLaTeX } from '$lib/utils/latex-protection';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import 'katex/dist/katex.min.css';
|
import '$styles/katex-custom.scss';
|
||||||
|
|
||||||
import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
|
import githubDarkCss from 'highlight.js/styles/github-dark.css?inline';
|
||||||
import githubLightCss from 'highlight.js/styles/github.css?inline';
|
import githubLightCss from 'highlight.js/styles/github.css?inline';
|
||||||
@@ -176,19 +177,9 @@
|
|||||||
return mutated ? tempDiv.innerHTML : html;
|
return mutated ? tempDiv.innerHTML : html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeMathDelimiters(text: string): string {
|
|
||||||
return text
|
|
||||||
.replace(/(^|[^\\])\\\[((?:\\.|[\s\S])*?)\\\]/g, (_, prefix: string, content: string) => {
|
|
||||||
return `${prefix}$$${content}$$`;
|
|
||||||
})
|
|
||||||
.replace(/(^|[^\\])\\\(((?:\\.|[\s\S])*?)\\\)/g, (_, prefix: string, content: string) => {
|
|
||||||
return `${prefix}$${content}$`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processMarkdown(text: string): Promise<string> {
|
async function processMarkdown(text: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const normalized = normalizeMathDelimiters(text);
|
let normalized = preprocessLaTeX(text);
|
||||||
const result = await processor().process(normalized);
|
const result = await processor().process(normalized);
|
||||||
const html = String(result);
|
const html = String(result);
|
||||||
const enhancedLinks = enhanceLinks(html);
|
const enhancedLinks = enhanceLinks(html);
|
||||||
|
|||||||
35
tools/server/webui/src/lib/constants/latex-protection.ts
Normal file
35
tools/server/webui/src/lib/constants/latex-protection.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Matches common Markdown code blocks to exclude them from further processing (e.g. LaTeX).
|
||||||
|
* - Fenced: ```...```
|
||||||
|
* - Inline: `...` (does NOT support nested backticks or multi-backtick syntax)
|
||||||
|
*
|
||||||
|
* Note: This pattern does not handle advanced cases like:
|
||||||
|
* `` `code with `backticks` `` or \\``...\\``
|
||||||
|
*/
|
||||||
|
export const CODE_BLOCK_REGEXP = /(```[\s\S]*?```|`[^`\n]+`)/g;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches LaTeX math delimiters \(...\) and \[...\] only when not preceded by a backslash (i.e., not escaped),
|
||||||
|
* while also capturing code blocks (```, `...`) so they can be skipped during processing.
|
||||||
|
*
|
||||||
|
* Uses negative lookbehind `(?<!\\)` to avoid matching \\( or \\[.
|
||||||
|
* Using the look‑behind pattern `(?<!\\)` we skip matches
|
||||||
|
* that are preceded by a backslash, e.g.
|
||||||
|
* `Definitions\\(also called macros)` (title of chapter 20 in The TeXbook)
|
||||||
|
* or `\\[4pt]` (LaTeX line-break).
|
||||||
|
*
|
||||||
|
* group 1: code-block
|
||||||
|
* group 2: square-bracket
|
||||||
|
* group 3: round-bracket
|
||||||
|
*/
|
||||||
|
export const LATEX_MATH_AND_CODE_PATTERN =
|
||||||
|
/(```[\S\s]*?```|`.*?`)|(?<!\\)\\\[([\S\s]*?[^\\])\\]|(?<!\\)\\\((.*?)\\\)/g;
|
||||||
|
|
||||||
|
/** Regex to capture the content of a $$...\\\\...$$ block (display-formula with line-break) */
|
||||||
|
export const LATEX_LINEBREAK_REGEXP = /\$\$([\s\S]*?\\\\[\s\S]*?)\$\$/;
|
||||||
|
|
||||||
|
/** map from mchem-regexp to replacement */
|
||||||
|
export const MHCHEM_PATTERN_MAP: readonly [RegExp, string][] = [
|
||||||
|
[/(\s)\$\\ce{/g, '$1$\\\\ce{'],
|
||||||
|
[/(\s)\$\\pu{/g, '$1$\\\\pu{']
|
||||||
|
] as const;
|
||||||
355
tools/server/webui/src/lib/utils/latex-protection.test.ts
Normal file
355
tools/server/webui/src/lib/utils/latex-protection.test.ts
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
/* eslint-disable no-irregular-whitespace */
|
||||||
|
import { describe, it, expect, test } from 'vitest';
|
||||||
|
import { maskInlineLaTeX, preprocessLaTeX } from './latex-protection';
|
||||||
|
|
||||||
|
describe('maskInlineLaTeX', () => {
|
||||||
|
it('should protect LaTeX $x + y$ but not money $3.99', () => {
|
||||||
|
const latexExpressions: string[] = [];
|
||||||
|
const input = 'I have $10, $3.99 and $x + y$ and $100x$. The amount is $2,000.';
|
||||||
|
const output = maskInlineLaTeX(input, latexExpressions);
|
||||||
|
|
||||||
|
expect(output).toBe('I have $10, $3.99 and <<LATEX_0>> and <<LATEX_1>>. The amount is $2,000.');
|
||||||
|
expect(latexExpressions).toEqual(['$x + y$', '$100x$']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore money like $5 and $12.99', () => {
|
||||||
|
const latexExpressions: string[] = [];
|
||||||
|
const input = 'Prices are $12.99 and $5. Tax?';
|
||||||
|
const output = maskInlineLaTeX(input, latexExpressions);
|
||||||
|
|
||||||
|
expect(output).toBe('Prices are $12.99 and $5. Tax?');
|
||||||
|
expect(latexExpressions).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should protect inline math $a^2 + b^2$ even after text', () => {
|
||||||
|
const latexExpressions: string[] = [];
|
||||||
|
const input = 'Pythagorean: $a^2 + b^2 = c^2$.';
|
||||||
|
const output = maskInlineLaTeX(input, latexExpressions);
|
||||||
|
|
||||||
|
expect(output).toBe('Pythagorean: <<LATEX_0>>.');
|
||||||
|
expect(latexExpressions).toEqual(['$a^2 + b^2 = c^2$']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not protect math that has letter after closing $ (e.g. units)', () => {
|
||||||
|
const latexExpressions: string[] = [];
|
||||||
|
const input = 'The cost is $99 and change.';
|
||||||
|
const output = maskInlineLaTeX(input, latexExpressions);
|
||||||
|
|
||||||
|
expect(output).toBe('The cost is $99 and change.');
|
||||||
|
expect(latexExpressions).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow $x$ followed by punctuation', () => {
|
||||||
|
const latexExpressions: string[] = [];
|
||||||
|
const input = 'We know $x$, right?';
|
||||||
|
const output = maskInlineLaTeX(input, latexExpressions);
|
||||||
|
|
||||||
|
expect(output).toBe('We know <<LATEX_0>>, right?');
|
||||||
|
expect(latexExpressions).toEqual(['$x$']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work across multiple lines', () => {
|
||||||
|
const latexExpressions: string[] = [];
|
||||||
|
const input = `Emma buys cupcakes for $3 each.\nHow much is $x + y$?`;
|
||||||
|
const output = maskInlineLaTeX(input, latexExpressions);
|
||||||
|
|
||||||
|
expect(output).toBe(`Emma buys cupcakes for $3 each.\nHow much is <<LATEX_0>>?`);
|
||||||
|
expect(latexExpressions).toEqual(['$x + y$']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not protect $100 but protect $matrix$', () => {
|
||||||
|
const latexExpressions: string[] = [];
|
||||||
|
const input = '$100 and $\\mathrm{GL}_2(\\mathbb{F}_7)$ are different.';
|
||||||
|
const output = maskInlineLaTeX(input, latexExpressions);
|
||||||
|
|
||||||
|
expect(output).toBe('$100 and <<LATEX_0>> are different.');
|
||||||
|
expect(latexExpressions).toEqual(['$\\mathrm{GL}_2(\\mathbb{F}_7)$']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip if $ is followed by digit and alphanumeric after close (money)', () => {
|
||||||
|
const latexExpressions: string[] = [];
|
||||||
|
const input = 'I paid $5 quickly.';
|
||||||
|
const output = maskInlineLaTeX(input, latexExpressions);
|
||||||
|
|
||||||
|
expect(output).toBe('I paid $5 quickly.');
|
||||||
|
expect(latexExpressions).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should protect LaTeX even with special chars inside', () => {
|
||||||
|
const latexExpressions: string[] = [];
|
||||||
|
const input = 'Consider $\\alpha_1 + \\beta_2$ now.';
|
||||||
|
const output = maskInlineLaTeX(input, latexExpressions);
|
||||||
|
|
||||||
|
expect(output).toBe('Consider <<LATEX_0>> now.');
|
||||||
|
expect(latexExpressions).toEqual(['$\\alpha_1 + \\beta_2$']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('short text', () => {
|
||||||
|
const latexExpressions: string[] = ['$0$'];
|
||||||
|
const input = '$a$\n$a$ and $b$';
|
||||||
|
const output = maskInlineLaTeX(input, latexExpressions);
|
||||||
|
|
||||||
|
expect(output).toBe('<<LATEX_1>>\n<<LATEX_2>> and <<LATEX_3>>');
|
||||||
|
expect(latexExpressions).toEqual(['$0$', '$a$', '$a$', '$b$']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('empty text', () => {
|
||||||
|
const latexExpressions: string[] = [];
|
||||||
|
const input = '$\n$$\n';
|
||||||
|
const output = maskInlineLaTeX(input, latexExpressions);
|
||||||
|
|
||||||
|
expect(output).toBe('$\n$$\n');
|
||||||
|
expect(latexExpressions).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('LaTeX-spacer preceded by backslash', () => {
|
||||||
|
const latexExpressions: string[] = [];
|
||||||
|
const input = `\\[
|
||||||
|
\\boxed{
|
||||||
|
\\begin{aligned}
|
||||||
|
N_{\\text{att}}^{\\text{(MHA)}} &=
|
||||||
|
h \\bigl[\\, d_{\\text{model}}\\;d_{k} + d_{\\text{model}}\\;d_{v}\\, \\bigr] && (\\text{Q,K,V の重み})\\\\
|
||||||
|
&\\quad+ h(d_{k}+d_{k}+d_{v}) && (\\text{バイアス Q,K,V)}\\\\[4pt]
|
||||||
|
&\\quad+ (h d_{v})\\, d_{\\text{model}} && (\\text{出力射影 }W^{O})\\\\
|
||||||
|
&\\quad+ d_{\\text{model}} && (\\text{バイアス }b^{O})
|
||||||
|
\\end{aligned}}
|
||||||
|
\\]`;
|
||||||
|
const output = maskInlineLaTeX(input, latexExpressions);
|
||||||
|
|
||||||
|
expect(output).toBe(input);
|
||||||
|
expect(latexExpressions).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('preprocessLaTeX', () => {
|
||||||
|
test('converts inline \\( ... \\) to $...$', () => {
|
||||||
|
const input =
|
||||||
|
'\\( \\mathrm{GL}_2(\\mathbb{F}_7) \\): Group of invertible matrices with entries in \\(\\mathbb{F}_7\\).';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
expect(output).toBe(
|
||||||
|
'$ \\mathrm{GL}_2(\\mathbb{F}_7) $: Group of invertible matrices with entries in $\\mathbb{F}_7$.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("don't inline \\\\( ... \\) to $...$", () => {
|
||||||
|
const input =
|
||||||
|
'Chapter 20 of The TeXbook, in source "Definitions\\\\(also called Macros)", containst the formula \\((x_1,\\ldots,x_n)\\).';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
expect(output).toBe(
|
||||||
|
'Chapter 20 of The TeXbook, in source "Definitions\\\\(also called Macros)", containst the formula $(x_1,\\ldots,x_n)$.'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves display math \\[ ... \\] and protects adjacent text', () => {
|
||||||
|
const input = `Some kernel of \\(\\mathrm{SL}_2(\\mathbb{F}_7)\\):
|
||||||
|
\\[
|
||||||
|
\\left\\{ \\begin{pmatrix} 1 & 0 \\\\ 0 & 1 \\end{pmatrix}, \\begin{pmatrix} -1 & 0 \\\\ 0 & -1 \\end{pmatrix} \\right\\} = \\{\\pm I\\}
|
||||||
|
\\]`;
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
expect(output).toBe(`Some kernel of $\\mathrm{SL}_2(\\mathbb{F}_7)$:
|
||||||
|
$$
|
||||||
|
\\left\\{ \\begin{pmatrix} 1 & 0 \\\\ 0 & 1 \\end{pmatrix}, \\begin{pmatrix} -1 & 0 \\\\ 0 & -1 \\end{pmatrix} \\right\\} = \\{\\pm I\\}
|
||||||
|
$$`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles standalone display math equation', () => {
|
||||||
|
const input = `Algebra:
|
||||||
|
\\[
|
||||||
|
x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}
|
||||||
|
\\]`;
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
expect(output).toBe(`Algebra:
|
||||||
|
$$
|
||||||
|
x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}
|
||||||
|
$$`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not interpret currency values as LaTeX', () => {
|
||||||
|
const input = 'I have $10, $3.99 and $x + y$ and $100x$. The amount is $2,000.';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
expect(output).toBe('I have \\$10, \\$3.99 and $x + y$ and $100x$. The amount is \\$2,000.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ignores dollar signs followed by digits (money), but keeps valid math $x + y$', () => {
|
||||||
|
const input = 'I have $10, $3.99 and $x + y$ and $100x$. The amount is $2,000.';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
expect(output).toBe('I have \\$10, \\$3.99 and $x + y$ and $100x$. The amount is \\$2,000.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles real-world word problems with amounts and no math delimiters', () => {
|
||||||
|
const input =
|
||||||
|
'Emma buys 2 cupcakes for $3 each and 1 cookie for $1.50. How much money does she spend in total?';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
expect(output).toBe(
|
||||||
|
'Emma buys 2 cupcakes for \\$3 each and 1 cookie for \\$1.50. How much money does she spend in total?'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles decimal amounts in word problem correctly', () => {
|
||||||
|
const input =
|
||||||
|
'Maria has $20. She buys a notebook for $4.75 and a pack of pencils for $3.25. How much change does she receive?';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
expect(output).toBe(
|
||||||
|
'Maria has \\$20. She buys a notebook for \\$4.75 and a pack of pencils for \\$3.25. How much change does she receive?'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves display math with surrounding non-ASCII text', () => {
|
||||||
|
const input = `1 kg の質量は
|
||||||
|
\\[
|
||||||
|
E = (1\\ \\text{kg}) \\times (3.0 \\times 10^8\\ \\text{m/s})^2 \\approx 9.0 \\times 10^{16}\\ \\text{J}
|
||||||
|
\\]
|
||||||
|
というエネルギーに相当します。これは約 21 百万トンの TNT が爆発したときのエネルギーに匹敵します。`;
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
expect(output).toBe(
|
||||||
|
`1 kg の質量は
|
||||||
|
$$
|
||||||
|
E = (1\\ \\text{kg}) \\times (3.0 \\times 10^8\\ \\text{m/s})^2 \\approx 9.0 \\times 10^{16}\\ \\text{J}
|
||||||
|
$$
|
||||||
|
というエネルギーに相当します。これは約 21 百万トンの TNT が爆発したときのエネルギーに匹敵します。`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('LaTeX-spacer preceded by backslash', () => {
|
||||||
|
const input = `\\[
|
||||||
|
\\boxed{
|
||||||
|
\\begin{aligned}
|
||||||
|
N_{\\text{att}}^{\\text{(MHA)}} &=
|
||||||
|
h \\bigl[\\, d_{\\text{model}}\\;d_{k} + d_{\\text{model}}\\;d_{v}\\, \\bigr] && (\\text{Q,K,V の重み})\\\\
|
||||||
|
&\\quad+ h(d_{k}+d_{k}+d_{v}) && (\\text{バイアス Q,K,V)}\\\\[4pt]
|
||||||
|
&\\quad+ (h d_{v})\\, d_{\\text{model}} && (\\text{出力射影 }W^{O})\\\\
|
||||||
|
&\\quad+ d_{\\text{model}} && (\\text{バイアス }b^{O})
|
||||||
|
\\end{aligned}}
|
||||||
|
\\]`;
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
expect(output).toBe(
|
||||||
|
`$$
|
||||||
|
\\boxed{
|
||||||
|
\\begin{aligned}
|
||||||
|
N_{\\text{att}}^{\\text{(MHA)}} &=
|
||||||
|
h \\bigl[\\, d_{\\text{model}}\\;d_{k} + d_{\\text{model}}\\;d_{v}\\, \\bigr] && (\\text{Q,K,V の重み})\\\\
|
||||||
|
&\\quad+ h(d_{k}+d_{k}+d_{v}) && (\\text{バイアス Q,K,V)}\\\\[4pt]
|
||||||
|
&\\quad+ (h d_{v})\\, d_{\\text{model}} && (\\text{出力射影 }W^{O})\\\\
|
||||||
|
&\\quad+ d_{\\text{model}} && (\\text{バイアス }b^{O})
|
||||||
|
\\end{aligned}}
|
||||||
|
$$`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('converts \\[ ... \\] even when preceded by text without space', () => {
|
||||||
|
const input = 'Some line ...\nAlgebra: \\[x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}\\]';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
expect(output).toBe(
|
||||||
|
'Some line ...\nAlgebra: \n$$x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}$$\n'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('converts \\[ ... \\] in table-cells', () => {
|
||||||
|
const input = `| ID | Expression |\n| #1 | \\[
|
||||||
|
x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}
|
||||||
|
\\] |`;
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
expect(output).toBe(
|
||||||
|
'| ID | Expression |\n| #1 | $x = \\frac{-b \\pm \\sqrt{\\,b^{2}-4ac\\,}}{2a}$ |'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('escapes isolated $ before digits ($5 → \\$5), but not valid math', () => {
|
||||||
|
const input = 'This costs $5 and this is math $x^2$. $100 is money.';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
expect(output).toBe('This costs \\$5 and this is math $x^2$. \\$100 is money.');
|
||||||
|
// Note: Since $x^2$ is detected as valid LaTeX, it's preserved.
|
||||||
|
// $5 becomes \$5 only *after* real math is masked — but here it's correct because the masking logic avoids treating $5 as math.
|
||||||
|
});
|
||||||
|
|
||||||
|
test('display with LaTeX-line-breaks', () => {
|
||||||
|
const input = String.raw`- Algebraic topology, Homotopy Groups of $\mathbb{S}^3$:
|
||||||
|
$$\pi_n(\mathbb{S}^3) = \begin{cases}
|
||||||
|
\mathbb{Z} & n = 3 \\
|
||||||
|
0 & n > 3, n \neq 4 \\
|
||||||
|
\mathbb{Z}_2 & n = 4 \\
|
||||||
|
\end{cases}$$`;
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
// If the formula contains '\\' the $$-delimiters should be in their own line.
|
||||||
|
expect(output).toBe(`- Algebraic topology, Homotopy Groups of $\\mathbb{S}^3$:
|
||||||
|
$$\n\\pi_n(\\mathbb{S}^3) = \\begin{cases}
|
||||||
|
\\mathbb{Z} & n = 3 \\\\
|
||||||
|
0 & n > 3, n \\neq 4 \\\\
|
||||||
|
\\mathbb{Z}_2 & n = 4 \\\\
|
||||||
|
\\end{cases}\n$$`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles mhchem notation safely if present', () => {
|
||||||
|
const input = 'Chemical reaction: \\( \\ce{H2O} \\) and $\\ce{CO2}$';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
expect(output).toBe('Chemical reaction: $ \\ce{H2O} $ and $\\ce{CO2}$');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves code blocks', () => {
|
||||||
|
const input = 'Inline code: `sum $total` and block:\n```\ndollar $amount\n```\nEnd.';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
expect(output).toBe(input); // Code blocks prevent misinterpretation
|
||||||
|
});
|
||||||
|
|
||||||
|
test('escape backslash in mchem ce', () => {
|
||||||
|
const input = 'mchem ce:\n$\\ce{2H2(g) + O2(g) -> 2H2O(l)}$';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
// mhchem-escape would insert a backslash here.
|
||||||
|
expect(output).toBe('mchem ce:\n$\\ce{2H2(g) + O2(g) -> 2H2O(l)}$');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('escape backslash in mchem pu', () => {
|
||||||
|
const input = 'mchem pu:\n$\\pu{-572 kJ mol^{-1}}$';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
// mhchem-escape would insert a backslash here.
|
||||||
|
expect(output).toBe('mchem pu:\n$\\pu{-572 kJ mol^{-1}}$');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('LaTeX in blockquotes with display math', () => {
|
||||||
|
const input =
|
||||||
|
'> **Definition (limit):** \n> \\[\n> \\lim_{x\\to a} f(x) = L\n> \\]\n> means that as \\(x\\) gets close to \\(a\\).';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
// Blockquote markers should be preserved, LaTeX should be converted
|
||||||
|
expect(output).toContain('> **Definition (limit):**');
|
||||||
|
expect(output).toContain('$$');
|
||||||
|
expect(output).toContain('$x$');
|
||||||
|
expect(output).not.toContain('\\[');
|
||||||
|
expect(output).not.toContain('\\]');
|
||||||
|
expect(output).not.toContain('\\(');
|
||||||
|
expect(output).not.toContain('\\)');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('LaTeX in blockquotes with inline math', () => {
|
||||||
|
const input =
|
||||||
|
"> The derivative \\(f'(x)\\) at point \\(x=a\\) measures slope.\n> Formula: \\(f'(a)=\\lim_{h\\to 0}\\frac{f(a+h)-f(a)}{h}\\)";
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
// Blockquote markers should be preserved, inline LaTeX converted to $...$
|
||||||
|
expect(output).toContain("> The derivative $f'(x)$ at point $x=a$ measures slope.");
|
||||||
|
expect(output).toContain("> Formula: $f'(a)=\\lim_{h\\to 0}\\frac{f(a+h)-f(a)}{h}$");
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Mixed content with blockquotes and regular text', () => {
|
||||||
|
const input =
|
||||||
|
'Regular text with \\(x^2\\).\n\n> Quote with \\(y^2\\).\n\nMore text with \\(z^2\\).';
|
||||||
|
const output = preprocessLaTeX(input);
|
||||||
|
|
||||||
|
// All LaTeX should be converted, blockquote markers preserved
|
||||||
|
expect(output).toBe('Regular text with $x^2$.\n\n> Quote with $y^2$.\n\nMore text with $z^2$.');
|
||||||
|
});
|
||||||
|
});
|
||||||
267
tools/server/webui/src/lib/utils/latex-protection.ts
Normal file
267
tools/server/webui/src/lib/utils/latex-protection.ts
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import {
|
||||||
|
CODE_BLOCK_REGEXP,
|
||||||
|
LATEX_MATH_AND_CODE_PATTERN,
|
||||||
|
LATEX_LINEBREAK_REGEXP,
|
||||||
|
MHCHEM_PATTERN_MAP
|
||||||
|
} from '$lib/constants/latex-protection';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces inline LaTeX expressions enclosed in `$...$` with placeholders, avoiding dollar signs
|
||||||
|
* that appear to be part of monetary values or identifiers.
|
||||||
|
*
|
||||||
|
* This function processes the input line by line and skips `$` sequences that are likely
|
||||||
|
* part of money amounts (e.g., `$5`, `$100.99`) or code-like tokens (e.g., `var$`, `$var`).
|
||||||
|
* Valid LaTeX inline math is replaced with a placeholder like `<<LATEX_0>>`, and the
|
||||||
|
* actual LaTeX content is stored in the provided `latexExpressions` array.
|
||||||
|
*
|
||||||
|
* @param content - The input text potentially containing LaTeX expressions.
|
||||||
|
* @param latexExpressions - An array used to collect extracted LaTeX expressions.
|
||||||
|
* @returns The processed string with LaTeX replaced by placeholders.
|
||||||
|
*/
|
||||||
|
export function maskInlineLaTeX(content: string, latexExpressions: string[]): string {
|
||||||
|
if (!content.includes('$')) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return content
|
||||||
|
.split('\n')
|
||||||
|
.map((line) => {
|
||||||
|
if (line.indexOf('$') == -1) {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
|
let processedLine = '';
|
||||||
|
let currentPosition = 0;
|
||||||
|
|
||||||
|
while (currentPosition < line.length) {
|
||||||
|
const openDollarIndex = line.indexOf('$', currentPosition);
|
||||||
|
|
||||||
|
if (openDollarIndex == -1) {
|
||||||
|
processedLine += line.slice(currentPosition);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is there a next $-sign?
|
||||||
|
const closeDollarIndex = line.indexOf('$', openDollarIndex + 1);
|
||||||
|
|
||||||
|
if (closeDollarIndex == -1) {
|
||||||
|
processedLine += line.slice(currentPosition);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const charBeforeOpen = openDollarIndex > 0 ? line[openDollarIndex - 1] : '';
|
||||||
|
const charAfterOpen = line[openDollarIndex + 1];
|
||||||
|
const charBeforeClose =
|
||||||
|
openDollarIndex + 1 < closeDollarIndex ? line[closeDollarIndex - 1] : '';
|
||||||
|
const charAfterClose = closeDollarIndex + 1 < line.length ? line[closeDollarIndex + 1] : '';
|
||||||
|
|
||||||
|
let shouldSkipAsNonLatex = false;
|
||||||
|
|
||||||
|
if (closeDollarIndex == currentPosition + 1) {
|
||||||
|
// No content
|
||||||
|
shouldSkipAsNonLatex = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (/[A-Za-z0-9_$-]/.test(charBeforeOpen)) {
|
||||||
|
// Character, digit, $, _ or - before first '$', no TeX.
|
||||||
|
shouldSkipAsNonLatex = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
/[0-9]/.test(charAfterOpen) &&
|
||||||
|
(/[A-Za-z0-9_$-]/.test(charAfterClose) || ' ' == charBeforeClose)
|
||||||
|
) {
|
||||||
|
// First $ seems to belong to an amount.
|
||||||
|
shouldSkipAsNonLatex = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldSkipAsNonLatex) {
|
||||||
|
processedLine += line.slice(currentPosition, openDollarIndex + 1);
|
||||||
|
currentPosition = openDollarIndex + 1;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Treat as LaTeX
|
||||||
|
processedLine += line.slice(currentPosition, openDollarIndex);
|
||||||
|
const latexContent = line.slice(openDollarIndex, closeDollarIndex + 1);
|
||||||
|
latexExpressions.push(latexContent);
|
||||||
|
processedLine += `<<LATEX_${latexExpressions.length - 1}>>`;
|
||||||
|
currentPosition = closeDollarIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedLine;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeBrackets(text: string): string {
|
||||||
|
return text.replace(
|
||||||
|
LATEX_MATH_AND_CODE_PATTERN,
|
||||||
|
(
|
||||||
|
match: string,
|
||||||
|
codeBlock: string | undefined,
|
||||||
|
squareBracket: string | undefined,
|
||||||
|
roundBracket: string | undefined
|
||||||
|
): string => {
|
||||||
|
if (codeBlock != null) {
|
||||||
|
return codeBlock;
|
||||||
|
} else if (squareBracket != null) {
|
||||||
|
return `$$${squareBracket}$$`;
|
||||||
|
} else if (roundBracket != null) {
|
||||||
|
return `$${roundBracket}$`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Escape $\\ce{...} → $\\ce{...} but with proper handling
|
||||||
|
function escapeMhchem(text: string): string {
|
||||||
|
return MHCHEM_PATTERN_MAP.reduce((result, [pattern, replacement]) => {
|
||||||
|
return result.replace(pattern, replacement);
|
||||||
|
}, text);
|
||||||
|
}
|
||||||
|
|
||||||
|
const doEscapeMhchem = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preprocesses markdown content to safely handle LaTeX math expressions while protecting
|
||||||
|
* against false positives (e.g., dollar amounts like $5.99) and ensuring proper rendering.
|
||||||
|
*
|
||||||
|
* This function:
|
||||||
|
* - Protects code blocks (```) and inline code (`...`)
|
||||||
|
* - Safeguards block and inline LaTeX: \(...\), \[...\], $$...$$, and selective $...$
|
||||||
|
* - Escapes standalone dollar signs before numbers (e.g., $5 → \$5) to prevent misinterpretation
|
||||||
|
* - Restores protected LaTeX and code blocks after processing
|
||||||
|
* - Converts \(...\) → $...$ and \[...\] → $$...$$ for compatibility with math renderers
|
||||||
|
* - Applies additional escaping for brackets and mhchem syntax if needed
|
||||||
|
*
|
||||||
|
* @param content - The raw text (e.g., markdown) that may contain LaTeX or code blocks.
|
||||||
|
* @returns The preprocessed string with properly escaped and normalized LaTeX.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* preprocessLaTeX("Price: $10. The equation is \\(x^2\\).")
|
||||||
|
* // → "Price: $10. The equation is $x^2$."
|
||||||
|
*/
|
||||||
|
export function preprocessLaTeX(content: string): string {
|
||||||
|
// See also:
|
||||||
|
// https://github.com/danny-avila/LibreChat/blob/main/client/src/utils/latex.ts
|
||||||
|
|
||||||
|
// Step 0: Temporarily remove blockquote markers (>) to process LaTeX correctly
|
||||||
|
// Store the structure so we can restore it later
|
||||||
|
const blockquoteMarkers: Map<number, string> = new Map();
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const processedLines = lines.map((line, index) => {
|
||||||
|
const match = line.match(/^(>\s*)/);
|
||||||
|
if (match) {
|
||||||
|
blockquoteMarkers.set(index, match[1]);
|
||||||
|
return line.slice(match[1].length);
|
||||||
|
}
|
||||||
|
return line;
|
||||||
|
});
|
||||||
|
content = processedLines.join('\n');
|
||||||
|
|
||||||
|
// Step 1: Protect code blocks
|
||||||
|
const codeBlocks: string[] = [];
|
||||||
|
|
||||||
|
content = content.replace(CODE_BLOCK_REGEXP, (match) => {
|
||||||
|
codeBlocks.push(match);
|
||||||
|
|
||||||
|
return `<<CODE_BLOCK_${codeBlocks.length - 1}>>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 2: Protect existing LaTeX expressions
|
||||||
|
const latexExpressions: string[] = [];
|
||||||
|
|
||||||
|
// Match \S...\[...\] and protect them and insert a line-break.
|
||||||
|
content = content.replace(/([\S].*?)\\\[([\s\S]*?)\\\](.*)/g, (match, group1, group2, group3) => {
|
||||||
|
// Check if there are characters following the formula (display-formula in a table-cell?)
|
||||||
|
if (group1.endsWith('\\')) {
|
||||||
|
return match; // Backslash before \[, do nothing.
|
||||||
|
}
|
||||||
|
const hasSuffix = /\S/.test(group3);
|
||||||
|
let optBreak;
|
||||||
|
|
||||||
|
if (hasSuffix) {
|
||||||
|
latexExpressions.push(`\\(${group2.trim()}\\)`); // Convert into inline.
|
||||||
|
optBreak = '';
|
||||||
|
} else {
|
||||||
|
latexExpressions.push(`\\[${group2}\\]`);
|
||||||
|
optBreak = '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${group1}${optBreak}<<LATEX_${latexExpressions.length - 1}>>${optBreak}${group3}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Match \(...\), \[...\], $$...$$ and protect them
|
||||||
|
content = content.replace(
|
||||||
|
/(\$\$[\s\S]*?\$\$|(?<!\\)\\\[[\s\S]*?\\\]|(?<!\\)\\\(.*?\\\))/g,
|
||||||
|
(match) => {
|
||||||
|
latexExpressions.push(match);
|
||||||
|
|
||||||
|
return `<<LATEX_${latexExpressions.length - 1}>>`;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Protect inline $...$ but NOT if it looks like money (e.g., $10, $3.99)
|
||||||
|
content = maskInlineLaTeX(content, latexExpressions);
|
||||||
|
|
||||||
|
// Step 3: Escape standalone $ before digits (currency like $5 → \$5)
|
||||||
|
// (Now that inline math is protected, this will only escape dollars not already protected)
|
||||||
|
content = content.replace(/\$(?=\d)/g, '\\$');
|
||||||
|
|
||||||
|
// Step 4: Restore protected LaTeX expressions (they are valid)
|
||||||
|
content = content.replace(/<<LATEX_(\d+)>>/g, (_, index) => {
|
||||||
|
let expr = latexExpressions[parseInt(index)];
|
||||||
|
const match = expr.match(LATEX_LINEBREAK_REGEXP);
|
||||||
|
if (match) {
|
||||||
|
// Katex: The $$-delimiters should be in their own line
|
||||||
|
// if there are \\-line-breaks.
|
||||||
|
const formula = match[1];
|
||||||
|
const prefix = formula.startsWith('\n') ? '' : '\n';
|
||||||
|
const suffix = formula.endsWith('\n') ? '' : '\n';
|
||||||
|
expr = '$$' + prefix + formula + suffix + '$$';
|
||||||
|
}
|
||||||
|
return expr;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 5: Restore code blocks
|
||||||
|
content = content.replace(/<<CODE_BLOCK_(\d+)>>/g, (_, index) => {
|
||||||
|
return codeBlocks[parseInt(index)];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 6: Apply additional escaping functions (brackets and mhchem)
|
||||||
|
content = escapeBrackets(content);
|
||||||
|
|
||||||
|
if (doEscapeMhchem && (content.includes('\\ce{') || content.includes('\\pu{'))) {
|
||||||
|
content = escapeMhchem(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final pass: Convert \(...\) → $...$, \[...\] → $$...$$
|
||||||
|
content = content
|
||||||
|
// Using the look‑behind pattern `(?<!\\)` we skip matches
|
||||||
|
// that are preceded by a backslash, e.g.
|
||||||
|
// `Definitions\\(also called macros)` (title of chapter 20 in The TeXbook).
|
||||||
|
.replace(/(?<!\\)\\\((.+?)\\\)/g, '$$$1$') // inline
|
||||||
|
.replace(
|
||||||
|
// Using the look‑behind pattern `(?<!\\)` we skip matches
|
||||||
|
// that are preceded by a backslash, e.g. `\\[4pt]`.
|
||||||
|
/(?<!\\)\\\[([\s\S]*?)\\\]/g, // display, see also PR #16599
|
||||||
|
(_, prefix: string, content: string) => {
|
||||||
|
return `${prefix}$$${content}$$`;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 7: Restore blockquote markers
|
||||||
|
if (blockquoteMarkers.size > 0) {
|
||||||
|
const finalLines = content.split('\n');
|
||||||
|
const restoredLines = finalLines.map((line, index) => {
|
||||||
|
const marker = blockquoteMarkers.get(index);
|
||||||
|
return marker ? marker + line : line;
|
||||||
|
});
|
||||||
|
content = restoredLines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable no-irregular-whitespace */
|
||||||
// Math Formulas Content
|
// Math Formulas Content
|
||||||
export const MATH_FORMULAS_MD = String.raw`
|
export const MATH_FORMULAS_MD = String.raw`
|
||||||
# Mathematical Formulas and Expressions
|
# Mathematical Formulas and Expressions
|
||||||
@@ -150,6 +151,70 @@ $$\lim_{x \to 0} \frac{\sin x}{x} = 1$$
|
|||||||
|
|
||||||
$$\lim_{n \to \infty} \left(1 + \frac{x}{n}\right)^n = e^x$$
|
$$\lim_{n \to \infty} \left(1 + \frac{x}{n}\right)^n = e^x$$
|
||||||
|
|
||||||
|
## Further Bracket Styles and Amounts
|
||||||
|
|
||||||
|
- \( \mathrm{GL}_2(\mathbb{F}_7) \): Group of invertible matrices with entries in \(\mathbb{F}_7\).
|
||||||
|
- Some kernel of \(\mathrm{SL}_2(\mathbb{F}_7)\):
|
||||||
|
\[
|
||||||
|
\left\{ \begin{pmatrix} 1 & 0 \\ 0 & 1 \end{pmatrix}, \begin{pmatrix} -1 & 0 \\ 0 & -1 \end{pmatrix} \right\} = \{\pm I\}
|
||||||
|
\]
|
||||||
|
- Algebra:
|
||||||
|
\[
|
||||||
|
x = \frac{-b \pm \sqrt{\,b^{2}-4ac\,}}{2a}
|
||||||
|
\]
|
||||||
|
- $100 and $12.99 are amounts, not LaTeX.
|
||||||
|
- I have $10, $3.99 and $x + y$ and $100x$. The amount is $2,000.
|
||||||
|
- Emma buys 2 cupcakes for $3 each and 1 cookie for $1.50. How much money does she spend in total?
|
||||||
|
- Maria has $20. She buys a notebook for $4.75 and a pack of pencils for $3.25. How much change does she receive?
|
||||||
|
- 1 kg の質量は
|
||||||
|
\[
|
||||||
|
E = (1\ \text{kg}) \times (3.0 \times 10^8\ \text{m/s})^2 \approx 9.0 \times 10^{16}\ \text{J}
|
||||||
|
\]
|
||||||
|
というエネルギーに相当します。これは約 21 百万トンの TNT が爆発したときのエネルギーに匹敵します。
|
||||||
|
- Algebra: \[
|
||||||
|
x = \frac{-b \pm \sqrt{\,b^{2}-4ac\,}}{2a}
|
||||||
|
\]
|
||||||
|
- Algebraic topology, Homotopy Groups of $\mathbb{S}^3$:
|
||||||
|
$$\pi_n(\mathbb{S}^3) = \begin{cases}
|
||||||
|
\mathbb{Z} & n = 3 \\
|
||||||
|
0 & n > 3, n \neq 4 \\
|
||||||
|
\mathbb{Z}_2 & n = 4 \\
|
||||||
|
\end{cases}$$
|
||||||
|
- Spacer preceded by backslash:
|
||||||
|
\[
|
||||||
|
\boxed{
|
||||||
|
\begin{aligned}
|
||||||
|
N_{\text{att}}^{\text{(MHA)}} &=
|
||||||
|
h \bigl[\, d_{\text{model}}\;d_{k} + d_{\text{model}}\;d_{v}\, \bigr] && (\text{Q,K,V の重み})\\
|
||||||
|
&\quad+ h(d_{k}+d_{k}+d_{v}) && (\text{バイアス Q,K,V)}\\[4pt]
|
||||||
|
&\quad+ (h d_{v})\, d_{\text{model}} && (\text{出力射影 }W^{O})\\
|
||||||
|
&\quad+ d_{\text{model}} && (\text{バイアス }b^{O})
|
||||||
|
\end{aligned}}
|
||||||
|
\]
|
||||||
|
|
||||||
|
## Formulas in a Table
|
||||||
|
|
||||||
|
| Area | Expression | Comment |
|
||||||
|
|------|------------|---------|
|
||||||
|
| **Algebra** | \[
|
||||||
|
x = \frac{-b \pm \sqrt{\,b^{2}-4ac\,}}{2a}
|
||||||
|
\] | Quadratic formula |
|
||||||
|
| | \[
|
||||||
|
(a+b)^{n} = \sum_{k=0}^{n}\binom{n}{k}\,a^{\,n-k}\,b^{\,k}
|
||||||
|
\] | Binomial theorem |
|
||||||
|
| | \(\displaystyle \prod_{k=1}^{n}k = n! \) | Factorial definition |
|
||||||
|
| **Geometry** | \( \mathbf{a}\cdot \mathbf{b} = \|\mathbf{a}\|\,\|\mathbf{b}\|\,\cos\theta \) | Dot product & angle |
|
||||||
|
|
||||||
|
## No math (but chemical)
|
||||||
|
|
||||||
|
Balanced chemical reaction with states:
|
||||||
|
|
||||||
|
\[
|
||||||
|
\ce{2H2(g) + O2(g) -> 2H2O(l)}
|
||||||
|
\]
|
||||||
|
|
||||||
|
The standard enthalpy change for the reaction is: $\Delta H^\circ = \pu{-572 kJ mol^{-1}}$.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*This document showcases various mathematical notation and formulas that can be rendered in markdown using LaTeX syntax.*
|
*This document showcases various mathematical notation and formulas that can be rendered in markdown using LaTeX syntax.*
|
||||||
|
|||||||
13
tools/server/webui/src/styles/katex-custom.scss
Normal file
13
tools/server/webui/src/styles/katex-custom.scss
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Override KaTeX SCSS variables to disable ttf and woff fonts
|
||||||
|
// Only use woff2 format which is embedded in the bundle
|
||||||
|
$use-woff2: true;
|
||||||
|
$use-woff: false;
|
||||||
|
$use-ttf: false;
|
||||||
|
|
||||||
|
// Use Vite alias for font folder
|
||||||
|
$font-folder: 'katex-fonts';
|
||||||
|
|
||||||
|
// Import KaTeX SCSS with overridden variables
|
||||||
|
// Note: @import is deprecated but required because KaTeX uses @import internally
|
||||||
|
// The deprecation warnings are from KaTeX's code and cannot be avoided
|
||||||
|
@import 'katex/src/styles/katex.scss';
|
||||||
@@ -22,6 +22,9 @@ const config = {
|
|||||||
}),
|
}),
|
||||||
output: {
|
output: {
|
||||||
bundleStrategy: 'inline'
|
bundleStrategy: 'inline'
|
||||||
|
},
|
||||||
|
alias: {
|
||||||
|
$styles: 'src/styles'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ const GUIDE_FOR_FRONTEND = `
|
|||||||
|
|
||||||
const MAX_BUNDLE_SIZE = 2 * 1024 * 1024;
|
const MAX_BUNDLE_SIZE = 2 * 1024 * 1024;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the maximum size of an embedded asset in bytes,
|
||||||
|
* e.g. maximum size of embedded font (see node_modules/katex/dist/fonts/*.woff2)
|
||||||
|
*/
|
||||||
|
const MAX_ASSET_SIZE = 32000;
|
||||||
|
|
||||||
|
/** public/index.html.gz minified flag */
|
||||||
|
const ENABLE_JS_MINIFICATION = true;
|
||||||
|
|
||||||
function llamaCppBuildPlugin() {
|
function llamaCppBuildPlugin() {
|
||||||
return {
|
return {
|
||||||
name: 'llamacpp:build',
|
name: 'llamacpp:build',
|
||||||
@@ -75,12 +84,28 @@ function llamaCppBuildPlugin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
build: {
|
resolve: {
|
||||||
chunkSizeWarningLimit: 3072
|
alias: {
|
||||||
|
'katex-fonts': resolve('node_modules/katex/dist/fonts')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
assetsInlineLimit: MAX_ASSET_SIZE,
|
||||||
|
chunkSizeWarningLimit: 3072,
|
||||||
|
minify: ENABLE_JS_MINIFICATION
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
additionalData: `
|
||||||
|
$use-woff2: true;
|
||||||
|
$use-woff: false;
|
||||||
|
$use-ttf: false;
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
plugins: [tailwindcss(), sveltekit(), devtoolsJson(), llamaCppBuildPlugin()],
|
plugins: [tailwindcss(), sveltekit(), devtoolsJson(), llamaCppBuildPlugin()],
|
||||||
|
|
||||||
test: {
|
test: {
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user