CODEBASE: Generate display data for math notation at built time and remove runtime mathjax dependency (#2447)

This commit is contained in:
catloversg
2026-01-18 05:41:24 +07:00
committed by GitHub
parent be16b2a375
commit c8e3eb2050
25 changed files with 910 additions and 584 deletions

View File

@@ -19,5 +19,7 @@ tsdoc-metadata.json
.git_blame_ignore_revs .git_blame_ignore_revs
# This file is generated by tools/bundle-doc/index.js # This file is generated by tools/bundle-doc/index.mjs
src/Documentation/pages.ts src/Documentation/pages.ts
# This file is generated by tools/bundle-doc/generate-math-notation-output.mjs
src/Documentation/data/MathNotationOutput.json

806
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,11 +27,12 @@
"ajv": "^8.17.1", "ajv": "^8.17.1",
"arg": "^5.0.2", "arg": "^5.0.2",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"better-react-mathjax": "^2.0.3",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"convert-source-map": "^2.0.0", "convert-source-map": "^2.0.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"fast-dice-coefficient": "1.0.3", "fast-dice-coefficient": "1.0.3",
"hast-util-from-dom": "^5.0.1",
"hast-util-to-text": "^4.0.2",
"jszip": "^3.10.1", "jszip": "^3.10.1",
"material-ui-color": "^1.2.0", "material-ui-color": "^1.2.0",
"material-ui-popup-state": "^1.9.3", "material-ui-popup-state": "^1.9.3",
@@ -45,9 +46,12 @@
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-resizable": "^3.0.5", "react-resizable": "^3.0.5",
"react-syntax-highlighter": "^15.6.6", "react-syntax-highlighter": "^15.6.6",
"rehype-raw": "^6.1.1",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"sprintf-js": "^1.1.3", "sprintf-js": "^1.1.3",
"tss-react": "^4.9.19" "tss-react": "^4.9.19",
"unist-util-visit-parents": "^5.1.3"
}, },
"description": "A cyberpunk-themed incremental game", "description": "A cyberpunk-themed incremental game",
"devDependencies": { "devDependencies": {
@@ -56,6 +60,7 @@
"@babel/preset-react": "^7.27.1", "@babel/preset-react": "^7.27.1",
"@babel/preset-typescript": "^7.27.1", "@babel/preset-typescript": "^7.27.1",
"@electron/packager": "^18.4.4", "@electron/packager": "^18.4.4",
"@mathjax/src": "^4.1.0",
"@microsoft/api-documenter": "^7.26.34", "@microsoft/api-documenter": "^7.26.34",
"@microsoft/api-extractor": "^7.52.13", "@microsoft/api-extractor": "^7.52.13",
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.1", "@pmmmwh/react-refresh-webpack-plugin": "^0.6.1",
@@ -75,7 +80,6 @@
"babel-jest": "^29.7.0", "babel-jest": "^29.7.0",
"babel-loader": "^10.0.0", "babel-loader": "^10.0.0",
"babel-plugin-transform-barrels": "^1.0.17", "babel-plugin-transform-barrels": "^1.0.17",
"copy-webpack-plugin": "^13.0.0",
"css-loader": "^7.1.2", "css-loader": "^7.1.2",
"csstype": "3.1.3", "csstype": "3.1.3",
"electron": "^38.4.0", "electron": "^38.4.0",
@@ -88,19 +92,16 @@
"jest-environment-jsdom": "^29.7.0", "jest-environment-jsdom": "^29.7.0",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mathjax-full": "^3.2.2",
"monaco-editor": "^0.55.1", "monaco-editor": "^0.55.1",
"monaco-editor-webpack-plugin": "^7.1.1", "monaco-editor-webpack-plugin": "^7.1.1",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"react-refresh": "^0.18.0", "react-refresh": "^0.18.0",
"rehype-mathjax": "^4.0.3",
"rehype-raw": "^6.1.1",
"remark-math": "^5.1.1",
"style-loader": "^4.0.0", "style-loader": "^4.0.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"webpack": "^5.100.2", "webpack": "^5.100.2",
"webpack-cli": "^6.0.1", "webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.2" "webpack-dev-server": "^5.2.2",
"xml-formatter": "^3.6.7"
}, },
"overrides": { "overrides": {
"react-syntax-highlighter": { "react-syntax-highlighter": {
@@ -123,8 +124,8 @@
"start:dev": "webpack serve --progress --mode development", "start:dev": "webpack serve --progress --mode development",
"build": "bash ./tools/build.sh production", "build": "bash ./tools/build.sh production",
"build:dev": "bash ./tools/build.sh development", "build:dev": "bash ./tools/build.sh development",
"lint": "eslint --fix --ext js,jsx,ts,tsx --max-warnings 0 src test", "lint": "eslint --fix --ext js,mjs,jsx,ts,mts,tsx --max-warnings 0 src test",
"lint:report": "eslint --ext js,jsx,ts,tsx --max-warnings 0 src test", "lint:report": "eslint --ext js,mjs,jsx,ts,mts,tsx --max-warnings 0 src test",
"preinstall": "node ./tools/engines-check/engines-check.js", "preinstall": "node ./tools/engines-check/engines-check.js",
"test": "jest", "test": "jest",
"test:watch": "jest --watch", "test:watch": "jest --watch",

View File

@@ -28,6 +28,7 @@ export const CONSTANTS = {
// Faction and Company favor-related things // Faction and Company favor-related things
BaseFavorToDonate: 150, BaseFavorToDonate: 150,
// If we change this constant, we must update the "RepDonation" value in src/Documentation/data/MathNotation.json
DonateMoneyToRepDivisor: 1e6, DonateMoneyToRepDivisor: 1e6,
// NeuroFlux Governor Augmentation cost multiplier // NeuroFlux Governor Augmentation cost multiplier

View File

@@ -1,7 +1,6 @@
// React Component for displaying an Division's overview information // React Component for displaying an Division's overview information
// (top-left panel in the Division UI) // (top-left panel in the Division UI)
import React, { useState } from "react"; import React, { useState } from "react";
import { MathJax } from "better-react-mathjax";
import { IndustryType } from "@enums"; import { IndustryType } from "@enums";
import { hireAdVert } from "../Actions"; import { hireAdVert } from "../Actions";
@@ -23,6 +22,8 @@ import Paper from "@mui/material/Paper";
import IconButton from "@mui/material/IconButton"; import IconButton from "@mui/material/IconButton";
import HelpIcon from "@mui/icons-material/Help"; import HelpIcon from "@mui/icons-material/Help";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import MathNotation from "../../Documentation/data/MathNotation.json";
import { MathNotationOutput } from "../../Documentation/ui/MathNotationOutput";
function MakeProductButton(): React.ReactElement { function MakeProductButton(): React.ReactElement {
const corp = useCorporation(); const corp = useCorporation();
@@ -132,8 +133,11 @@ export function DivisionOverview(props: DivisionOverviewProps): React.ReactEleme
<> <>
<Typography>Multiplier for this industry's sales due to its awareness and popularity.</Typography> <Typography>Multiplier for this industry's sales due to its awareness and popularity.</Typography>
<br /> <br />
<MathJax>{`\\(\\text{${division.industry} Industry: }\\alpha = ${division.advertisingFactor}\\)`}</MathJax> <Typography>
<MathJax>{`\\(\\text{multiplier} = \\left((\\text{awareness}+1)^{\\alpha} \\times (\\text{popularity}+1)^{\\alpha} \\times \\frac{\\text{popularity}+0.001}{\\text{awareness}}\\right)^{0.85}\\)`}</MathJax> {division.industry} Industry: 𝞪 = {division.advertisingFactor}
</Typography>
<br />
<MathNotationOutput notation={MathNotation.CorpAdvertFactor} />
<br /> <br />
<StatsTable <StatsTable
rows={[ rows={[

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { Division } from "../Division"; import { Division } from "../Division";
import { MathJax } from "better-react-mathjax";
import { getRecordEntries } from "../../Types/Record"; import { getRecordEntries } from "../../Types/Record";
import Typography from "@mui/material/Typography";
interface IProps { interface IProps {
division: Division; division: Division;
@@ -10,13 +10,19 @@ interface IProps {
export function IndustryProductEquation(props: IProps): React.ReactElement { export function IndustryProductEquation(props: IProps): React.ReactElement {
const reqs = []; const reqs = [];
for (const [reqMat, reqAmt] of getRecordEntries(props.division.requiredMaterials)) { for (const [reqMat, reqAmt] of getRecordEntries(props.division.requiredMaterials)) {
if (!reqAmt) continue; if (!reqAmt) {
reqs.push(String.raw`${reqAmt}\;\textit{${reqMat}}`); continue;
}
reqs.push(`${reqAmt} ${reqMat}`);
} }
const prod = props.division.producedMaterials.map((p) => `1\\;\\textit{${p}}`); const prod = props.division.producedMaterials.map((materialName) => `1 ${materialName}`);
if (props.division.makesProducts) { if (props.division.makesProducts) {
prod.push("\\textit{Products}"); prod.push("Products");
} }
return <MathJax>{"\\(" + reqs.join("+") + `\\Rightarrow ` + prod.join("+") + "\\)"}</MathJax>; return (
<Typography component="span">
{reqs.join(" + ")} {prod.join(" + ")}
</Typography>
);
} }

View File

@@ -1,6 +1,5 @@
// React Component for displaying Corporation Overview info // React Component for displaying Corporation Overview info
import React, { useState } from "react"; import React, { useState } from "react";
import { MathJax } from "better-react-mathjax";
import { LevelableUpgrade } from "./LevelableUpgrade"; import { LevelableUpgrade } from "./LevelableUpgrade";
import { Unlock } from "./Unlock"; import { Unlock } from "./Unlock";
import { BribeFactionModal } from "./modals/BribeFactionModal"; import { BribeFactionModal } from "./modals/BribeFactionModal";
@@ -36,6 +35,8 @@ import { ButtonWithTooltip } from "../../ui/Components/ButtonWithTooltip";
import { CreateCorporationModal } from "./modals/CreateCorporationModal"; import { CreateCorporationModal } from "./modals/CreateCorporationModal";
import InfoIcon from "@mui/icons-material/Info"; import InfoIcon from "@mui/icons-material/Info";
import { CorruptibleText } from "../../ui/React/CorruptibleText"; import { CorruptibleText } from "../../ui/React/CorruptibleText";
import MathNotation from "../../Documentation/data/MathNotation.json";
import { MathNotationOutput } from "../../Documentation/ui/MathNotationOutput";
interface IProps { interface IProps {
rerender: () => void; rerender: () => void;
@@ -390,9 +391,9 @@ function DividendsStats({ profit }: IDividendsStatsProps): React.ReactElement {
"TributeModifier". Formulas: "TributeModifier". Formulas:
<br /> <br />
<br /> <br />
<MathJax>{`\\(TotalDividends = DividendRate\\ast(Revenue - Expenses)\\ast 10\\)`}</MathJax> <MathNotationOutput notation={MathNotation.CorpTotalDividends} />
<br /> <br />
<MathJax>{`\\(Dividend = \\left(OwnedShares\\ast\\frac{TotalDividends}{TotalShares}\\right)^{1 - TributeModifier}\\)`}</MathJax> <MathNotationOutput notation={MathNotation.CorpDividend} />
</> </>
} }
> >

View File

@@ -0,0 +1,10 @@
{
"RepDonation": "reputation = \\frac{\\text{donation amount} \\cdot \\text{reputation multiplier}}{10^{6}}",
"CoreCost": "\\large{cost = 10^9 \\cdot 7.5 ^{\\text{cores}}}",
"HomeRAMCost": "\\large{cost = ram \\cdot 3.2 \\cdot 10^4 \\cdot 1.58^{log_2{(ram)}}} \\cdot HomeRamCostMult",
"FavorBonus": "\\huge{\\Delta r = \\Delta r \\times \\frac{100+favor}{100}}",
"RepToFavor": "\\huge{favor=\\log_{1.02}\\left(1+\\frac{r}{25000}\\right)}",
"CorpTotalDividends": "TotalDividends = DividendRate\\ast(Revenue - Expenses)\\ast 10",
"CorpDividend": "Dividend = \\left(OwnedShares\\ast\\frac{TotalDividends}{TotalShares}\\right)^{1 - TributeModifier}",
"CorpAdvertFactor": "\\text{multiplier} = \\left((\\text{awareness}+1)^{\\alpha} \\times (\\text{popularity}+1)^{\\alpha} \\times \\frac{\\text{popularity}+0.001}{\\text{awareness}}\\right)^{0.85}"
}

File diff suppressed because one or more lines are too long

View File

@@ -1,5 +1,6 @@
import { AllPages } from "./pages"; import { AllPages } from "./pages";
import { EventEmitter } from "../utils/EventEmitter"; import { EventEmitter } from "../utils/EventEmitter";
import MathNotationOutput from "./data/MathNotationOutput.json";
export const resolvePage = (title: string): { pageName: string | null; pageContent: string } => { export const resolvePage = (title: string): { pageName: string | null; pageContent: string } => {
const lang = new Intl.Locale(navigator.language).language; const lang = new Intl.Locale(navigator.language).language;
@@ -35,3 +36,13 @@ export const DocumentationPopUpEvents = new EventEmitter<[string | undefined]>()
export function openDocumentationPopUp(path: string): void { export function openDocumentationPopUp(path: string): void {
DocumentationPopUpEvents.emit(path); DocumentationPopUpEvents.emit(path);
} }
export function convertMathNotation(value: string): string {
// MathNotationOutput is imported as json data, so we need to typecast here to access the value via string index
// easier without fighting with the TS compiler.
const output = (MathNotationOutput as Record<string, string>)[value];
if (output == null) {
throw new Error(`Unknown math notation: ${value}`);
}
return output;
}

View File

@@ -0,0 +1,9 @@
import React from "react";
import Typography from "@mui/material/Typography";
import { convertMathNotation } from "../root";
export function MathNotationOutput({ notation }: { notation: string }): JSX.Element {
// It's fine to use dangerouslySetInnerHTML here. We control the data in both MathNotation.json and
// MathNotationOutput.json. They are not user-provided data.
return <Typography dangerouslySetInnerHTML={{ __html: convertMathNotation(notation) }} />;
}

View File

@@ -1,6 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { CONSTANTS } from "../../Constants";
import { Faction } from "../Faction"; import { Faction } from "../Faction";
import { Player } from "@player"; import { Player } from "@player";
import { canDonate, donate, repFromDonation } from "../formulas/donation"; import { canDonate, donate, repFromDonation } from "../formulas/donation";
@@ -10,12 +9,13 @@ import { Money } from "../../ui/React/Money";
import { Reputation } from "../../ui/React/Reputation"; import { Reputation } from "../../ui/React/Reputation";
import { dialogBoxCreate } from "../../ui/React/DialogBox"; import { dialogBoxCreate } from "../../ui/React/DialogBox";
import { MathJax } from "better-react-mathjax";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import Button from "@mui/material/Button"; import Button from "@mui/material/Button";
import { NumberInput } from "../../ui/React/NumberInput"; import { NumberInput } from "../../ui/React/NumberInput";
import MathNotation from "../../Documentation/data/MathNotation.json";
import { MathNotationOutput } from "../../Documentation/ui/MathNotationOutput";
type DonateOptionProps = { type DonateOptionProps = {
faction: Faction; faction: Faction;
@@ -27,7 +27,6 @@ type DonateOptionProps = {
/** React component for a donate option on the Faction UI */ /** React component for a donate option on the Faction UI */
export function DonateOption({ faction, favorToDonate, disabled, rerender }: DonateOptionProps): React.ReactElement { export function DonateOption({ faction, favorToDonate, disabled, rerender }: DonateOptionProps): React.ReactElement {
const [donateAmt, setDonateAmt] = useState<number>(NaN); const [donateAmt, setDonateAmt] = useState<number>(NaN);
const digits = (CONSTANTS.DonateMoneyToRepDivisor + "").length - 1;
function onDonate(): void { function onDonate(): void {
const repGain = donate(donateAmt, faction); const repGain = donate(donateAmt, faction);
@@ -76,9 +75,7 @@ export function DonateOption({ faction, favorToDonate, disabled, rerender }: Don
), ),
}} }}
/> />
<Typography> <MathNotationOutput notation={MathNotation.RepDonation} />
<MathJax>{`\\(reputation = \\frac{\\text{donation amount} \\cdot \\text{reputation multiplier}}{10^{${digits}}}\\)`}</MathJax>
</Typography>
</> </>
)} )}
</Paper> </Paper>

View File

@@ -6,7 +6,8 @@ import Typography from "@mui/material/Typography";
import { Player } from "@player"; import { Player } from "@player";
import { Money } from "../../ui/React/Money"; import { Money } from "../../ui/React/Money";
import { MathJax } from "better-react-mathjax"; import MathNotation from "../../Documentation/data/MathNotation.json";
import { MathNotationOutput } from "../../Documentation/ui/MathNotationOutput";
interface IProps { interface IProps {
rerender: () => void; rerender: () => void;
@@ -32,7 +33,7 @@ export function CoresButton(props: IProps): React.ReactElement {
} }
return ( return (
<Tooltip title={<MathJax>{`\\(\\large{cost = 10^9 \\cdot 7.5 ^{\\text{cores}}}\\)`}</MathJax>}> <Tooltip title={<MathNotationOutput notation={MathNotation.CoreCost} />}>
<span> <span>
<br /> <br />
<Typography> <Typography>

View File

@@ -9,9 +9,10 @@ import { purchaseRamForHomeComputer } from "../../Server/ServerPurchases";
import { Money } from "../../ui/React/Money"; import { Money } from "../../ui/React/Money";
import { formatRam } from "../../ui/formatNumber"; import { formatRam } from "../../ui/formatNumber";
import { MathJax } from "better-react-mathjax";
import { currentNodeMults } from "../../BitNode/BitNodeMultipliers"; import { currentNodeMults } from "../../BitNode/BitNodeMultipliers";
import { ServerConstants } from "../../Server/data/Constants"; import { ServerConstants } from "../../Server/data/Constants";
import MathNotation from "../../Documentation/data/MathNotation.json";
import { MathNotationOutput } from "../../Documentation/ui/MathNotationOutput";
interface IProps { interface IProps {
rerender: () => void; rerender: () => void;
@@ -30,12 +31,13 @@ export function RamButton(props: IProps): React.ReactElement {
props.rerender(); props.rerender();
} }
const bnMult = currentNodeMults.HomeComputerRamCost === 1 ? "" : `\\cdot ${currentNodeMults.HomeComputerRamCost}`;
return ( return (
<Tooltip <Tooltip
title={ title={
<MathJax>{`\\(\\large{cost = ram \\cdot 3.2 \\cdot 10^4 \\cdot 1.58^{log_2{(ram)}}} ${bnMult}\\)`}</MathJax> <>
<Typography>HomeRamCostMult = {currentNodeMults.HomeComputerRamCost}</Typography>
<MathNotationOutput notation={MathNotation.HomeRAMCost} />
</>
} }
> >
<span> <span>

6
src/ThirdParty/RehypePlugin.d.mts vendored Normal file
View File

@@ -0,0 +1,6 @@
// Minimal type information
export declare function createPlugin(
createRenderer: () => {
render: (value: string, { display }: { display: boolean }) => any[];
},
): () => (tree: any, file: any) => void;

343
src/ThirdParty/RehypePlugin.mjs vendored Normal file
View File

@@ -0,0 +1,343 @@
/**
* We need to use a custom rehype plugin similar to rehype-mathjax. Specifically, we need to implement a function
* similar to "createPlugin" in that library. That library depends on mathjax, which is what we want to get rid of at
* runtime, so we copy only "create-plugin.js" from that library.
*
* The source code is retrieved from https://raw.githubusercontent.com/remarkjs/remark-math/76c14978ff6805011b8c56727c54104a511b9055/packages/rehype-mathjax/lib/create-plugin.js
* The license is retrieved from https://raw.githubusercontent.com/remarkjs/remark-math/76c14978ff6805011b8c56727c54104a511b9055/license
*/
/**
(The MIT License)
Copyright (c) Junyoung Choi <fluke8259@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
/**
* @import {ElementContent, Element, Root} from 'hast'
* @import {VFile} from 'vfile'
*/
/**
* @callback CreateRenderer
* Create a renderer.
* @param {Readonly<Options>} options
* Configuration.
* @returns {Renderer}
* Rendeder.
*
* @callback FormatError
* Format an error.
* @param {any} jax
* MathJax object.
* @param {any} error
* Error.
* @returns {string}
* Formatted error.
*
* @typedef InputTexOptions
* Configuration for input tex math.
* <http://docs.mathjax.org/en/latest/options/input/tex.html#the-configuration-block>
* @property {string | null | undefined} [baseURL]
* URL for use with links to tags, when there is a `<base>` tag in effect
* (optional).
* @property {RegExp | null | undefined} [digits]
* Pattern for recognizing numbers (optional).
* @property {ReadonlyArray<MathNotation> | null | undefined} [displayMath]
* Start/end delimiter pairs for display math (optional).
* @property {FormatError | null | undefined} [formatError]
* Function called when TeX syntax errors occur (optional).
* @property {ReadonlyArray<MathNotation> | null | undefined} [inlineMath]
* Start/end delimiter pairs for in-line math (optional).
* @property {number | null | undefined} [maxBuffer]
* Max size for the internal TeX string (5K) (optional).
* @property {number | null | undefined} [maxMacros]
* Max number of macro substitutions per expression (optional).
* @property {ReadonlyArray<string> | null | undefined} [packages]
* Extensions to use (optional).
* @property {boolean | null | undefined} [processEnvironments]
* Process `\begin{xxx}...\end{xxx}` outside math mode (optional).
* @property {boolean | null | undefined} [processEscapes]
* Use `\$` to produce a literal dollar sign (optional).
* @property {boolean | null | undefined} [processRefs]
* Process `\ref{...}` outside of math mode (optional).
* @property {string | null | undefined} [tagIndent]
* Amount to indent tags (optional).
* @property {'left' | 'right' | null | undefined} [tagSide]
* Side for `\tag` macros (optional).
* @property {'all' | 'ams' | 'none' | null | undefined} [tags]
* Optional.
* @property {boolean | null | undefined} [useLabelIds]
* Use label name rather than tag for ids (optional).
*
* @typedef {[open: string, close: string]} MathNotation
* Markers to use for math.
* See: <http://docs.mathjax.org/en/latest/options/input/tex.html#the-configuration-block>
*
* @typedef Options
* Configuration.
*
* ###### Notes
*
* When using `rehype-mathjax/browser`, only `options.tex.displayMath` and
* `options.tex.inlineMath` are used.
* That plugin will use the first delimiter pair in those fields to wrap
* math.
* Then you need to load MathJax yourself on the client and start it with the
* same markers.
* You can pass other options on the client.
*
* When using `rehype-mathjax/chtml`, `options.chtml.fontURL` is required.
* For example:
*
* ```js
* // …
* .use(rehypeMathjaxChtml, {
* chtml: {
* fontURL: 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/output/chtml/fonts/woff-v2'
* }
* })
* // …
* ```
* @property {Readonly<OutputChtmlOptions> | null | undefined} [chtml]
* Configuration for the output, when CHTML (optional).
* @property {Readonly<OutputSvgOptions> | null | undefined} [svg]
* Configuration for the output, when SVG (optional).
* @property {Readonly<InputTexOptions> | null | undefined} [tex]
* Configuration for the input TeX (optional).
*
* @typedef OutputChtmlOptions
* Configuration for output CHTML.
* <http://docs.mathjax.org/en/latest/options/output/chtml.html#the-configuration-block>
* @property {boolean | null | undefined} [adaptiveCSS]
* `true` means only produce CSS that is used in the processed equations (optional).
* @property {'center' | 'left' | 'right' | null | undefined} [displayAlign]
* Default for indentalign when set to `'auto'` (optional).
* @property {string | null | undefined} [displayIndent]
* Default for indentshift when set to `'auto'` (optional).
* @property {number | null | undefined} [exFactor]
* Default size of ex in em units (optional).
* @property {string} fontURL
* The URL where the fonts are found (**required**).
* @property {boolean | null | undefined} [matchFontHeight]
* `true` to match ex-height of surrounding font (optional).
* @property {boolean | null | undefined} [mathmlSpacing]
* `true` for MathML spacing rules, false for TeX rules (optional).
* @property {boolean | null | undefined} [merrorInheritFont]
* `true` to make merror text use surrounding font (optional).
* @property {number | null | undefined} [minScale]
* Smallest scaling factor to use (optional).
* @property {boolean | null | undefined} [mtextInheritFont]
* `true` to make mtext elements use surrounding font (optional).
* @property {number | null | undefined} [scale]
* Global scaling factor for all expressions (optional).
* @property {Readonly<Record<string, boolean>> | null | undefined} [skipAttributes]
* RFDa and other attributes NOT to copy to the output (optional).
*
* @typedef OutputSvgOptions
* Configuration for output SVG.
* <http://docs.mathjax.org/en/latest/options/output/svg.html#the-configuration-block>
* @property {'center' | 'left' | 'right' | null | undefined} [displayAlign]
* Default for indentalign when set to `'auto'` (optional).
* @property {string | null | undefined} [displayIndent]
* Default for indentshift when set to `'auto'` (optional).
* @property {number | null | undefined} [exFactor]
* Default size of ex in em units (optional).
* @property {'global' | 'local' | 'none' | null | undefined} [fontCache]
* Or `'global'` or `'none'` (optional).
* @property {boolean | null | undefined} [internalSpeechTitles]
* Insert `<title>` tags with speech content (optional).
* @property {string | null | undefined} [localID]
* ID to use for local font cache, for single equation processing (optional).
* @property {boolean | null | undefined} [mathmlSpacing]
* `true` for MathML spacing rules, `false` for TeX rules (optional).
* @property {boolean | null | undefined} [merrorInheritFont]
* `true` to make merror text use surrounding font (optional).
* @property {number | null | undefined} [minScale]
* Smallest scaling factor to use (optional).
* @property {boolean | null | undefined} [mtextInheritFont]
* `true` to make mtext elements use surrounding font (optional).
* @property {number | null | undefined} [scale]
* Global scaling factor for all expressions (optional).
* @property {Readonly<Record<string, boolean>> | null | undefined} [skipAttributes]
* RFDa and other attributes *not* to copy to the output (optional).
* @property {number | null | undefined} [titleID]
* Initial ID number to use for `aria-labeledby` titles (optional).
*
* @callback Render
* Render a math node.
* @param {string} value
* Math value.
* @param {Readonly<RenderOptions>} options
* Configuration.
* @returns {Array<ElementContent>}
* Content.
*
* @typedef RenderOptions
* Configuration.
* @property {boolean} display
* Whether to render display math.
*
* @typedef Renderer
* Renderer.
* @property {(() => undefined) | undefined} [register]
* Called before transform.
* @property {(() => undefined) | undefined} [unregister]
* Called after transform.
* @property {Render} render
* Render a math node.
* @property {StyleSheet | undefined} [styleSheet]
* Render a style sheet (optional).
*
* @callback StyleSheet
* Render a style sheet.
* @returns {Element}
* Style sheet.
*/
import { toText } from "hast-util-to-text";
import { SKIP, visitParents } from "unist-util-visit-parents";
/** @type {Readonly<Options>} */
const emptyOptions = {};
/** @type {ReadonlyArray<unknown>} */
const emptyClasses = [];
/**
* Create a plugin.
*
* @param {CreateRenderer} createRenderer
* Create a renderer.
* @returns
* Plugin.
*/
export function createPlugin(createRenderer) {
/**
* Plugin.
*
* @param {Readonly<Options> | null | undefined} [options]
* Configuration (optional).
* @returns
* Transform.
*/
return function (options) {
/**
* Transform.
*
* @param {Root} tree
* Tree.
* @param {VFile} file
* File.
* @returns {undefined}
* Nothing.
*/
return function (tree, file) {
const renderer = createRenderer(options || emptyOptions);
let found = false;
/** @type {Element | Root} */
let context = tree;
visitParents(tree, "element", function (element, parents) {
const classes = Array.isArray(element.properties.className) ? element.properties.className : emptyClasses;
// This class can be generated from markdown with ` ```math `.
const languageMath = classes.includes("language-math");
// This class is used by `remark-math` for flow math (block, `$$\nmath\n$$`).
const mathDisplay = classes.includes("math-display");
// This class is used by `remark-math` for text math (inline, `$math$`).
const mathInline = classes.includes("math-inline");
let display = mathDisplay;
// Find `<head>`.
if (element.tagName === "head") {
context = element;
}
// Any class is fine.
if (!languageMath && !mathDisplay && !mathInline) {
return;
}
let parent = parents[parents.length - 1];
let scope = element;
// If this was generated with ` ```math `, replace the `<pre>` and use
// display.
if (
element.tagName === "code" &&
languageMath &&
parent &&
parent.type === "element" &&
parent.tagName === "pre"
) {
scope = parent;
parent = parents[parents.length - 2];
display = true;
}
/* c8 ignore next -- verbose to test. */
if (!parent) return;
if (!found && renderer.register) renderer.register();
found = true;
const text = toText(scope, { whitespace: "pre" });
/** @type {Array<ElementContent> | undefined} */
let result;
try {
result = renderer.render(text, { display });
} catch (error) {
const cause = /** @type {Error} */ (error);
file.message("Could not render math with mathjax", {
ancestors: [...parents, element],
cause,
place: element.position,
ruleId: "mathjax-error",
source: "rehype-mathjax",
});
result = [
{
type: "element",
tagName: "span",
properties: {
className: ["mathjax-error"],
style: "color:#cc0000",
title: String(cause),
},
children: [{ type: "text", value: text }],
},
];
}
const index = parent.children.indexOf(scope);
parent.children.splice(index, 1, ...result);
return SKIP;
});
if (found) {
if (renderer.styleSheet) context.children.push(renderer.styleSheet());
if (renderer.unregister) renderer.unregister();
}
};
};
}

View File

@@ -69,7 +69,6 @@ import { BypassWrapper } from "./React/BypassWrapper";
import { Apr1 } from "./Apr1"; import { Apr1 } from "./Apr1";
import { V2Modal } from "../utils/V2Modal"; import { V2Modal } from "../utils/V2Modal";
import { MathJaxContext } from "better-react-mathjax";
import { useRerender } from "./React/hooks"; import { useRerender } from "./React/hooks";
import { HistoryProvider } from "./React/Documentation"; import { HistoryProvider } from "./React/Documentation";
import { GoRoot } from "../Go/ui/GoRoot"; import { GoRoot } from "../Go/ui/GoRoot";
@@ -505,7 +504,7 @@ export function GameRoot(): React.ReactElement {
}, []); }, []);
return ( return (
<MathJaxContext version={3} src={__webpack_public_path__ + "mathjax/tex-chtml.js"}> <>
<ErrorBoundary key={errorBoundaryKey} softReset={softReset}> <ErrorBoundary key={errorBoundaryKey} softReset={softReset}>
<BypassWrapper content={bypassGame ? mainPage : null}> <BypassWrapper content={bypassGame ? mainPage : null}>
<HistoryProvider> <HistoryProvider>
@@ -547,6 +546,6 @@ export function GameRoot(): React.ReactElement {
</BypassWrapper> </BypassWrapper>
</ErrorBoundary> </ErrorBoundary>
<V2Modal /> <V2Modal />
</MathJaxContext> </>
); );
} }

View File

@@ -6,11 +6,31 @@ import { h1, h2, h3, h4, h5, h6, li, Td, Th, table, tr, Blockquote, p } from "./
import { code, Pre } from "./code"; import { code, Pre } from "./code";
import { A } from "./a"; import { A } from "./a";
import remarkMath from "remark-math"; import remarkMath from "remark-math";
import rehypeMathjax from "rehype-mathjax/svg";
import rehypeRaw from "rehype-raw"; import rehypeRaw from "rehype-raw";
import { FilePath } from "../../Paths/FilePath"; import { FilePath } from "../../Paths/FilePath";
import { getPage } from "../../Documentation/root"; import { convertMathNotation, getPage } from "../../Documentation/root";
import { DocImages } from "../../Documentation/pages"; import { DocImages } from "../../Documentation/pages";
import { createPlugin } from "../../ThirdParty/RehypePlugin.mjs";
import { Settings } from "../../Settings/Settings";
import { fromDom } from "hast-util-from-dom";
const rehypePlugin = createPlugin(function () {
return {
render(value: string, { display }: { display: boolean }) {
const mml = convertMathNotation(value);
const element = document.createElement(display ? "div" : "span");
if (display) {
element.style.textAlign = "center";
}
element.style.fontFamily = Settings.styles.fontFamily;
// Check the comment in src/Themes/ui/StyleEditorModal.tsx to see why we need to convert the font size.
element.style.fontSize = `${Settings.styles.fontSize * (16 / 14)}px`;
element.style.color = Settings.theme.primary;
element.innerHTML = mml;
return [fromDom(element)];
},
};
});
export function MD({ pageFilePath }: { pageFilePath: FilePath }): React.ReactElement { export function MD({ pageFilePath }: { pageFilePath: FilePath }): React.ReactElement {
const pageContent = getPage(pageFilePath); const pageContent = getPage(pageFilePath);
@@ -38,8 +58,9 @@ export function MD({ pageFilePath }: { pageFilePath: FilePath }): React.ReactEle
a: A, a: A,
}} }}
remarkPlugins={[remarkGfm, remarkMath]} remarkPlugins={[remarkGfm, remarkMath]}
// Use rehypeRaw to support HTML content in NS API docs. // Use a custom rehype plugin to render LaTeX notation in md files. Use rehypeRaw to support HTML content in NS
rehypePlugins={[rehypeMathjax, rehypeRaw]} // API docs.
rehypePlugins={[rehypePlugin, rehypeRaw]}
transformImageUri={(__src, alt) => { transformImageUri={(__src, alt) => {
return DocImages[alt]; return DocImages[alt];
}} }}

View File

@@ -1,11 +1,11 @@
import React from "react"; import React from "react";
import { MathJax } from "better-react-mathjax";
import InfoIcon from "@mui/icons-material/Info"; import InfoIcon from "@mui/icons-material/Info";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import { Favor } from "../../ui/React/Favor"; import { Favor } from "../../ui/React/Favor";
import MathNotation from "../../Documentation/data/MathNotation.json";
import { MathNotationOutput } from "../../Documentation/ui/MathNotationOutput";
export function FavorInfo({ favor, boldLabel }: { favor: number; boldLabel?: boolean }): React.ReactElement { export function FavorInfo({ favor, boldLabel }: { favor: number; boldLabel?: boolean }): React.ReactElement {
return ( return (
@@ -17,8 +17,8 @@ export function FavorInfo({ favor, boldLabel }: { favor: number; boldLabel?: boo
favor is gained whenever you install an Augmentation. The amount of favor you gain depends on the total favor is gained whenever you install an Augmentation. The amount of favor you gain depends on the total
amount of reputation you earned with this faction across all resets. amount of reputation you earned with this faction across all resets.
</Typography> </Typography>
<MathJax>{"\\(\\huge{r = reputation}\\)"}</MathJax> <Typography style={{ fontSize: "2rem" }}>r = Reputation gain</Typography>
<MathJax>{"\\(\\huge{\\Delta r = \\Delta r \\times \\frac{100+favor}{100}}\\)"}</MathJax> <MathNotationOutput notation={MathNotation.FavorBonus} />
</> </>
} }
> >

View File

@@ -1,5 +1,4 @@
import React from "react"; import React from "react";
import { MathJax } from "better-react-mathjax";
import InfoIcon from "@mui/icons-material/Info"; import InfoIcon from "@mui/icons-material/Info";
import Tooltip from "@mui/material/Tooltip"; import Tooltip from "@mui/material/Tooltip";
@@ -8,6 +7,8 @@ import Typography from "@mui/material/Typography";
import { addRepToFavor } from "../../Faction/formulas/favor"; import { addRepToFavor } from "../../Faction/formulas/favor";
import { Favor } from "../../ui/React/Favor"; import { Favor } from "../../ui/React/Favor";
import { Reputation } from "./Reputation"; import { Reputation } from "./Reputation";
import MathNotation from "../../Documentation/data/MathNotation.json";
import { MathNotationOutput } from "../../Documentation/ui/MathNotationOutput";
export function ReputationInfo({ export function ReputationInfo({
favor, favor,
@@ -26,8 +27,8 @@ export function ReputationInfo({
You will have <Favor favor={addRepToFavor(favor, playerReputation)} /> faction favor after installing an You will have <Favor favor={addRepToFavor(favor, playerReputation)} /> faction favor after installing an
Augmentation. Augmentation.
</Typography> </Typography>
<MathJax>{"\\(\\huge{r = \\text{total faction reputation}}\\)"}</MathJax> <Typography style={{ fontSize: "2rem" }}>r = Total faction reputation</Typography>
<MathJax>{"\\(\\huge{favor=\\log_{1.02}\\left(1+\\frac{r}{25000}\\right)}\\)"}</MathJax> <MathNotationOutput notation={MathNotation.RepToFavor} />
</> </>
} }
> >

View File

@@ -0,0 +1,46 @@
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import remarkMath from "remark-math";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import { unified } from "unified";
import { createPlugin } from "../../src/ThirdParty/RehypePlugin.mjs";
import { convertToMML, walkDir } from "./utils.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const data = {};
const parser = unified().use(remarkParse).use(remarkMath);
const tool = unified()
.use(remarkRehype)
.use(
createPlugin(function () {
return {
render(value) {
data[value] = convertToMML(value);
return [];
},
};
}),
);
function processDir(dir) {
walkDir(dir, (filePath) => {
tool.runSync(parser.parse(readFileSync(filePath)));
});
}
processDir(resolve(__dirname, "../../src/Documentation/doc/en"));
const notationInput = JSON.parse(readFileSync(resolve(__dirname, "../../src/Documentation/data/MathNotation.json")));
for (const value of Object.values(notationInput)) {
data[value] = convertToMML(value);
}
console.log(`Generated MathML data for ${Object.keys(data).length} notation entries`);
writeFileSync(resolve(__dirname, "../../src/Documentation/data/MathNotationOutput.json"), JSON.stringify(data), {
encoding: "utf-8",
});

View File

@@ -1,27 +1,25 @@
const fs = require("fs"); import { writeFileSync } from "node:fs";
const path = require("path"); import { dirname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { walkDir } from "./utils.mjs";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const docFiles = []; const docFiles = [];
const nsDocFiles = []; const nsDocFiles = [];
const docImagesFiles = []; const docImagesFiles = [];
const docRoot = path.resolve(__dirname, "../../src/Documentation/doc"); const docRoot = resolve(__dirname, "../../src/Documentation/doc");
const markdownRoot = path.resolve(__dirname, "../../markdown"); const markdownRoot = resolve(__dirname, "../../markdown");
const docImagesRoot = path.resolve(__dirname, "../../src/Documentation/images"); const docImagesRoot = resolve(__dirname, "../../src/Documentation/images");
const addFileToListOfDocPages = (files, root, filePath) => {
function addFileToListOfDocPages(files, root, filePath) {
// Windows path uses "\", so we need to replace it with "/". // Windows path uses "\", so we need to replace it with "/".
files.push(path.relative(root, filePath).replace(/\\/g, "/")); files.push(relative(root, filePath).replace(/\\/g, "/"));
}; }
const processDir = (dir) => {
if (!fs.existsSync(dir)) { function processDir(dir) {
return; walkDir(dir, (filePath) => {
}
console.log(dir);
for (const file of fs.readdirSync(dir).sort((a, b) => a.localeCompare(b, "en-US"))) {
const filePath = path.join(dir, file);
if (fs.lstatSync(filePath).isDirectory()) {
processDir(filePath);
continue;
}
if (filePath.startsWith(docRoot)) { if (filePath.startsWith(docRoot)) {
addFileToListOfDocPages(docFiles, docRoot, filePath); addFileToListOfDocPages(docFiles, docRoot, filePath);
} else if (filePath.startsWith(markdownRoot)) { } else if (filePath.startsWith(markdownRoot)) {
@@ -29,8 +27,8 @@ const processDir = (dir) => {
} else { } else {
addFileToListOfDocPages(docImagesFiles, docImagesRoot, filePath); addFileToListOfDocPages(docImagesFiles, docImagesRoot, filePath);
} }
} });
}; }
processDir(docRoot); processDir(docRoot);
processDir(markdownRoot); processDir(markdownRoot);
@@ -68,9 +66,4 @@ export const nsApiPages = Object.keys(AllPages)
export const DocImages: Record<string, string> = {}; export const DocImages: Record<string, string> = {};
${docImagesFiles.map((f) => `DocImages["${f}"] = docImages_${f.replaceAll(/\.|-/g, "_")};\n`).join("")}`; ${docImagesFiles.map((f) => `DocImages["${f}"] = docImages_${f.replaceAll(/\.|-/g, "_")};\n`).join("")}`;
fs.writeFile(path.resolve(__dirname, "../../src/Documentation/pages.ts"), autogeneratedContent, (err) => { writeFileSync(resolve(__dirname, "../../src/Documentation/pages.ts"), autogeneratedContent, { encoding: "utf-8" });
if (err) {
console.error(err);
}
// file written successfully
});

View File

@@ -0,0 +1,63 @@
import { existsSync, lstatSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { mathjax } from "@mathjax/src/js/mathjax.js";
import { TeX } from "@mathjax/src/js/input/tex.js";
import { liteAdaptor } from "@mathjax/src/js/adaptors/liteAdaptor.js";
import { RegisterHTMLHandler } from "@mathjax/src/js/handlers/html.js";
import "@mathjax/src/js/util/asyncLoad/esm.js";
import { SerializedMmlVisitor } from "@mathjax/src/js/core/MmlTree/SerializedMmlVisitor.js";
import "@mathjax/src/js/input/tex/base/BaseConfiguration.js";
import "@mathjax/src/js/input/tex/ams/AmsConfiguration.js";
import "@mathjax/src/js/input/tex/newcommand/NewcommandConfiguration.js";
import "@mathjax/src/js/input/tex/noundefined/NoUndefinedConfiguration.js";
import xmlFormat from "xml-formatter";
export function walkDir(dir, callback) {
if (!existsSync(dir)) {
return;
}
console.log(`Processing ${dir}`);
for (const file of readdirSync(dir).sort((a, b) => a.localeCompare(b, "en-US"))) {
const filePath = join(dir, file);
if (lstatSync(filePath).isDirectory()) {
walkDir(filePath, callback);
continue;
}
callback(filePath);
}
}
const adaptor = liteAdaptor();
RegisterHTMLHandler(adaptor);
const tex = new TeX({
packages: ["base", "ams", "newcommand", "noundefined"],
formatError(jax, err) {
console.error(jax, err);
process.exit(1);
},
});
const visitor = new SerializedMmlVisitor();
const docMML = mathjax.document("", { InputJax: tex });
export function convertToMML(value) {
const node = docMML.convert(value, {
// Do not set "display" css property. The display mode will be decided by the rehype plugin.
display: false,
});
node.walkTree((node) => {
const attributes = node.attributes;
// Remove unnecessary attributes.
attributes.unset("data-latex");
attributes.unset("data-latex-item");
});
const mathJaxOutput = visitor.visitTree(node, docMML);
return xmlFormat.minify(mathJaxOutput, {
filter: (node) => node.type !== "Comment",
collapseContent: true,
});
}

View File

@@ -13,12 +13,16 @@ rm input/bitburner.api.json && rm -r input
echo "" echo ""
echo "Bundling ingame documentation..." echo "Bundling ingame documentation..."
node tools/bundle-doc/index.js node tools/bundle-doc/index.mjs
echo "" echo ""
echo "Add generated docs to git..." echo "Add generated docs to git..."
# This git add is needed due to documenter using wrong line endings. Console spam discarded. # This git add is needed due to documenter using wrong line endings. Console spam discarded.
git add markdown/ 2> /dev/null && git add tsdoc-metadata.json 2> /dev/null git add markdown/ 2> /dev/null && git add tsdoc-metadata.json 2> /dev/null
echo ""
echo "Generate math notation output..."
node tools/bundle-doc/generate-math-notation-output.mjs
echo "" echo ""
echo "Documentation build completed." echo "Documentation build completed."

View File

@@ -5,7 +5,6 @@ const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin"); const HtmlWebpackPlugin = require("html-webpack-plugin");
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");
const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin"); const ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
const CopyPlugin = require("copy-webpack-plugin");
/** @type {import("webpack-cli").CallableOption} */ /** @type {import("webpack-cli").CallableOption} */
module.exports = (env, argv) => { module.exports = (env, argv) => {
@@ -124,15 +123,6 @@ module.exports = (env, argv) => {
module: true, module: true,
}), }),
enableReactRefresh && new ReactRefreshWebpackPlugin(), enableReactRefresh && new ReactRefreshWebpackPlugin(),
new CopyPlugin({
patterns: [
{
from: "{tex-chtml.js,*/**/*}",
to: "mathjax",
context: "node_modules/mathjax-full/es5",
},
],
}),
].filter(Boolean), ].filter(Boolean),
target: "web", target: "web",
entry: entry, entry: entry,