" item
+// header up to the next column-0 "-" (the following item) or EOF, then
+// dedent them by their common indent so the rest of this script can treat
+// them exactly like a standalone file.
+$lines = [];
+$capturing = false;
+foreach ( file( $path ) as $line ) {
+ if ( !$capturing ) {
+ if ( preg_match( '/^-\s*name:\s*' . preg_quote( $demo, '/' ) . '\s*$/', $line ) ) {
+ $capturing = true;
+ }
+ continue;
+ }
+ if ( $line !== '' && $line[0] === '-' ) {
+ break;
+ }
+ $lines[] = $line;
+}
+if ( !$capturing ) {
+ fwrite( STDERR, "render.php: no demo named '$demo' found in $path\n" );
+ exit( 1 );
+}
+$indent = null;
+foreach ( $lines as $line ) {
+ if ( trim( $line ) === '' ) {
+ continue;
+ }
+ $lineIndent = strlen( $line ) - strlen( ltrim( $line ) );
+ $indent = $indent === null ? $lineIndent : min( $indent, $lineIndent );
+}
+if ( $indent !== null ) {
+ foreach ( $lines as &$line ) {
+ if ( trim( $line ) !== '' ) {
+ $line = substr( $line, $indent );
+ }
+ }
+ unset( $line );
+}
+
+if ( $mode === 'settings' ) {
+ $out = [];
+ $inBlock = false;
+ $indent = null;
+ foreach ( $lines as $line ) {
+ if ( !$inBlock ) {
+ if ( preg_match( '/^settings:\s*\|\s*$/', $line ) ) {
+ $inBlock = true;
+ }
+ continue;
+ }
+ if ( trim( $line ) === '' ) {
+ $out[] = "\n";
+ continue;
+ }
+ $lineIndent = strlen( $line ) - strlen( ltrim( $line ) );
+ $indent ??= $lineIndent;
+ if ( $lineIndent < $indent ) {
+ break;
+ }
+ $out[] = substr( $line, $indent );
+ }
+ echo "wfLoadExtension( 'SimpleMathJax' );\n";
+ if ( $out !== [] ) {
+ echo rtrim( implode( '', $out ) ) . "\n";
+ }
+ exit;
+}
+
+if ( $mode === 'examples' ) {
+ $examples = [];
+ $count = count( $lines );
+ for ( $i = 0; $i < $count; $i++ ) {
+ $line = $lines[$i];
+ if ( preg_match( '/^\s*-\s*"(.*)"\s*$/', $line, $m ) ) {
+ $examples[] = json_decode( "\"{$m[1]}\"" );
+ continue;
+ }
+ if ( preg_match( "/^\s*-\s*'(.*)'\s*$/", $line, $m ) ) {
+ // YAML single-quoted scalars escape a literal apostrophe as ''.
+ $examples[] = str_replace( "''", "'", $m[1] );
+ continue;
+ }
+ // A `- |` literal block scalar: every following line indented more
+ // than the "-" is taken verbatim (no quote-escaping) until indentation
+ // drops back to the item's own level or lower, then dedented by its
+ // own common indent and trailing blank lines clipped — long examples
+ // (e.g. a multi-line continued fraction) read better this way than
+ // escaped into one quoted line.
+ if ( preg_match( '/^(\s*)-\s*\|\s*$/', $line, $m ) ) {
+ $itemIndent = strlen( $m[1] );
+ $blockLines = [];
+ $blockIndent = null;
+ for ( $i++; $i < $count; $i++ ) {
+ $next = $lines[$i];
+ if ( trim( $next ) === '' ) {
+ // Each non-blank line below still carries its own
+ // trailing "\n" from file(), so joining with '' (not a
+ // "\n" glue) reproduces the source exactly — matching
+ // how the settings-mode block above is joined.
+ $blockLines[] = "\n";
+ continue;
+ }
+ $nextIndent = strlen( $next ) - strlen( ltrim( $next ) );
+ if ( $nextIndent <= $itemIndent ) {
+ break;
+ }
+ $blockIndent ??= $nextIndent;
+ $blockLines[] = substr( $next, $blockIndent );
+ }
+ $i--; // the for loop's own $i++ will land back on the line that broke us out
+ $examples[] = rtrim( implode( '', $blockLines ) );
+ }
+ }
+ // Column fragmentation ("column-count" below) makes the container its
+ // own block formatting context, so the first item's own top margin (a
+ // browser default on , which renders as) doesn't
+ // collapse into the page above it the way it normally would — visible
+ // as a gap above column 1 only, since a later column's break point
+ // isn't a "start" and so never re-applies that margin. Pull the whole
+ // block up by that amount to cancel it out.
+ echo '';
+ foreach ( $examples as $example ) {
+ echo "$example $example\n";
+ }
+ echo '
';
+ exit;
+}
+
+if ( $mode === 'addrightclickshot' ) {
+ foreach ( $lines as $line ) {
+ if ( preg_match( '/^addRightClickShot:\s*true\s*$/', $line ) ) {
+ echo "true\n";
+ break;
+ }
+ }
+ exit;
+}
+
+if ( $mode === 'adddiffshot' ) {
+ foreach ( $lines as $line ) {
+ if ( preg_match( '/^addDiffShot:\s*true\s*$/', $line ) ) {
+ echo "true\n";
+ break;
+ }
+ }
+ exit;
+}
+
+fwrite( STDERR, "render.php: unknown mode '$mode' (want 'settings', 'examples', 'addrightclickshot' or 'adddiffshot')\n" );
+exit( 1 );
diff --git a/hack/demo/screenshot.mjs b/hack/demo/screenshot.mjs
new file mode 100644
index 00000000..11698ed2
--- /dev/null
+++ b/hack/demo/screenshot.mjs
@@ -0,0 +1,105 @@
+#!/usr/bin/env node
+// Screenshots a page of the local test wiki (see demo.sh) with a real
+// headless browser, so the captured image reflects MathJax's actual
+// client-side rendering — not just the raw parsed HTML.
+//
+// Usage: URL=... OUT=... RIGHT_CLICK= RIGHT_CLICK_OUT=... node screenshot.mjs
+// (env vars, not positional args, so setting only one of them can't shift
+// the other — `make screenshot OUT=...` just works)
+//
+// RIGHT_CLICK, when set, takes one extra screenshot (to RIGHT_CLICK_OUT)
+// after right-clicking the LAST element matching that CSS selector — used
+// by demo.sh's `addRightClickShot: true` field to also show MathJax's context menu
+// (only visible on right-click, so the plain OUT capture can't demonstrate
+// $wgSmjEnableMenu on its own). The normal OUT capture always happens
+// first, unaffected by this.
+import puppeteer from 'puppeteer';
+import fs from 'node:fs';
+import path from 'node:path';
+
+// The container/CI environment this runs in has no CJK fonts installed, so
+// Korean/Japanese/Chinese text in a demo page (including inside MathJax's
+// own output) would render blank. Rather than depend on system fonts
+// (apt-get, different per host), load pinned Fontsource packages and apply
+// them as fallbacks. One sans (regular text) and one serif (MathJax defaults
+// to a serif TeX look), so each keeps the weight it would normally have.
+const DEMO_DIR = path.dirname(new URL(import.meta.url).pathname);
+
+function localFontCss(packageName) {
+ const packageDir = path.join(DEMO_DIR, 'node_modules', '@fontsource', packageName);
+ let css = fs.readFileSync(path.join(packageDir, 'index.css'), 'utf8');
+ return css.replace(/url\(\.\/files\/([^\)]+)\)/g, (match, filename) => {
+ const font = fs.readFileSync(path.join(packageDir, 'files', filename));
+ return `url(data:font/woff2;base64,${font.toString('base64')})`;
+ });
+}
+
+const url = process.env.URL ?? 'http://localhost:8080/index.php/Demo';
+const outfile = process.env.OUT ?? '../../docs/demo1-screenshot.png';
+
+// --no-sandbox is needed when running as root (e.g. in CI or a dev
+// container) — Chromium's sandbox refuses to start otherwise.
+const browser = await puppeteer.launch(
+ process.getuid?.() === 0 ? { args: ['--no-sandbox'] } : {}
+);
+try {
+ const page = await browser.newPage();
+ await page.setViewport({ width: 1024, height: 768 });
+ await page.goto(url, { waitUntil: 'networkidle0' });
+ // Real browsers already show CJK fine here — the OS substitutes any
+ // installed CJK font for glyphs missing from whatever font is named
+ // (even MathJax's own inline `font-family: MJXZERO, serif` on its
+ // `` text nodes, used only for characters outside its own
+ // font, and the browser default `pre, code { font-family: monospace }`
+ // used by blocks). This sandbox just has no CJK font
+ // at all, so nothing to substitute. `!important` is needed because
+ // these are all higher-priority than inheriting from `body`.
+ //
+ // The per-script substitution CDP offers for exactly this
+ // (`Page.setFontFamilies`) would avoid hand-listing every selector that
+ // names its own font-family below, but it's a no-op in this headless
+ // Chromium (call succeeds, page still shows tofu) — tried and reverted,
+ // see git history if revisiting.
+ //
+ // mjx-utext is scoped narrowly, not every mjx-container descendant:
+ // stretchy delimiters like \left(...\right) are sized glyphs from
+ // MathJax's own font (e.g. class "TEX-S2"), not text — forcing a web
+ // font onto those too breaks their metrics, so `\left(` stops growing
+ // to match its contents. pre/code keep "monospace" first so Latin text
+ // stays aligned, with the web font only as a per-glyph fallback for CJK.
+ await page.addStyleTag({ content: localFontCss('noto-sans-kr') });
+ await page.addStyleTag({ content: localFontCss('noto-serif-kr') });
+ await page.addStyleTag({
+ content: `
+ body { font-family: 'Noto Sans KR', sans-serif !important; }
+ mjx-utext { font-family: 'Noto Serif KR', serif !important; }
+ pre, code { font-family: monospace, 'Noto Sans KR' !important; }
+ .diff-addedline, .diff-deletedline, .diff-context,
+ .mw-diff-inline-added, .mw-diff-inline-deleted,
+ .mw-diff-inline-moved, .mw-diff-inline-changed,
+ .mw-diff-inline-context { font-family: monospace, 'Noto Sans KR' !important; }
+ `,
+ });
+ await page.evaluate(() => document.fonts.ready);
+ // MathJax typesets asynchronously after the page loads; give it a moment.
+ await page.waitForNetworkIdle({ idleTime: 500 }).catch(() => { });
+ await page.screenshot({ path: outfile, fullPage: true });
+ console.log(`==> Saved ${outfile}`);
+
+ const rightClick = process.env.RIGHT_CLICK;
+ if (rightClick) {
+ const rightClickOutfile = process.env.RIGHT_CLICK_OUT;
+ // page.click() only ever hits the first match; a demo page can have
+ // several mjx-container elements (one per example), so grab all of
+ // them and right-click the last one instead.
+ const matches = await page.$$(rightClick);
+ await matches[matches.length - 1].click({ button: 'right' });
+ // The context menu renders synchronously off the click, but give
+ // MathJax's own transition/animation a moment to settle.
+ await new Promise((resolve) => setTimeout(resolve, 500));
+ await page.screenshot({ path: rightClickOutfile, fullPage: true });
+ console.log(`==> Saved ${rightClickOutfile}`);
+ }
+} finally {
+ await browser.close();
+}
diff --git a/hack/local-mathjax.sh b/hack/local-mathjax.sh
new file mode 100755
index 00000000..a0276730
--- /dev/null
+++ b/hack/local-mathjax.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# Pins the bundled local MathJax submodule to one tag.
+#
+# The CDN version ($wgSmjCdnVersion's default in extension.json) is managed
+# independently — edit that value directly, the same as any other config
+# default — since it doesn't have to track the local submodule's version.
+#
+# Usage: hack/local-mathjax.sh
+# e.g. hack/local-mathjax.sh 4.1.3
+set -euo pipefail
+cd "$(dirname "$0")/.."
+
+MATHJAX_DIR="resources/MathJax"
+LOCAL_VERSION="${1:-}"
+
+if [ -z "$LOCAL_VERSION" ]; then
+ echo "Usage: hack/local-mathjax.sh (e.g. hack/local-mathjax.sh 4.1.3)" >&2
+ exit 1
+fi
+
+git submodule update --init "$MATHJAX_DIR"
+(cd "$MATHJAX_DIR" && git fetch --tags origin && git checkout "tags/$LOCAL_VERSION")
+git add "$MATHJAX_DIR"
+
+echo "==> Pinned local MathJax to $LOCAL_VERSION."
+echo "==> Review with 'git status', then commit the bump."
diff --git a/hack/mathjax.sh b/hack/mathjax.sh
deleted file mode 100755
index fd98ac6b..00000000
--- a/hack/mathjax.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/usr/bin/env bash
-# Pins the resources/MathJax submodule to a tag and updates the CDN URL's
-# major version in resources/ext.SimpleMathJax.js. The two are versioned
-# separately on purpose: the local copy is pinned to an exact tag, while
-# the CDN URL only pins a major version (jsdelivr resolves it to the
-# latest matching release).
-#
-# Usage: hack/mathjax.sh
-# e.g. hack/mathjax.sh 4.1.3 4
-set -euo pipefail
-cd "$(dirname "$0")/.."
-
-MATHJAX_DIR="resources/MathJax"
-JS_FILE="resources/ext.SimpleMathJax.js"
-LOCAL_VERSION="${1:-}"
-CDN_VERSION="${2:-}"
-
-if [ -z "$LOCAL_VERSION" ] || [ -z "$CDN_VERSION" ]; then
- echo "Usage: hack/mathjax.sh (e.g. hack/mathjax.sh 4.1.3 4)" >&2
- exit 1
-fi
-
-git submodule update --init "$MATHJAX_DIR"
-(cd "$MATHJAX_DIR" && git fetch --tags origin && git checkout "tags/$LOCAL_VERSION")
-git add "$MATHJAX_DIR"
-
-sed -i -E "s#(cdn\.jsdelivr\.net/npm/mathjax@)[^/]+#\1${CDN_VERSION}#" "$JS_FILE"
-git add "$JS_FILE"
-
-echo "==> Pinned $MATHJAX_DIR to $LOCAL_VERSION and the CDN URL to mathjax@$CDN_VERSION."
-echo "==> Review with 'git status', then commit the bump."
diff --git a/includes/Hooks.php b/includes/Hooks.php
index 2c40d9df..6b810f74 100644
--- a/includes/Hooks.php
+++ b/includes/Hooks.php
@@ -9,200 +9,185 @@
use PPFrame;
class Hooks {
- /** @var bool */
- private static $useChem;
- /** @var bool */
- private static $wrapDisplaystyle;
- /** @var bool */
- private static $enableHtmlAttributes;
- /** @var string */
- private static $directMathJax;
- /** @var array[] */
- private static $displayMath;
- /** @var array[] */
- private static $extraInlineMath;
+ private static array $allowedAttributes = [];
+ private static bool $extraDelimitersEnabled = false;
+ private static array $extraDelimitersInlineMath = [];
+ private static array $extraDelimitersDisplayMath = [];
+ private static string $ignoreHtmlClass = '';
public static function onParserFirstCallInit( Parser $parser ) {
- global $wgOut, $wgSmjUseCdn, $wgSmjUseChem, $wgSmjDirectMathJax, $wgSmjEnableMenu,
- $wgSmjDisplayMath, $wgSmjExtraInlineMath, $wgSmjIgnoreHtmlClass,
- $wgSmjScale, $wgSmjDisplayAlign, $wgSmjWrapDisplaystyle,
- $wgSmjEnableHtmlAttributes, $wgSmjConfigByRevision;
+ global $wgOut, $wgSmjCdnEnabled, $wgSmjCdnVersion, $wgSmjEnableMenu,
+ $wgSmjDelimitersEnabled, $wgSmjDelimitersInlineMath, $wgSmjDelimitersDisplayMath,
+ $wgSmjIgnoreHtmlClass, $wgSmjScale,
+ $wgSmjAllowedAttributes, $wgSmjRevisionOverrides;
$config = [
- "wgSmjUseCdn" => $wgSmjUseCdn,
- "wgSmjUseChem" => $wgSmjUseChem,
- "wgSmjDirectMathJax" => $wgSmjDirectMathJax,
- "wgSmjDisplayMath" => $wgSmjDisplayMath,
- "wgSmjExtraInlineMath" => $wgSmjExtraInlineMath,
- "wgSmjIgnoreHtmlClass" => $wgSmjIgnoreHtmlClass,
- "wgSmjScale" => $wgSmjScale,
- "wgSmjEnableMenu" => $wgSmjEnableMenu,
- "wgSmjDisplayAlign" => $wgSmjDisplayAlign,
- "wgSmjWrapDisplaystyle" => $wgSmjWrapDisplaystyle,
- "wgSmjEnableHtmlAttributes" => $wgSmjEnableHtmlAttributes,
+ "wgSmjCdnEnabled" => $wgSmjCdnEnabled,
+ "wgSmjCdnVersion" => $wgSmjCdnVersion,
+ "wgSmjDelimitersEnabled" => $wgSmjDelimitersEnabled,
+ "wgSmjDelimitersInlineMath" => $wgSmjDelimitersInlineMath,
+ "wgSmjDelimitersDisplayMath" => $wgSmjDelimitersDisplayMath,
+ "wgSmjIgnoreHtmlClass" => $wgSmjIgnoreHtmlClass,
+ "wgSmjScale" => $wgSmjScale,
+ "wgSmjEnableMenu" => $wgSmjEnableMenu,
+ "wgSmjAllowedAttributes" => $wgSmjAllowedAttributes,
];
$articlerev = (int)$wgOut->getRevisionId();
- foreach ( $wgSmjConfigByRevision as $confset ) {
+ $config = self::applyRevisionOverrides( $config, $wgSmjRevisionOverrides, $articlerev );
+
+ $clientConfigVars = [ "wgSmjCdnEnabled", "wgSmjCdnVersion",
+ "wgSmjDelimitersEnabled", "wgSmjDelimitersInlineMath", "wgSmjDelimitersDisplayMath",
+ "wgSmjIgnoreHtmlClass", "wgSmjScale", "wgSmjEnableMenu" ];
+ foreach ( $clientConfigVars as $varname ) {
+ $wgOut->addJsConfigVars( $varname, $config[$varname] );
+ }
+
+ self::$allowedAttributes =
+ is_array( $config["wgSmjAllowedAttributes"] ) ? $config["wgSmjAllowedAttributes"] : [];
+ self::$extraDelimitersEnabled = (bool)$config["wgSmjDelimitersEnabled"];
+ self::$extraDelimitersInlineMath =
+ is_array( $config["wgSmjDelimitersInlineMath"] ) ? $config["wgSmjDelimitersInlineMath"] : [];
+ self::$extraDelimitersDisplayMath =
+ is_array( $config["wgSmjDelimitersDisplayMath"] ) ? $config["wgSmjDelimitersDisplayMath"] : [];
+ self::$ignoreHtmlClass =
+ is_string( $config["wgSmjIgnoreHtmlClass"] ) ? $config["wgSmjIgnoreHtmlClass"] : '';
+
+ if ( self::$extraDelimitersEnabled ) {
+ $wgOut->addModules( [ 'ext.SimpleMathJax' ] );
+ }
+
+ $parser->setHook( 'math', __CLASS__ . '::renderMath' );
+ $parser->setHook( 'chem', __CLASS__ . '::renderChem' );
+ }
+
+ // $pattern is an admin-supplied regex fragment with no delimiter of its
+ // own, so avoid one that could occur inside it.
+ private static function matchesIgnoreHtmlClass( string $pattern, string $class ): bool {
+ $delimiter = strpos( $pattern, '~' ) === false ? '~' : "\x01";
+ $result = preg_match( $delimiter . $pattern . $delimiter, $class );
+ return $result === 1;
+ }
+
+ // Apply $wgSmjRevisionOverrides on top of $config for the given revision id.
+ // A free function so it's unit-testable without a MediaWiki bootstrap.
+ public static function applyRevisionOverrides( array $config, array $overrides, int $articlerev ): array {
+ foreach ( $overrides as $confset ) {
if ( $articlerev == 0 ) {
break;
}
- if ( !isset( $confset["upto"] ) && !isset( $confset["since"] ) ) {
+
+ if ( !isset( $confset["min"] ) && !isset( $confset["max"] ) ) {
continue;
}
- if ( isset( $confset["upto"] ) && $confset["upto"] < $articlerev ) {
+
+ if ( isset( $confset["max"] ) && $confset["max"] < $articlerev ) {
continue;
}
- if ( isset( $confset["since"] ) && $confset["since"] > $articlerev ) {
+
+ if ( isset( $confset["min"] ) && $confset["min"] > $articlerev ) {
continue;
}
- foreach ( array_keys( $config ) as $varname ) {
- if ( array_key_exists( $varname, $confset ) ) {
- $config[$varname] = $confset[$varname];
+
+ foreach ( $confset as $key => $value ) {
+ if ( array_key_exists( $key, $config ) ) {
+ $config[$key] = $value;
}
}
}
-
- $clientConfigVars = [ "wgSmjUseCdn", "wgSmjDirectMathJax",
- "wgSmjDisplayMath", "wgSmjExtraInlineMath", "wgSmjIgnoreHtmlClass",
- "wgSmjScale", "wgSmjEnableMenu", "wgSmjDisplayAlign" ];
- foreach ( $clientConfigVars as $varname ) {
- $wgOut->addJsConfigVars( $varname, $config[$varname] );
- }
-
- self::$useChem = $config["wgSmjUseChem"];
- self::$wrapDisplaystyle = $config["wgSmjWrapDisplaystyle"];
- self::$enableHtmlAttributes = $config["wgSmjEnableHtmlAttributes"];
-
- // Cached for onInternalParseBeforeLinks(), which otherwise has no way
- // to see $wgSmjConfigByRevision overrides applied above — reading the
- // raw globals there could protect quotes for a different mode/delimiter
- // set than what the client (built from this same effective $config)
- // actually parses on a wiki using per-revision overrides.
- self::$directMathJax = $config["wgSmjDirectMathJax"];
- self::$displayMath = is_array( $config["wgSmjDisplayMath"] ) ? $config["wgSmjDisplayMath"] : [];
- self::$extraInlineMath = is_array( $config["wgSmjExtraInlineMath"] ) ? $config["wgSmjExtraInlineMath"] : [];
-
- if ( $config["wgSmjDirectMathJax"] !== 'none' ) {
- $wgOut->addModules( [ 'ext.SimpleMathJax' ] );
- }
-
- $parser->setHook( 'math', __CLASS__ . '::renderMath' );
- if ( self::$useChem ) {
- $parser->setHook( 'chem', __CLASS__ . '::renderChem' );
- }
+ return $config;
}
- public static function renderMath( $tex, array $args, Parser $parser, PPFrame $frame ) {
+ public static function renderMath( ?string $tex, array $args, Parser $parser, PPFrame $frame ) {
$parserOutput = $parser->getOutput();
$parserOutput->addModules( [ 'ext.SimpleMathJax' ] );
- if ( !self::$enableHtmlAttributes ) {
- $args = [];
- }
+
+ // Unconditional: this only preloads the mhchem JS package, unrelated
+ // to display handling.
if ( isset( $args["chem"] ) ) {
- $parserOutput->setJsConfigVar( "wgSmjPreloadChem", true );
+ $parserOutput->setJsConfigVar( "smjPreloadChem", true );
}
- if ( isset( $args["inline-block"] ) ) {
- if ( isset( $args["display"] ) ) {
- return self::renderError(
- 'SimpleMathJax: Do not use the inline-block attribute ' .
- 'and the display attribute together on the same element.'
- );
- }
- $tex = "\\displaystyle{ $tex }";
- } elseif ( !isset( $args["display"] ) ) {
- if ( self::$wrapDisplaystyle ) {
- $tex = "\\displaystyle{ $tex }";
- }
- } else {
- switch ( $args["display"] ) {
- case "":
- break;
- case "inline":
- $tex = "\\textstyle{ $tex }";
- break;
- case "block":
- break;
- default:
- return self::renderError(
- 'SimpleMathJax: Invalid attribute value: display="' . $args["display"] . '"'
- );
- }
+
+ if ( isset( $args["display"] ) && !in_array( $args["display"], [ "", "inline", "block" ], true ) ) {
+ return self::renderError( 'SimpleMathJax: invalid display="' . $args["display"] . '"' );
}
- return self::renderTex( $tex, $parser, $args );
+
+ // renderTex() applies \displaystyle{}/\textstyle{} itself, since it's
+ // only a default guess, not part of what the editor wrote.
+ return self::renderTex( $tex, $parser, $args, true );
}
- public static function renderChem( $tex, array $args, Parser $parser, PPFrame $frame ) {
+ public static function renderChem( ?string $tex, array $args, Parser $parser, PPFrame $frame ) {
$parserOutput = $parser->getOutput();
$parserOutput->addModules( [ 'ext.SimpleMathJax' ] );
- $parserOutput->setJsConfigVar( "wgSmjPreloadChem", true );
- if ( !self::$enableHtmlAttributes ) {
- $args = [];
- }
- return self::renderTex( "\\ce{ $tex }", $parser, $args );
+ $parserOutput->setJsConfigVar( "smjPreloadChem", true );
+
+ // Wrapping happens inside renderTex(), not here, so an ignored
+ // element (see below) shows the editor's original TeX rather than
+ // the \ce{} wrapper meant for MathJax.
+ return self::renderTex( $tex, $parser, $args, false, true );
}
- private static function renderTex( $tex, $parser, $args ) {
- $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
- $attributes = [ "style" => "opacity:.5", "class" => "" ];
- $inherit_tags = [ "class", "id", "title", "lang", "dir" ];
- $validatedAttribs = Sanitizer::validateAttributes( $args, array_fill_keys( $inherit_tags, true ) );
- $attributes = array_merge( $attributes, $validatedAttribs );
+ private static function renderTex(
+ ?string $tex, Parser $parser, array $args, bool $mathTag, bool $wrapChem = false
+ ) {
+ $hookContainer = MediaWikiServices::getInstance()->getHookContainer();
+ $attributes = [ "style" => "opacity:.5", "class" => "" ];
+ $allowedAttributes = array_filter( self::$allowedAttributes, 'is_string' );
+ $validatedAttribs = Sanitizer::validateAttributes(
+ $args,
+ array_fill_keys( $allowedAttributes, true )
+ );
+ $attributes = array_merge( $attributes, $validatedAttribs );
$hookContainer->run( "SimpleMathJaxAttributes", [ &$attributes, $tex, $args ] );
- if ( !isset( $attributes["smj-debug"] ) && !isset( $args["smj-debug"] ) ) {
- $attributes["class"] .= " smj-container";
- }
-
- if ( isset( $args["display"] ) && $args["display"] == "block" ) {
- $element = Html::Element( "span", $attributes, "\\begin{displaymjx}{$tex}\\end{displaymjx}" );
+ // An ignored element is never typeset, so it skips smj-container and
+ // the delimiter wrapping instead of showing them as literal text.
+ $isIgnored = self::$ignoreHtmlClass !== ''
+ && self::matchesIgnoreHtmlClass( self::$ignoreHtmlClass, $attributes["class"] );
+ if ( $isIgnored ) {
+ unset( $attributes["style"] );
+ $element = Html::Element( "span", $attributes, $tex );
} else {
- $element = Html::Element( "span", $attributes, "[math]{$tex}[/math]" );
+ if ( !isset( $attributes["smj-debug"] ) && !isset( $args["smj-debug"] ) ) {
+ $attributes["class"] .= " smj-container";
+ }
+ if ( $wrapChem ) {
+ $tex = "\\ce{ $tex }";
+ }
+ if ( $mathTag ) {
+ if ( !isset( $args["display"] ) ) {
+ $tex = "\\displaystyle{ $tex }";
+ } elseif ( $args["display"] === "inline" ) {
+ $tex = "\\textstyle{ $tex }";
+ }
+ }
+ $element = isset( $args["display"] ) && $args["display"] === "block"
+ ? Html::Element( "span", $attributes, "\\begin{displaymjx}{$tex}\\end{displaymjx}" )
+ : Html::Element( "span", $attributes, "[math]{$tex}[/math]" );
}
return [ $element, 'markerType' => 'nowiki' ];
}
- private static function renderError( $str ) {
+ private static function renderError( string $str ) {
$attributes = [ "class" => "error texerror" ];
- $element = Html::Element( "strong", $attributes, $str );
+ $element = Html::Element( "strong", $attributes, $str );
return [ $element, 'markerType' => 'nowiki' ];
}
- /**
- * Protect '' / ''' runs inside MathJax-delimited math from MediaWiki's
- * wikitext emphasis parsing (InternalParseBeforeLinks runs after
- * nowiki/tag stripping but before handleAllQuotes, so code blocks are
- * already markers and prose italics is still to come). In TeX, '' and
- * ''' are primes (y'', f'''(x)); without this guard the parser inserts
- * / inside the delimited text, splitting it so MathJax cannot find
- * the closing delimiter (dangling $ then swallows prose as math).
- *
- * Runs only when direct $…$/$$…$$ parsing is enabled (mode 'full'/'env');
- * in 'none' mode MathJax handles only