diff --git a/AGENTS.md b/AGENTS.md index 4037ac69..be3ac186 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,10 +3,3 @@ Run `make checks` before committing. `test` runs plain PHP scripts (`php tests/*.php`), not PHPUnit. `phpcs` needs `composer install`, which `make` does automatically. - -`hack/demo/demo.sh` / `make screenshots [demo]` render a demo through a -local Docker MediaWiki and regenerate `docs/screenshots-$MW_VERSION/*.png` -(`MW_VERSION` defaults to `1.43`, e.g. `MW_VERSION=1.45 make screenshots` to -test against a different MediaWiki version). Only run this when asked to, -or when a change affects rendering (`resources/`, `hack/demo/demos.yaml`) -— not for routine changes. diff --git a/Makefile b/Makefile index 8faa1f19..f96edcca 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: checks test phpcs local-mathjax screenshots +.PHONY: checks test phpcs local-mathjax screenshots demo down animations LOCAL_MATHJAX_VERSION ?= 4.1.3 @@ -18,15 +18,21 @@ local-mathjax: ## Pin the bundled local MathJax submodule, e.g. `make local-math vendor/autoload.php: composer.json composer install --no-progress -screenshots: ## Screenshot a demo page, e.g. `make screenshots custom01` (no demo = every demo in hack/demo/demos.yaml); `MW_VERSION=1.45 make screenshots` targets a different mediawiki Docker image tag (default 1.43) +demo: ## Start (or reuse) a local demo wiki at http://localhost:8080 (Admin / demo12345678), e.g. `make demo 1.45` + MW_VERSION=$(or $(filter-out $@,$(MAKECMDGOALS)),1.43) hack/demo/demo.sh up + +down: ## Stop the local demo wiki and wipe its data + hack/demo/demo.sh down + +screenshots: ## Screenshot a demo page, e.g. `make screenshots custom01` (no demo = every demo) hack/demo/demo.sh screenshot $(filter-out $@,$(MAKECMDGOALS)) -# Swallows the extra word in `make screenshots custom01` so make doesn't -# treat "custom01" as a target of its own and fail with "No rule to make -# target". Scoped to only fire when `screenshots` is actually one of the -# invoked goals, so an unrelated typo like `make cheks` still fails loudly -# instead of silently no-op'ing. -ifneq ($(filter screenshots,$(MAKECMDGOALS)),) +animations: ## Record a VisualEditor insert animation per doc in hack/demo/demo-animations.yaml, e.g. `make animations 1.45` + hack/demo/demo.sh animations $(filter-out $@,$(MAKECMDGOALS)) + +# Swallows the extra word in `make screenshots custom01` / `make demo 1.45` +# so make doesn't treat it as a target of its own and fail. +ifneq ($(filter screenshots demo animations,$(MAKECMDGOALS)),) %: @: endif diff --git a/docs/animations/1.43/animation-default.gif b/docs/animations/1.43/animation-default.gif new file mode 100644 index 00000000..73c99b5e Binary files /dev/null and b/docs/animations/1.43/animation-default.gif differ diff --git a/docs/screenshots-1.43/screenshot-custom01-diff.png b/docs/screenshots/1.43/screenshot-custom01-diff.png similarity index 100% rename from docs/screenshots-1.43/screenshot-custom01-diff.png rename to docs/screenshots/1.43/screenshot-custom01-diff.png diff --git a/docs/screenshots-1.43/screenshot-custom01-rightclick.png b/docs/screenshots/1.43/screenshot-custom01-rightclick.png similarity index 100% rename from docs/screenshots-1.43/screenshot-custom01-rightclick.png rename to docs/screenshots/1.43/screenshot-custom01-rightclick.png diff --git a/docs/screenshots-1.43/screenshot-custom01.png b/docs/screenshots/1.43/screenshot-custom01.png similarity index 100% rename from docs/screenshots-1.43/screenshot-custom01.png rename to docs/screenshots/1.43/screenshot-custom01.png diff --git a/docs/screenshots-1.43/screenshot-custom02-diff.png b/docs/screenshots/1.43/screenshot-custom02-diff.png similarity index 100% rename from docs/screenshots-1.43/screenshot-custom02-diff.png rename to docs/screenshots/1.43/screenshot-custom02-diff.png diff --git a/docs/screenshots-1.43/screenshot-custom02.png b/docs/screenshots/1.43/screenshot-custom02.png similarity index 100% rename from docs/screenshots-1.43/screenshot-custom02.png rename to docs/screenshots/1.43/screenshot-custom02.png diff --git a/docs/screenshots-1.43/screenshot-default01.png b/docs/screenshots/1.43/screenshot-default01.png similarity index 100% rename from docs/screenshots-1.43/screenshot-default01.png rename to docs/screenshots/1.43/screenshot-default01.png diff --git a/docs/screenshots-1.43/screenshot-default02-diff.png b/docs/screenshots/1.43/screenshot-default02-diff.png similarity index 100% rename from docs/screenshots-1.43/screenshot-default02-diff.png rename to docs/screenshots/1.43/screenshot-default02-diff.png diff --git a/docs/screenshots-1.43/screenshot-default02-rightclick.png b/docs/screenshots/1.43/screenshot-default02-rightclick.png similarity index 100% rename from docs/screenshots-1.43/screenshot-default02-rightclick.png rename to docs/screenshots/1.43/screenshot-default02-rightclick.png diff --git a/docs/screenshots-1.43/screenshot-default02.png b/docs/screenshots/1.43/screenshot-default02.png similarity index 100% rename from docs/screenshots-1.43/screenshot-default02.png rename to docs/screenshots/1.43/screenshot-default02.png diff --git a/docs/screenshots-1.45/screenshot-custom01-diff.png b/docs/screenshots/1.45/screenshot-custom01-diff.png similarity index 100% rename from docs/screenshots-1.45/screenshot-custom01-diff.png rename to docs/screenshots/1.45/screenshot-custom01-diff.png diff --git a/docs/screenshots-1.45/screenshot-custom01-rightclick.png b/docs/screenshots/1.45/screenshot-custom01-rightclick.png similarity index 100% rename from docs/screenshots-1.45/screenshot-custom01-rightclick.png rename to docs/screenshots/1.45/screenshot-custom01-rightclick.png diff --git a/docs/screenshots-1.45/screenshot-custom01.png b/docs/screenshots/1.45/screenshot-custom01.png similarity index 100% rename from docs/screenshots-1.45/screenshot-custom01.png rename to docs/screenshots/1.45/screenshot-custom01.png diff --git a/docs/screenshots-1.45/screenshot-custom02-diff.png b/docs/screenshots/1.45/screenshot-custom02-diff.png similarity index 100% rename from docs/screenshots-1.45/screenshot-custom02-diff.png rename to docs/screenshots/1.45/screenshot-custom02-diff.png diff --git a/docs/screenshots-1.45/screenshot-custom02.png b/docs/screenshots/1.45/screenshot-custom02.png similarity index 100% rename from docs/screenshots-1.45/screenshot-custom02.png rename to docs/screenshots/1.45/screenshot-custom02.png diff --git a/docs/screenshots-1.45/screenshot-default01.png b/docs/screenshots/1.45/screenshot-default01.png similarity index 100% rename from docs/screenshots-1.45/screenshot-default01.png rename to docs/screenshots/1.45/screenshot-default01.png diff --git a/docs/screenshots-1.45/screenshot-default02-diff.png b/docs/screenshots/1.45/screenshot-default02-diff.png similarity index 100% rename from docs/screenshots-1.45/screenshot-default02-diff.png rename to docs/screenshots/1.45/screenshot-default02-diff.png diff --git a/docs/screenshots-1.45/screenshot-default02-rightclick.png b/docs/screenshots/1.45/screenshot-default02-rightclick.png similarity index 100% rename from docs/screenshots-1.45/screenshot-default02-rightclick.png rename to docs/screenshots/1.45/screenshot-default02-rightclick.png diff --git a/docs/screenshots-1.45/screenshot-default02.png b/docs/screenshots/1.45/screenshot-default02.png similarity index 100% rename from docs/screenshots-1.45/screenshot-default02.png rename to docs/screenshots/1.45/screenshot-default02.png diff --git a/extension.json b/extension.json index 4d4025d8..1ae045dd 100644 --- a/extension.json +++ b/extension.json @@ -1,6 +1,6 @@ { "name": "SimpleMathJax", - "version": "1.0.1", + "version": "1.1.0", "author": "jmnote", "url": "https://www.mediawiki.org/wiki/Extension:SimpleMathJax", "description": "render TeX between and ", @@ -58,11 +58,56 @@ "ParserFirstCallInit": "MediaWiki\\Extension\\SimpleMathJax\\Hooks::onParserFirstCallInit", "InternalParseBeforeLinks": "MediaWiki\\Extension\\SimpleMathJax\\Hooks::onInternalParseBeforeLinks" }, + "attributes": { + "VisualEditor": { + "PluginModules": [ + "ext.SimpleMathJax.visualEditor" + ] + } + }, + "MessagesDirs": { + "SimpleMathJax": [ + "i18n" + ] + }, "ResourceModules": { "ext.SimpleMathJax": { "scripts": [ "resources/ext.SimpleMathJax.js" ] + }, + "ext.SimpleMathJax.visualEditor": { + "scripts": [ + "resources/ve/ve.dm.MWMathNode.js", + "resources/ve/ve.dm.MWChemNode.js", + "resources/ve/ve.ce.MWFormulaNode.js", + "resources/ve/ve.ce.MWMathNode.js", + "resources/ve/ve.ce.MWChemNode.js", + "resources/ve/ve.ui.MWFormulaDialog.js", + "resources/ve/ve.ui.MWMathDialog.js", + "resources/ve/ve.ui.MWChemDialog.js", + "resources/ve/ve.ui.MWMathDialogTool.js", + "resources/ve/ve.ui.MWChemDialogTool.js", + "resources/ve/ve.ui.MWFormulaContextItem.js", + "resources/ve/ve.ui.MWMathContextItem.js", + "resources/ve/ve.ui.MWChemContextItem.js" + ], + "styles": [ + "resources/ve/ve.ui.MWFormulaDialog.less" + ], + "dependencies": [ + "ext.visualEditor.mwcore", + "ext.SimpleMathJax" + ], + "messages": [ + "simplemathjax-visualeditor-mwmathdialog-title", + "simplemathjax-visualeditor-mwchemdialog-title", + "simplemathjax-visualeditor-mwformuladialog-formula", + "simplemathjax-visualeditor-mwformuladialog-display", + "simplemathjax-visualeditor-mwformuladialog-display-default", + "simplemathjax-visualeditor-mwformuladialog-display-inline", + "simplemathjax-visualeditor-mwformuladialog-display-block" + ] } }, "ResourceFileModulePaths": { @@ -70,4 +115,4 @@ "remoteExtPath": "SimpleMathJax" }, "manifest_version": 2 -} +} \ No newline at end of file diff --git a/hack/demo/demo-animations.yaml b/hack/demo/demo-animations.yaml new file mode 100644 index 00000000..bfa5c98f --- /dev/null +++ b/hack/demo/demo-animations.yaml @@ -0,0 +1,15 @@ +mediawiki: + versions: + - 1.43 + # - 1.45 +viewport: + width: 800 + height: 600 +docs: + - name: default + content: | + 옴(Ohm)의 법칙: + 전압 = 전류 \times 저항 + Chemical reaction: H2O + CO2 -> H2CO3 + + Mass–energy equivalence: E=mc^2 diff --git a/hack/demo/demos.yaml b/hack/demo/demo-screenshots.yaml similarity index 100% rename from hack/demo/demos.yaml rename to hack/demo/demo-screenshots.yaml diff --git a/hack/demo/demo.md b/hack/demo/demo.md new file mode 100644 index 00000000..c4cd175c --- /dev/null +++ b/hack/demo/demo.md @@ -0,0 +1,26 @@ +# Demo tooling + +`hack/demo/demo.sh` (driven by the `Makefile` targets below) runs a local +Docker `mediawiki` image + SQLite, with this repo bind-mounted in as the +extension — no MediaWiki core checkout needed. VisualEditor is bundled with +the image, so `?veaction=edit` works too. + +- `make demo [version]` / `make down` — start/stop the wiki manually at + `http://localhost:8080` (`Admin` / `demo12345678`), e.g. `make demo 1.45` + to target that MediaWiki version instead of the default `1.43`. + +- `make screenshots [demo]` — regenerate `docs/screenshots/$MW_VERSION/*.png` + from `hack/demo/demo-screenshots.yaml` (no `demo` = every demo in that + file). `MW_VERSION=1.45 make screenshots` targets a different MediaWiki + version. Only run this when asked to, or when a change affects rendering + (`resources/`, `demo-screenshots.yaml`) — not for routine changes. + +- `make animations [version]` — drives the wiki through VisualEditor's + Insert menu, per doc listed in `hack/demo/demo-animations.yaml`, + inserting each of that doc's formulas then saving, and records + `docs/animations/$MW_VERSION/animation-.gif` per doc. + `make animations 1.45` targets just that version; with none given it + loops over every version in `demo-animations.yaml`'s own + `mediawiki: versions:` list instead of defaulting to one. Only run this + when asked to, or when a change affects `resources/ve/*.js` — not for + routine changes. diff --git a/hack/demo/demo.sh b/hack/demo/demo.sh index 2599e678..64f27b9a 100755 --- a/hack/demo/demo.sh +++ b/hack/demo/demo.sh @@ -1,17 +1,19 @@ #!/usr/bin/env bash -# Local test wiki: the official `mediawiki` Docker image + SQLite (no -# separate DB container), with this repo bind-mounted in as the extension — -# no MediaWiki core checkout needed. See AGENTS.md. +# Local test wiki: the official `mediawiki` Docker image + SQLite, with this +# repo bind-mounted in as the extension. VisualEditor is also installed +# (bundled with the image) so resources/ve/*.js can be exercised at +# ?veaction=edit. # # Usage: hack/demo/demo.sh [up|down] [demo] # hack/demo/demo.sh screenshot [demo] +# hack/demo/demo.sh animations [version] # MW_VERSION=1.45 hack/demo/demo.sh screenshot [demo] -# `demo` is a top-level key in hack/demo/demos.yaml (e.g. default01, custom01); -# its capture is saved to docs/screenshots-$MW_VERSION/screenshot-.png. -# Defaults to `default01`; `screenshot` with no `demo` given screenshots -# every demo in demos.yaml. MW_VERSION selects the `mediawiki` Docker image -# tag to test against (default 1.43) — e.g. to check whether a bug is -# specific to one MediaWiki version. +# `demo` is a top-level key in demo-screenshots.yaml (default: default01); +# `screenshot` with none given screenshots every demo. MW_VERSION selects +# the `mediawiki` Docker image tag (default 1.43). `animations` takes its +# version as a plain argument (like `screenshot`'s demo, not MW_VERSION), +# and with none given loops over demo-animations.yaml's own +# `mediawiki: versions:` list. set -euo pipefail DEMO_DIR="$(cd "$(dirname "$0")" && pwd)" cd "$DEMO_DIR/../.." @@ -24,7 +26,7 @@ DATA="$PWD/hack/demo/temp" DOCKER_USER=33:33 PASS=demo12345678 PAGE_TITLE=SimpleMathJax -SCREENSHOT_DIR="$PWD/docs/screenshots-$MW_VERSION" +SCREENSHOT_DIR="$PWD/docs/screenshots/$MW_VERSION" # Encodes the configured page title for index.php query-string URLs. API # requests below use curl's --data-urlencode instead. @@ -45,34 +47,26 @@ edit_new_revid() { php -r 'echo json_decode(stream_get_contents(STDIN), true)["edit"]["newrevid"];' } -# Prints a demo's `settings:` block from docs/demos.yaml — the raw PHP +# Prints a demo's `settings:` block from docs/demo-screenshots.yaml — the raw PHP # lines appended into LocalSettings.php (see up()) and shown on the demo # page in a block. See render.php. local_settings_body() { - php "$DEMO_DIR/render.php" "$DEMO_DIR/demos.yaml" "$1" settings + php "$DEMO_DIR/render.php" "$DEMO_DIR/demo-screenshots.yaml" "$1" settings } -# Prints a demo's `examples:` list from docs/demos.yaml, each rendered as a +# Prints a demo's `examples:` list from docs/demo-screenshots.yaml, each rendered as a # syntaxhighlight block next to its live render, in a responsive flex row. # See render.php. render_examples() { - php "$DEMO_DIR/render.php" "$DEMO_DIR/demos.yaml" "$1" examples + php "$DEMO_DIR/render.php" "$DEMO_DIR/demo-screenshots.yaml" "$1" examples } -# Logs in as Admin and edits the configured demo page (not "Main Page", which -# the installer already fills with its own default content) with a small -# demo showing primes inside $...$/$$...$$ surviving wikitext emphasis -# parsing, prefixed with a MediaWiki {{CURRENTVERSION}}/SimpleMathJax -# version line (so a screenshot alone shows what it was captured against — -# useful once screenshots exist for more than one MediaWiki version, see -# MW_VERSION) and the demo's own `settings:` block (wrapped in -# ) for context. Blanks the page first so the demo edit -# always has an empty previous revision to diff against — a real -# two-column diff, not just a "page creation" summary — regardless of how -# many times this runs against the same wiki (see screenshot_one's -# addDiffShot handling). Re-run on every up so editing a demo's entry in -# docs/demos.yaml and re-running `up` (or `screenshot`) always shows the -# latest content. +# Logs in as Admin and edits the configured demo page (not "Main Page") with +# the demo's rendered examples, prefixed with a +# {{CURRENTVERSION}}/SimpleMathJax version line and its `settings:` block. +# Blanks the page first so the edit always has an empty previous revision to +# diff against (see screenshot_one's addDiffShot handling). Re-run on every +# `up` so the page always reflects the latest demo-screenshots.yaml. seed_demo_page() { local demo="$1" local jar url="http://localhost:$PORT/api.php" @@ -117,9 +111,8 @@ seed_demo_page() { } wait_for_wiki() { - # Main Page, not the configured demo page: it's the installer's own - # default page, so it already exists the moment the wiki responds — unlike - # the demo page, which seed_demo_page hasn't created yet at this point. + # Main Page, not the demo page: it exists as soon as the wiki responds, + # unlike the demo page, which seed_demo_page hasn't created yet. for _ in $(seq 1 30); do curl -sf -o /dev/null "http://localhost:$PORT/index.php/Main_Page" && return 0 sleep 1 @@ -143,7 +136,7 @@ up() { --dbtype sqlite --dbpath /var/www/html/data \ --scriptpath "" --server "http://localhost:$PORT" \ --pass "$PASS" \ - --extensions SyntaxHighlight_GeSHi \ + --extensions SyntaxHighlight_GeSHi,VisualEditor \ SimpleMathJax Admin local_settings_body "$demo" >> "$DATA/LocalSettings.php" fi @@ -158,28 +151,19 @@ up() { wait_for_wiki && seed_demo_page "$demo" } -# Stops the container and wipes its data, so the next `up` reinstalls fresh -# (also how you pick up changes to a demo's `settings:` block, which is -# only applied during install). +# Stops the container and wipes its data, so the next `up` reinstalls fresh. down() { docker rm -f "$NAME" >/dev/null 2>&1 || true rm -rf "$DATA" } # Screenshots one demo's page with a real browser (Puppeteer); see -# screenshot.mjs. Forces a fresh install (down, then up) so the demo's own -# `settings:` block is guaranteed to be the one in effect, then saves to -# $SCREENSHOT_DIR/screenshot-.png (docs/screenshots-$MW_VERSION by -# default). A demo with `addRightClickShot: true` (see render.php) gets one -# extra screenshot, screenshot--rightclick.png, after right-clicking -# its first mjx-container — needed to show MathJax's context menu, e.g. for -# $wgSmjEnableMenu, since the normal capture never triggers one. A demo with -# `addDiffShot: true` gets a separate extra screenshot, -# screenshot--diff.png, of the demo page's diff against the blank -# revision seed_demo_page saves right before its real edit — needed to show -# $wgSmjIgnoreHtmlClass keeping bare-delimiter scanning out of diff views, -# since a diff is a different page/URL entirely, not something a click on -# the normal capture can reveal. +# screenshot.mjs. Forces a fresh install (down, then up), then saves to +# $SCREENSHOT_DIR/screenshot-.png. `addRightClickShot: true` adds +# screenshot--rightclick.png (MathJax's context menu, e.g. for +# $wgSmjEnableMenu). `addDiffShot: true` adds screenshot--diff.png, +# the demo page's diff against the blank seed revision (e.g. for +# $wgSmjIgnoreHtmlClass). screenshot_one() { local demo="$1" down @@ -189,7 +173,7 @@ screenshot_one() { ( cd "$DEMO_DIR" && npm install ) fi local right_click="" - if [ "$(php "$DEMO_DIR/render.php" "$DEMO_DIR/demos.yaml" "$demo" addrightclickshot)" = "true" ]; then + if [ "$(php "$DEMO_DIR/render.php" "$DEMO_DIR/demo-screenshots.yaml" "$demo" addrightclickshot)" = "true" ]; then right_click=mjx-container fi mkdir -p "$SCREENSHOT_DIR" @@ -201,7 +185,7 @@ screenshot_one() { RIGHT_CLICK_OUT="$SCREENSHOT_DIR/screenshot-$demo-rightclick.png" \ node screenshot.mjs ) - if [ "$(php "$DEMO_DIR/render.php" "$DEMO_DIR/demos.yaml" "$demo" adddiffshot)" = "true" ]; then + if [ "$(php "$DEMO_DIR/render.php" "$DEMO_DIR/demo-screenshots.yaml" "$demo" adddiffshot)" = "true" ]; then local revid revid=$(<"$DATA/last_revid") ( @@ -213,29 +197,74 @@ screenshot_one() { fi } -# Screenshots every demo in docs/demos.yaml when none is named. +# Screenshots every demo in docs/demo-screenshots.yaml when none is named. screenshot() { if [ -n "${1:-}" ]; then screenshot_one "$1" return fi - # Regenerating every demo: clear old captures first so a demo that got - # renamed or removed from demos.yaml doesn't leave a stale screenshot - # behind under its old name. + # Clear old captures first so a renamed/removed demo doesn't leave a + # stale screenshot behind. mkdir -p "$SCREENSHOT_DIR" rm -f "$SCREENSHOT_DIR"/screenshot-*.png local demo - for demo in $(sed -n 's/^- name: //p' "$DEMO_DIR/demos.yaml"); do + for demo in $(sed -n 's/^- name: //p' "$DEMO_DIR/demo-screenshots.yaml"); do screenshot_one "$demo" done } +# demo-animations.yaml's optional top-level `mediawiki: versions:` list, +# consulted only when animations() has no version argument; falls back to +# $MW_VERSION if that block is missing. +animation_versions() { + local versions + versions=$(awk '/^mediawiki:/ { f=1; next } /^[a-zA-Z]/ { f=0 } f' "$DEMO_DIR/demo-animations.yaml" | + sed -n 's/^[[:space:]]*-[[:space:]]*//p') + echo "${versions:-$MW_VERSION}" +} + +# Records docs/animations/$MW_VERSION/animation-.gif for each doc in +# demo-animations.yaml: inserting that doc's formulas via VisualEditor's +# Insert menu (see resources/ve/*.js), then saving. Forces a fresh install +# (down, then up). +animations_one() { + IMAGE="mediawiki:$MW_VERSION" + down + up + if [ ! -d "$DEMO_DIR/node_modules" ]; then + echo "==> Installing animation dependencies (npm install)" + ( cd "$DEMO_DIR" && npm install ) + fi + local animation_dir="$PWD/docs/animations/$MW_VERSION" + mkdir -p "$animation_dir" + ( + cd "$DEMO_DIR" && + URL="http://localhost:$PORT" \ + OUT_DIR="$animation_dir" \ + node ve_animation.mjs + ) +} + +# Runs animations_one for a single explicit version (an argument, like +# screenshot's demo), or once per version in animation_versions. +animations() { + if [ -n "${1:-}" ]; then + MW_VERSION="$1" animations_one + return + fi + local v + for v in $(animation_versions); do + MW_VERSION="$v" animations_one + done +} + case "${1:-up}" in up) up "${2:-}" ;; down) down ;; screenshot) screenshot "${2:-}" ;; + animations) animations "${2:-}" ;; *) - echo "Usage: $0 [up|down] [demo] | $0 screenshot [demo]" >&2 + echo "Usage: $0 [up|down] [demo] | $0 screenshot [demo] | $0 animations [version]" >&2 exit 1 ;; esac diff --git a/hack/demo/package-lock.json b/hack/demo/package-lock.json index c5e71d50..bb71c4f8 100644 --- a/hack/demo/package-lock.json +++ b/hack/demo/package-lock.json @@ -8,7 +8,20 @@ "devDependencies": { "@fontsource/noto-sans-kr": "^5.3.0", "@fontsource/noto-serif-kr": "^5.3.0", - "puppeteer": "^25.10.0" + "gif-encoder-2": "^1.0.5", + "puppeteer": "^25.10.0", + "sharp": "^0.35.4" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, "node_modules/@fontsource/noto-sans-kr": { @@ -31,6 +44,533 @@ "url": "https://github.com/sponsors/ayuhito" } }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@puppeteer/browsers": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.2.tgz", @@ -136,6 +676,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/devtools-protocol": { "version": "0.0.1666840", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1666840.tgz", @@ -183,6 +733,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gif-encoder-2": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/gif-encoder-2/-/gif-encoder-2-1.0.5.tgz", + "integrity": "sha512-fsRAKbZuUoZ7FYGjpFElmflTkKwsn/CzAmL/xDl4558aTAgysIDCUF6AXWO8dmai/ApfZACbPVAM+vPezJXlFg==", + "dev": true, + "license": "UNLICENSE" + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -253,6 +810,69 @@ "node": ">=22.12.0" } }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/string-width": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", @@ -286,6 +906,14 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/typed-query-selector": { "version": "2.12.2", "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", diff --git a/hack/demo/package.json b/hack/demo/package.json index 740398b3..70d0352c 100644 --- a/hack/demo/package.json +++ b/hack/demo/package.json @@ -3,11 +3,14 @@ "private": true, "description": "Dev-only tooling for the local test wiki (see demo.sh). Not part of the SimpleMathJax extension itself.", "scripts": { - "screenshot": "node screenshot.mjs" + "screenshot": "node screenshot.mjs", + "animation": "node ve_animation.mjs" }, "devDependencies": { "@fontsource/noto-sans-kr": "^5.3.0", "@fontsource/noto-serif-kr": "^5.3.0", - "puppeteer": "^25.10.0" + "gif-encoder-2": "^1.0.5", + "puppeteer": "^25.10.0", + "sharp": "^0.35.4" } } diff --git a/hack/demo/render.php b/hack/demo/render.php index 512c15e1..d6bc7ef1 100644 --- a/hack/demo/render.php +++ b/hack/demo/render.php @@ -1,35 +1,15 @@ settings prints `wfLoadExtension( -// 'SimpleMathJax' );` followed by the -// demo's `settings:` literal block -// (its $wgSmj* overrides only — every -// demo needs the wfLoadExtension line, -// so it isn't repeated in demos.yaml) -// — raw PHP appended into -// LocalSettings.php (see up()) and -// shown on the demo page in a -// block. -// render.php examples prints the demo's `examples:` list -// as wikitext, each example as a -// syntaxhighlight block next to its -// live render, laid out in a -// responsive flex row. -// render.php addrightclickshot prints "true" if the demo has -// `addRightClickShot: true`. -// render.php adddiffshot prints "true" if the demo has -// `addDiffShot: true`. +// Renders one field of one demo out of docs/demo-screenshots.yaml for demo.sh: +// render.php settings the demo's settings: block as +// PHP appended into LocalSettings.php +// render.php examples the demo's examples: list as wikitext +// render.php addrightclickshot "true" if addRightClickShot: true +// render.php adddiffshot "true" if addDiffShot: true // -// Only this narrow shape is supported, not general YAML: a top-level -// sequence of demo items (`- name: `), each an optional `settings:` -// literal block scalar (`|`), an `examples:` block sequence whose items are -// either quoted scalars or their own `- |` literal block scalar, and an -// optional `addRightClickShot: true` / `addDiffShot: true` scalar. -// YAML's C-style escaping for \\, \" and \n inside a double-quoted scalar -// is a strict subset of JSON's, so each quoted example is unescaped by -// wrapping it in JSON quotes and handing it to json_decode — this project -// already requires php (see demo.sh's json_field()), so reusing it here -// avoids a YAML library dependency just for this. +// Only this narrow YAML shape is supported: a top-level sequence of demo +// items (`- name: `), each an optional `settings:` literal block +// scalar (`|`), an `examples:` block sequence of quoted or `- |` block +// scalars, and optional `addRightClickShot:`/`addDiffShot:` booleans. [ , $path, $demo, $mode ] = $argv; @@ -115,12 +95,9 @@ $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. + // A `- |` literal block scalar, for long examples (e.g. a + // multi-line continued fraction) that read better unescaped than + // squeezed into one quoted line. if ( preg_match( '/^(\s*)-\s*\|\s*$/', $line, $m ) ) { $itemIndent = strlen( $m[1] ); $blockLines = []; @@ -128,10 +105,6 @@ 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; } @@ -146,13 +119,8 @@ $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.
+	// column-count fragmentation would otherwise collapse the first
+	// example's top margin into the page above it; this cancels that out.
 	echo '
'; foreach ( $examples as $example ) { echo "$example $example\n"; diff --git a/hack/demo/ve_animation.mjs b/hack/demo/ve_animation.mjs new file mode 100644 index 00000000..93479c57 --- /dev/null +++ b/hack/demo/ve_animation.mjs @@ -0,0 +1,302 @@ +#!/usr/bin/env node +import puppeteer from 'puppeteer'; +import GIFEncoder from 'gif-encoder-2'; +import sharp from 'sharp'; +import fs from 'node:fs'; +import path from 'node:path'; + +const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname); +const BASE = process.env.URL ?? 'http://localhost:8080'; +const OUT_DIR = process.env.OUT_DIR ?? '../../docs/animations'; + +function readViewport(content) { + const match = content.match(/^viewport:\s*\n\s+width:\s*(\d+)\s*\n\s+height:\s*(\d+)\s*$/m); + return match ? + { width: Number(match[1]), height: Number(match[2]) } : + { width: 1280, height: 900 }; +} + +function readDocs(content) { + const lines = content.split('\n'); + const docs = []; + let i = 0; + while (i < lines.length) { + const nameMatch = lines[i].match(/^\s*-\s*name:\s*(.+?)\s*$/); + if (!nameMatch) { + i++; + continue; + } + const name = nameMatch[1]; + i++; + let content = ''; + while (i < lines.length && !/^\s*-\s*name:\s*/.test(lines[i])) { + if (!/^\s*content:\s*\|\s*$/.test(lines[i])) { + i++; + continue; + } + i++; + const blockLines = []; + let blockIndent = null; + while (i < lines.length) { + const line = lines[i]; + if (line.trim() === '') { + blockLines.push(''); + i++; + continue; + } + const thisIndent = line.match(/^\s*/)[0].length; + if (blockIndent === null) { + blockIndent = thisIndent; + } else if (thisIndent < blockIndent) { + break; + } + blockLines.push(line.slice(blockIndent)); + i++; + } + content = blockLines.join('\n'); + } + docs.push({ name, content }); + } + return docs; +} + +function parseContent(content) { + const tagRe = /<(math|chem)(?:\s+display="(default|inline|block)")?>([\s\S]*?)<\/\1>/g; + const segments = []; + let lastIndex = 0; + let match; + while ((match = tagRe.exec(content)) !== null) { + segments.push({ text: content.slice(lastIndex, match.index) }); + const [, tag, display, formula] = match; + segments.push({ toolClass: tag === 'math' ? 'smjMath' : 'smjChem', formula, display: display ?? 'default' }); + lastIndex = tagRe.lastIndex; + } + segments.push({ text: content.slice(lastIndex) }); + return segments; +} + +function localFontCss(packageName) { + const packageDir = path.join(SCRIPT_DIR, 'node_modules', '@fontsource', packageName); + const 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')})`; + }); +} + +async function injectCjkFonts(page) { + await page.addStyleTag({ content: localFontCss('noto-sans-kr') }); + await page.addStyleTag({ content: localFontCss('noto-serif-kr') }); + await page.addStyleTag({ + content: ` + body, .oo-ui-inputWidget-input, .ve-ui-linearContextItem-body { font-family: 'Noto Sans KR', sans-serif !important; } + mjx-utext { font-family: 'Noto Serif KR', serif !important; } + ` + }); + await page.evaluate(() => document.fonts.ready); +} + +async function recordDoc(browser, name, segments, viewport, outDir) { + const pageTitle = `SmjVeDemo-${name}`; + const { width: WIDTH, height: HEIGHT } = viewport; + const encoder = new GIFEncoder(WIDTH, HEIGHT, 'octree'); + encoder.start(); + encoder.setRepeat(0); + encoder.setQuality(15); + + async function addFrame(page, holdMs) { + const png = await page.screenshot({ type: 'png' }); + const { data } = await sharp(png) + .ensureAlpha() + .raw() + .toBuffer({ resolveWithObject: true }); + encoder.setDelay(holdMs); + encoder.addFrame(data); + } + + const page = await browser.newPage(); + await page.setViewport(viewport); + + await page.goto(`${BASE}/index.php?title=Special:UserLogin&returnto=Main+Page`, { waitUntil: 'networkidle0' }); + await page.type('#wpName1', 'Admin'); + await page.type('#wpPassword1', 'demo12345678'); + await Promise.all([ + page.click('#wpLoginAttempt'), + page.waitForNavigation({ waitUntil: 'networkidle0' }) + ]); + const csrf = await page.evaluate(async () => { + const res = await fetch('/api.php?action=query&meta=tokens&format=json', { credentials: 'same-origin' }); + return (await res.json()).query.tokens.csrftoken; + }); + await page.evaluate(async (title, token) => { + await fetch('/api.php', { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ action: 'edit', title, text: '', token, format: 'json' }) + }); + }, pageTitle, csrf); + + await page.goto(`${BASE}/index.php?title=${encodeURIComponent(pageTitle)}&veaction=edit`, { waitUntil: 'networkidle0' }); + await page.waitForSelector('.ve-ui-toolbar', { timeout: 30000 }); + await injectCjkFonts(page); + await new Promise((r) => setTimeout(r, 2000)); + for (const b of await page.$$('.oo-ui-buttonElement-button')) { + const text = await page.evaluate((el) => el.textContent, b); + if (text && text.trim() === 'Start editing') { + await b.click(); + await new Promise((r) => setTimeout(r, 1500)); + break; + } + } + await page.click('.ve-ce-documentNode').catch(() => { }); + await new Promise((r) => setTimeout(r, 500)); + await addFrame(page, 1200); + + async function clickButtonByText(text) { + for (const b of await page.$$('.oo-ui-buttonElement-button')) { + const t = await page.evaluate((el) => el.textContent, b); + if (t && t.trim() === text) { + await b.click(); + return true; + } + } + return false; + } + + async function openInsertMenu(toolClass) { + for (const el of await page.$$('.oo-ui-popupToolGroup-handle')) { + const text = await page.evaluate((e) => e.textContent.trim(), el); + if (text === 'Insert') { + await el.click(); + break; + } + } + await new Promise((r) => setTimeout(r, 500)); + const alreadyVisible = await page.evaluate((cls) => { + const el = document.querySelector(`.oo-ui-tool-name-${cls} .oo-ui-tool-link`); + if (!el) { + return false; + } + const rect = el.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; + }, toolClass); + if (!alreadyVisible) { + for (const el of await page.$$('.oo-ui-tool-name-more-fewer .oo-ui-tool-link')) { + if (await el.isIntersectingViewport().catch(() => false)) { + await el.click(); + break; + } + } + await new Promise((r) => setTimeout(r, 500)); + } + } + + async function typeFormulaWithFrames(text, chunkSize) { + for (let i = 0; i < text.length; i += chunkSize) { + await page.keyboard.type(text.slice(i, i + chunkSize)); + await addFrame(page, 120); + } + } + + async function typeMultiline(text) { + for (const part of text.split(/(\n+)/)) { + if (/^\n\n+$/.test(part)) { + await page.keyboard.press('Enter'); + } else if (part && part !== '\n') { + await page.keyboard.type(part); + } + } + } + + async function clickDisplayButton(display) { + const label = { default: 'Default', inline: 'Inline', block: 'Block' }[display] ?? 'Default'; + for (let attempt = 0; attempt < 5; attempt++) { + for (const b of await page.$$('.oo-ui-buttonOptionWidget')) { + const text = await page.evaluate((el) => el.textContent.trim(), b); + if (text !== label) { + continue; + } + try { + await b.click(); + return; + } catch { + await new Promise((r) => setTimeout(r, 300)); + } + } + } + } + + async function insertFormula(toolClass, formulaText, display) { + await openInsertMenu(toolClass); + await addFrame(page, 900); + const tool = await page.waitForSelector(`.oo-ui-tool-name-${toolClass} .oo-ui-tool-link`, { timeout: 10000, visible: true }); + await tool.click(); + await new Promise((r) => setTimeout(r, 800)); + await addFrame(page, 900); + await page.waitForSelector('.ve-ui-smjFormulaDialog-content textarea', { timeout: 10000 }); + await typeFormulaWithFrames(formulaText, 4); + if (display && display !== 'default') { + await clickDisplayButton(display); + await new Promise((r) => setTimeout(r, 300)); + } + await new Promise((r) => setTimeout(r, 1500)); + await addFrame(page, 2000); + await clickButtonByText('Insert'); + await new Promise((r) => setTimeout(r, 1200)); + await addFrame(page, 1800); + } + + for (const segment of segments) { + if ('toolClass' in segment) { + console.log(`[${name}] Inserting ${segment.formula}`); + await insertFormula(segment.toolClass, segment.formula, segment.display); + await page.keyboard.press('ArrowRight'); + await new Promise((r) => setTimeout(r, 200)); + } else if (segment.text) { + await typeMultiline(segment.text); + await addFrame(page, 1200); + } + } + + for (const el of await page.$$('.oo-ui-tool-name-showSave .oo-ui-tool-link')) { + await el.click(); + break; + } + await new Promise((r) => setTimeout(r, 1200)); + await addFrame(page, 1200); + for (let i = 0; i < 5 && /veaction=edit/.test(page.url()); i++) { + await clickButtonByText('Save page') || await clickButtonByText('Save changes'); + await new Promise((r) => setTimeout(r, 1200)); + } + await page.reload({ waitUntil: 'load', timeout: 60000 }).catch(() => { }); + await injectCjkFonts(page); + await page.waitForFunction( + () => Array.from(document.querySelectorAll('span.smj-container')) + .some((el) => getComputedStyle(el).opacity === '1'), + { timeout: 20000 } + ).catch(() => { }); + await addFrame(page, 5000); + + await page.close(); + + encoder.finish(); + const outPath = path.resolve(SCRIPT_DIR, outDir, `animation-${name}.gif`); + fs.mkdirSync(path.dirname(outPath), { recursive: true }); + fs.writeFileSync(outPath, encoder.out.getData()); + console.log(`==> Saved ${outPath}`); +} + +const yamlContent = fs.readFileSync(path.join(SCRIPT_DIR, 'demo-animations.yaml'), 'utf8'); +const viewport = readViewport(yamlContent); +const docs = readDocs(yamlContent); +const browser = await puppeteer.launch( + process.getuid?.() === 0 ? { args: ['--no-sandbox'] } : {} +); +try { + for (const { name, content } of docs) { + await recordDoc(browser, name, parseContent(content), viewport, OUT_DIR); + } +} finally { + await browser.close(); +} diff --git a/i18n/en.json b/i18n/en.json new file mode 100644 index 00000000..9f7f2cc6 --- /dev/null +++ b/i18n/en.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [] + }, + "simplemathjax-visualeditor-mwmathdialog-title": "Math formula", + "simplemathjax-visualeditor-mwchemdialog-title": "Chemical formula", + "simplemathjax-visualeditor-mwformuladialog-formula": "Formula", + "simplemathjax-visualeditor-mwformuladialog-display": "Display", + "simplemathjax-visualeditor-mwformuladialog-display-default": "Default", + "simplemathjax-visualeditor-mwformuladialog-display-inline": "Inline", + "simplemathjax-visualeditor-mwformuladialog-display-block": "Block" +} diff --git a/i18n/ko.json b/i18n/ko.json new file mode 100644 index 00000000..bd64497d --- /dev/null +++ b/i18n/ko.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [] + }, + "simplemathjax-visualeditor-mwmathdialog-title": "수학식", + "simplemathjax-visualeditor-mwchemdialog-title": "화학식", + "simplemathjax-visualeditor-mwformuladialog-formula": "식", + "simplemathjax-visualeditor-mwformuladialog-display": "표시", + "simplemathjax-visualeditor-mwformuladialog-display-default": "기본값", + "simplemathjax-visualeditor-mwformuladialog-display-inline": "인라인", + "simplemathjax-visualeditor-mwformuladialog-display-block": "블록" +} \ No newline at end of file diff --git a/i18n/qqq.json b/i18n/qqq.json new file mode 100644 index 00000000..eb693f6b --- /dev/null +++ b/i18n/qqq.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [] + }, + "simplemathjax-visualeditor-mwmathdialog-title": "Title of the VisualEditor dialog for inserting/editing a SimpleMathJax formula, and label of the Insert-menu tool and context item that open it.", + "simplemathjax-visualeditor-mwchemdialog-title": "Title of the VisualEditor dialog for inserting/editing a SimpleMathJax chemical formula, and label of the Insert-menu tool and context item that open it.", + "simplemathjax-visualeditor-mwformuladialog-formula": "Label of the TeX/chem input field in the VisualEditor formula dialog (shared by the and dialogs).", + "simplemathjax-visualeditor-mwformuladialog-display": "Label of the display-mode picker (default/inline/block) in the VisualEditor formula dialog (shared by the and dialogs).", + "simplemathjax-visualeditor-mwformuladialog-display-default": "Option label: leave the display mode unset, i.e. no display=\"...\" attribute is written.", + "simplemathjax-visualeditor-mwformuladialog-display-inline": "Option label: force inline display, i.e. display=\"inline\".", + "simplemathjax-visualeditor-mwformuladialog-display-block": "Option label: force block display, i.e. display=\"block\"." +} diff --git a/resources/ext.SimpleMathJax.js b/resources/ext.SimpleMathJax.js index a872fe9f..d772ce13 100644 --- a/resources/ext.SimpleMathJax.js +++ b/resources/ext.SimpleMathJax.js @@ -1,146 +1,195 @@ -mw.hook( 'wikipage.content' ).add( function ( $content ) { -window.MathJax = { - tex: { - inlineMath: mw.config.get('wgSmjDelimitersInlineMath').concat([['[math]','[/math]']]), - displayMath: mw.config.get('wgSmjDelimitersDisplayMath'), - processEnvironments: true, - processRefs: mw.config.get('wgSmjDelimitersEnabled'), - processEscapes: mw.config.get('wgSmjDelimitersEnabled'), - packages: mw.config.exists('smjPreloadChem') ? {'[+]': ['autoload','mhchem']} : {'[+]': ['autoload']}, - macros: { - AA: "{\u00c5}", - alef: "{\\aleph}", - alefsym: "{\\aleph}", - Alpha: "{\\mathrm{A}}", - and: "{\\land}", - ang: "{\\angle}", - Bbb: "{\\mathbb}", - Beta: "{\\mathrm{B}}", - bold: "{\\mathbf}", - bull: "{\\bullet}", - C: "{\\mathbb{C}}", - Chi: "{\\mathrm{X}}", - clubs: "{\\clubsuit}", - cnums: "{\\mathbb{C}}", - Complex: "{\\mathbb{C}}", - coppa: "{\u03D9}", - Coppa: "{\u03D8}", - Dagger: "{\\ddagger}", - Digamma: "{\u03DC}", - darr: "{\\downarrow}", - dArr: "{\\Downarrow}", - Darr: "{\\Downarrow}", - dashint: "{\\unicodeInt{x2A0D}}", - ddashint: "{\\unicodeInt{x2A0E}}", - diamonds: "{\\diamondsuit}", - empty: "{\\emptyset}", - Epsilon: "{\\mathrm{E}}", - Eta: "{\\mathrm{H}}", - euro: "{\u20AC}", - exist: "{\\exists}", - geneuro: "{\u20AC}", - geneuronarrow: "{\u20AC}", - geneurowide: "{\u20AC}", - H: "{\\mathbb{H}}", - hAar: "{\\Leftrightarrow}", - harr: "{\\leftrightarrow}", - Harr: "{\\Leftrightarrow}", - hearts: "{\\heartsuit}", - image: "{\\Im}", - infin: "{\\infty}", - Iota: "{\\mathrm{I}}", - isin: "{\\in}", - Kappa: "{\\mathrm{K}}", - koppa: "{\u03DF}", - Koppa: "{\u03DE}", - lang: "{\\langle}", - larr: "{\\leftarrow}", - Larr: "{\\Leftarrow}", - lArr: "{\\Leftarrow}", - lrarr: "{\\leftrightarrow}", - Lrarr: "{\\Leftrightarrow}", - lrArr: "{\\Leftrightarrow}", - Mu: "{\\mathrm{M}}", - N: "{\\mathbb{N}}", - natnums: "{\\mathbb{N}}", - Nu: "{\\mathrm{N}}", - O: "{\\emptyset}", - oiint: "{\\unicodeInt{x222F}}", - oiiint: "{\\unicodeInt{x2230}}", - ointctrclockwise: "{\\unicodeInt{x2233}}", - officialeuro: "{\u20AC}", - Omicron: "{\\mathrm{O}}", - or: "{\\lor}", - P: "{\u00B6}", - pagecolor: ["",1], - part: "{\\partial}", - plusmn: "{\\pm}", - Q: "{\\mathbb{Q}}", - R: "{\\mathbb{R}}", - rang: "{\\rangle}", - rarr: "{\\rightarrow}", - Rarr: "{\\Rightarrow}", - rArr: "{\\Rightarrow}", - real: "{\\Re}", - reals: "{\\mathbb{R}}", - Reals: "{\\mathbb{R}}", - Rho: "{\\mathrm{P}}", - sdot: "{\\cdot}", - sampi: "{\u03E1}", - Sampi: "{\u03E0}", - sect: "{\\S}", - spades: "{\\spadesuit}", - stigma: "{\u03DB}", - Stigma: "{\u03DA}", - sub: "{\\subset}", - sube: "{\\subseteq}", - supe: "{\\supseteq}", - Tau: "{\\mathrm{T}}", - textvisiblespace: "{\u2423}", - thetasym: "{\\vartheta}", - uarr: "{\\uparrow}", - uArr: "{\\Uparrow}", - Uarr: "{\\Uparrow}", - unicodeInt: ["{\\mathop{\\vcenter{\\mathchoice{\\huge\\unicode{#1}\\,}{\\unicode{#1}}{\\unicode{#1}}{\\unicode{#1}}}\\,}\\nolimits}", 1], - varcoppa: "{\u03D9}", - varstigma: "{\u03DB}", - varointclockwise: "{\\unicodeInt{x2232}}", - vline: ["{\\smash{\\large\\lvert #1}", 0], - weierp: "{\\wp}", - Z: "{\\mathbb{Z}}", - Zeta: "{\\mathrm{Z}}" +mw.libs.smj = mw.libs.smj || {}; +(function () { +var loaded = false; +var scriptLoadPromise = null; + +function ensureLoaded() { + if (loaded) { + return; + } + loaded = true; + window.MathJax = { + tex: { + inlineMath: mw.config.get('wgSmjDelimitersInlineMath').concat([['[math]','[/math]']]), + displayMath: mw.config.get('wgSmjDelimitersDisplayMath'), + processEnvironments: true, + processRefs: mw.config.get('wgSmjDelimitersEnabled'), + processEscapes: mw.config.get('wgSmjDelimitersEnabled'), + packages: mw.config.exists('smjPreloadChem') ? {'[+]': ['autoload','mhchem']} : {'[+]': ['autoload']}, + macros: { + AA: "{\u00c5}", + alef: "{\\aleph}", + alefsym: "{\\aleph}", + Alpha: "{\\mathrm{A}}", + and: "{\\land}", + ang: "{\\angle}", + Bbb: "{\\mathbb}", + Beta: "{\\mathrm{B}}", + bold: "{\\mathbf}", + bull: "{\\bullet}", + C: "{\\mathbb{C}}", + Chi: "{\\mathrm{X}}", + clubs: "{\\clubsuit}", + cnums: "{\\mathbb{C}}", + Complex: "{\\mathbb{C}}", + coppa: "{\u03D9}", + Coppa: "{\u03D8}", + Dagger: "{\\ddagger}", + Digamma: "{\u03DC}", + darr: "{\\downarrow}", + dArr: "{\\Downarrow}", + Darr: "{\\Downarrow}", + dashint: "{\\unicodeInt{x2A0D}}", + ddashint: "{\\unicodeInt{x2A0E}}", + diamonds: "{\\diamondsuit}", + empty: "{\\emptyset}", + Epsilon: "{\\mathrm{E}}", + Eta: "{\\mathrm{H}}", + euro: "{\u20AC}", + exist: "{\\exists}", + geneuro: "{\u20AC}", + geneuronarrow: "{\u20AC}", + geneurowide: "{\u20AC}", + H: "{\\mathbb{H}}", + hAar: "{\\Leftrightarrow}", + harr: "{\\leftrightarrow}", + Harr: "{\\Leftrightarrow}", + hearts: "{\\heartsuit}", + image: "{\\Im}", + infin: "{\\infty}", + Iota: "{\\mathrm{I}}", + isin: "{\\in}", + Kappa: "{\\mathrm{K}}", + koppa: "{\u03DF}", + Koppa: "{\u03DE}", + lang: "{\\langle}", + larr: "{\\leftarrow}", + Larr: "{\\Leftarrow}", + lArr: "{\\Leftarrow}", + lrarr: "{\\leftrightarrow}", + Lrarr: "{\\Leftrightarrow}", + lrArr: "{\\Leftrightarrow}", + Mu: "{\\mathrm{M}}", + N: "{\\mathbb{N}}", + natnums: "{\\mathbb{N}}", + Nu: "{\\mathrm{N}}", + O: "{\\emptyset}", + oiint: "{\\unicodeInt{x222F}}", + oiiint: "{\\unicodeInt{x2230}}", + ointctrclockwise: "{\\unicodeInt{x2233}}", + officialeuro: "{\u20AC}", + Omicron: "{\\mathrm{O}}", + or: "{\\lor}", + P: "{\u00B6}", + pagecolor: ["",1], + part: "{\\partial}", + plusmn: "{\\pm}", + Q: "{\\mathbb{Q}}", + R: "{\\mathbb{R}}", + rang: "{\\rangle}", + rarr: "{\\rightarrow}", + Rarr: "{\\Rightarrow}", + rArr: "{\\Rightarrow}", + real: "{\\Re}", + reals: "{\\mathbb{R}}", + Reals: "{\\mathbb{R}}", + Rho: "{\\mathrm{P}}", + sdot: "{\\cdot}", + sampi: "{\u03E1}", + Sampi: "{\u03E0}", + sect: "{\\S}", + spades: "{\\spadesuit}", + stigma: "{\u03DB}", + Stigma: "{\u03DA}", + sub: "{\\subset}", + sube: "{\\subseteq}", + supe: "{\\supseteq}", + Tau: "{\\mathrm{T}}", + textvisiblespace: "{\u2423}", + thetasym: "{\\vartheta}", + uarr: "{\\uparrow}", + uArr: "{\\Uparrow}", + Uarr: "{\\Uparrow}", + unicodeInt: ["{\\mathop{\\vcenter{\\mathchoice{\\huge\\unicode{#1}\\,}{\\unicode{#1}}{\\unicode{#1}}{\\unicode{#1}}}\\,}\\nolimits}", 1], + varcoppa: "{\u03D9}", + varstigma: "{\u03DB}", + varointclockwise: "{\\unicodeInt{x2232}}", + vline: ["{\\smash{\\large\\lvert #1}", 0], + weierp: "{\\wp}", + Z: "{\\mathbb{Z}}", + Zeta: "{\\mathrm{Z}}" + }, + environments: { + displaymjx: ["", ""] + } }, - environments: { - displaymjx: ["", ""] - } - }, - options: { - ignoreHtmlClass: mw.config.get('wgSmjIgnoreHtmlClass'), - processHtmlClass: "mathjax_process|smj-container" - }, - chtml: { - scale: mw.config.get('wgSmjScale'), - }, - loader: { - load: ['ui/safe','[tex]/autoload'].concat(mw.config.exists('smjPreloadChem') ? ['[tex]/mhchem'] : []) - }, - startup: { - elements: mw.config.get('wgSmjDelimitersEnabled') ? null : ["span.smj-container"], - pageReady: () => { - return MathJax.startup.defaultPageReady().then(() => { - document.querySelectorAll("span.smj-container > .MathJax").forEach((mjx) => { - mjx.parentElement.style.opacity = 1; + options: { + ignoreHtmlClass: mw.config.get('wgSmjIgnoreHtmlClass'), + processHtmlClass: "mathjax_process|smj-container" + }, + chtml: { + scale: mw.config.get('wgSmjScale'), + }, + loader: { + load: ['ui/safe','[tex]/autoload'].concat(mw.config.exists('smjPreloadChem') ? ['[tex]/mhchem'] : []) + }, + startup: { + elements: mw.config.get('wgSmjDelimitersEnabled') ? null : ["span.smj-container"], + pageReady: () => { + return MathJax.startup.defaultPageReady().then(() => { + document.querySelectorAll("span.smj-container > .MathJax").forEach((mjx) => { + mjx.parentElement.style.opacity = 1; + }); }); - }); + } } - } -}; -(function () { - const script = document.createElement('script'); + }; + + var script = document.createElement('script'); script.src = mw.config.get('wgSmjCdnEnabled') ? 'https://cdn.jsdelivr.net/npm/mathjax@' + mw.config.get('wgSmjCdnVersion') + '/tex-chtml.js' : mw.config.get('wgExtensionAssetsPath') + '/SimpleMathJax/resources/MathJax/tex-chtml.js'; script.async = true; + scriptLoadPromise = new Promise((resolve, reject) => { + script.onload = resolve; + script.onerror = () => { + loaded = false; + reject(); + }; + }); document.head.appendChild(script); -})(); +} + +mw.libs.smj.ensureLoaded = ensureLoaded; + +mw.libs.smj.whenReady = function () { + ensureLoaded(); + return scriptLoadPromise.then(() => MathJax.startup.promise); +}; + +var typesetQueue = Promise.resolve(); +mw.libs.smj.typeset = function (elements) { + var result = typesetQueue + .then(() => mw.libs.smj.whenReady()) + .then(() => MathJax.typesetPromise(elements)); + typesetQueue = result.catch(() => {}); + return result; +}; + +mw.hook('wikipage.content').add(function ($content) { + var alreadyLoaded = loaded; + ensureLoaded(); + if (!alreadyLoaded) { + return; + } + var $containers = $content.filter('.smj-container').add($content.find('.smj-container')); + var delimitersEnabled = mw.config.get('wgSmjDelimitersEnabled'); + if (!delimitersEnabled && !$containers.length) { + return; + } + var elements = delimitersEnabled ? $content.toArray() : $containers.toArray(); + mw.libs.smj.typeset(elements).then(() => { + $containers.children('.MathJax').parent().css('opacity', 1); + }); }); +})(); diff --git a/resources/ve/ve.ce.MWChemNode.js b/resources/ve/ve.ce.MWChemNode.js new file mode 100644 index 00000000..ca221ddd --- /dev/null +++ b/resources/ve/ve.ce.MWChemNode.js @@ -0,0 +1,10 @@ +ve.ce.MWChemNode = function VeCeMWChemNode() { + ve.ce.MWChemNode.super.apply(this, arguments); +}; + +OO.inheritClass(ve.ce.MWChemNode, ve.ce.MWFormulaNode); + +ve.ce.MWChemNode.static.name = 'smjChem'; +ve.ce.MWChemNode.static.primaryCommandName = 'smjChemDialog'; +ve.ce.MWChemNode.static.iconWhenInvisible = 'labFlask'; +ve.ce.nodeFactory.register(ve.ce.MWChemNode); diff --git a/resources/ve/ve.ce.MWFormulaNode.js b/resources/ve/ve.ce.MWFormulaNode.js new file mode 100644 index 00000000..f0d103d3 --- /dev/null +++ b/resources/ve/ve.ce.MWFormulaNode.js @@ -0,0 +1,32 @@ +ve.ce.MWFormulaNode = function VeCeMWFormulaNode() { + ve.ce.MWFormulaNode.super.apply( this, arguments ); +}; + +OO.inheritClass( ve.ce.MWFormulaNode, ve.ce.MWInlineExtensionNode ); + +ve.ce.MWFormulaNode.prototype.onSetup = function () { + ve.ce.MWFormulaNode.super.prototype.onSetup.call( this ); + + this.$element.addClass( 've-ce-smjFormulaNode' ); +}; + +ve.ce.MWFormulaNode.prototype.validateGeneratedContents = function ( $element ) { + return !( $element.find( '.error' ).addBack( '.error' ).length ); +}; + +ve.ce.MWFormulaNode.prototype.afterRender = function () { + var node = this; + var $elements = this.$element; + + function done() { + ve.ce.MWFormulaNode.super.prototype.afterRender.call( node ); + } + + mw.libs.smj.typeset( $elements.toArray() ).then( () => { + var $containers = $elements.filter( '.smj-container' ).add( $elements.find( '.smj-container' ) ); + $containers.children( '.MathJax' ).parent().css( 'opacity', 1 ); + done(); + }, () => { + done(); + } ); +}; diff --git a/resources/ve/ve.ce.MWMathNode.js b/resources/ve/ve.ce.MWMathNode.js new file mode 100644 index 00000000..88b3cde4 --- /dev/null +++ b/resources/ve/ve.ce.MWMathNode.js @@ -0,0 +1,10 @@ +ve.ce.MWMathNode = function VeCeMWMathNode() { + ve.ce.MWMathNode.super.apply(this, arguments); +}; + +OO.inheritClass(ve.ce.MWMathNode, ve.ce.MWFormulaNode); + +ve.ce.MWMathNode.static.name = 'smjMath'; +ve.ce.MWMathNode.static.primaryCommandName = 'smjMathDialog'; +ve.ce.MWMathNode.static.iconWhenInvisible = 'mathematics'; +ve.ce.nodeFactory.register(ve.ce.MWMathNode); diff --git a/resources/ve/ve.dm.MWChemNode.js b/resources/ve/ve.dm.MWChemNode.js new file mode 100644 index 00000000..408f1dfb --- /dev/null +++ b/resources/ve/ve.dm.MWChemNode.js @@ -0,0 +1,9 @@ +ve.dm.MWChemNode = function VeDmMWChemNode() { + ve.dm.MWChemNode.super.apply(this, arguments); +}; + +OO.inheritClass(ve.dm.MWChemNode, ve.dm.MWInlineExtensionNode); + +ve.dm.MWChemNode.static.name = 'smjChem'; +ve.dm.MWChemNode.static.extensionName = 'chem'; +ve.dm.modelRegistry.register(ve.dm.MWChemNode); diff --git a/resources/ve/ve.dm.MWMathNode.js b/resources/ve/ve.dm.MWMathNode.js new file mode 100644 index 00000000..6a6b114c --- /dev/null +++ b/resources/ve/ve.dm.MWMathNode.js @@ -0,0 +1,9 @@ +ve.dm.MWMathNode = function VeDmMWMathNode() { + ve.dm.MWMathNode.super.apply(this, arguments); +}; + +OO.inheritClass(ve.dm.MWMathNode, ve.dm.MWInlineExtensionNode); + +ve.dm.MWMathNode.static.name = 'smjMath'; +ve.dm.MWMathNode.static.extensionName = 'math'; +ve.dm.modelRegistry.register(ve.dm.MWMathNode); diff --git a/resources/ve/ve.ui.MWChemContextItem.js b/resources/ve/ve.ui.MWChemContextItem.js new file mode 100644 index 00000000..e60fdefa --- /dev/null +++ b/resources/ve/ve.ui.MWChemContextItem.js @@ -0,0 +1,18 @@ +ve.ui.MWChemContextItem = function VeUiMWChemContextItem() { + ve.ui.MWChemContextItem.super.apply( this, arguments ); +}; + +OO.inheritClass( ve.ui.MWChemContextItem, ve.ui.MWFormulaContextItem ); + +ve.ui.MWChemContextItem.static.name = 'smjChem'; + +ve.ui.MWChemContextItem.static.icon = 'labFlask'; + +ve.ui.MWChemContextItem.static.label = + OO.ui.deferMsg( 'simplemathjax-visualeditor-mwchemdialog-title' ); + +ve.ui.MWChemContextItem.static.modelClasses = [ ve.dm.MWChemNode ]; + +ve.ui.MWChemContextItem.static.commandName = 'smjChemDialog'; + +ve.ui.contextItemFactory.register( ve.ui.MWChemContextItem ); diff --git a/resources/ve/ve.ui.MWChemDialog.js b/resources/ve/ve.ui.MWChemDialog.js new file mode 100644 index 00000000..13afdb62 --- /dev/null +++ b/resources/ve/ve.ui.MWChemDialog.js @@ -0,0 +1,10 @@ +ve.ui.MWChemDialog = function VeUiMWChemDialog(config) { + ve.ui.MWChemDialog.super.call(this, config); +}; + +OO.inheritClass(ve.ui.MWChemDialog, ve.ui.MWFormulaDialog); + +ve.ui.MWChemDialog.static.name = 'smjChemDialog'; +ve.ui.MWChemDialog.static.title = OO.ui.deferMsg('simplemathjax-visualeditor-mwchemdialog-title'); +ve.ui.MWChemDialog.static.modelClasses = [ve.dm.MWChemNode]; +ve.ui.windowFactory.register(ve.ui.MWChemDialog); diff --git a/resources/ve/ve.ui.MWChemDialogTool.js b/resources/ve/ve.ui.MWChemDialogTool.js new file mode 100644 index 00000000..d8249ed1 --- /dev/null +++ b/resources/ve/ve.ui.MWChemDialogTool.js @@ -0,0 +1,31 @@ +ve.ui.MWChemDialogTool = function VeUiMWChemDialogTool(toolGroup, config) { + ve.ui.MWChemDialogTool.super.call(this, toolGroup, config); +}; + +OO.inheritClass(ve.ui.MWChemDialogTool, ve.ui.FragmentWindowTool); + +ve.ui.MWChemDialogTool.static.name = 'smjChem'; +ve.ui.MWChemDialogTool.static.group = 'object'; +ve.ui.MWChemDialogTool.static.icon = 'labFlask'; +ve.ui.MWChemDialogTool.static.title = + OO.ui.deferMsg('simplemathjax-visualeditor-mwchemdialog-title'); + +ve.ui.MWChemDialogTool.static.modelClasses = [ve.dm.MWChemNode]; +ve.ui.MWChemDialogTool.static.commandName = 'smjChemDialog'; +ve.ui.toolFactory.register(ve.ui.MWChemDialogTool); + +ve.ui.commandRegistry.register( + new ve.ui.Command( + 'smjChemDialog', 'window', 'open', + { args: ['smjChemDialog'], supportedSelections: ['linear'] } + ) +); + +ve.ui.sequenceRegistry.register( + new ve.ui.Sequence('smjWikitextChem', 'smjChemDialog', ' { + this.previewElement.$element.empty(); + originalUpdatePreview(); + }; + + this.displaySelect = new OO.ui.ButtonSelectWidget({ + items: [ + new OO.ui.ButtonOptionWidget({ + data: 'default', + label: ve.msg('simplemathjax-visualeditor-mwformuladialog-display-default') + }), + new OO.ui.ButtonOptionWidget({ + data: 'inline', + label: ve.msg('simplemathjax-visualeditor-mwformuladialog-display-inline') + }), + new OO.ui.ButtonOptionWidget({ + data: 'block', + label: ve.msg('simplemathjax-visualeditor-mwformuladialog-display-block') + }) + ] + }); + + const inputField = new OO.ui.FieldLayout(this.input, { + align: 'top', + label: ve.msg('simplemathjax-visualeditor-mwformuladialog-formula') + }); + const displayField = new OO.ui.FieldLayout(this.displaySelect, { + align: 'top', + label: ve.msg('simplemathjax-visualeditor-mwformuladialog-display') + }); + + this.$body + .addClass('ve-ui-smjFormulaDialog-content') + .append( + this.previewElement.$element, + inputField.$element, + displayField.$element + ); +}; + +ve.ui.MWFormulaDialog.prototype.getSetupProcess = function (data) { + return ve.ui.MWFormulaDialog.super.prototype.getSetupProcess.call(this, data) + .next(() => { + const attributes = this.selectedNode && this.selectedNode.getAttribute('mw').attrs, + display = attributes && attributes.display || 'default', + isReadOnly = this.isReadOnly(); + + this.displaySelect.selectItemByData(display).setDisabled(isReadOnly); + + this.input.on('change', this.onChangeHandler); + this.displaySelect.on('choose', this.onChangeHandler); + }); +}; + +ve.ui.MWFormulaDialog.prototype.getReadyProcess = function (data) { + return ve.ui.MWFormulaDialog.super.prototype.getReadyProcess.call(this, data) + .next(() => { + this.input.focus().moveCursorToEnd(); + }); +}; + +ve.ui.MWFormulaDialog.prototype.getTeardownProcess = function (data) { + return ve.ui.MWFormulaDialog.super.prototype.getTeardownProcess.call(this, data) + .first(() => { + this.input.off('change', this.onChangeHandler); + this.displaySelect.off('choose', this.onChangeHandler); + }); +}; + +ve.ui.MWFormulaDialog.prototype.updateMwData = function (mwData) { + ve.ui.MWFormulaDialog.super.prototype.updateMwData.call(this, mwData); + + const display = this.displaySelect.findSelectedItem() && + this.displaySelect.findSelectedItem().getData(); + mwData.attrs.display = display && display !== 'default' ? display : undefined; +}; + +ve.ui.MWFormulaDialog.prototype.getBodyHeight = function () { + return 350; +}; diff --git a/resources/ve/ve.ui.MWFormulaDialog.less b/resources/ve/ve.ui.MWFormulaDialog.less new file mode 100644 index 00000000..e7539acd --- /dev/null +++ b/resources/ve/ve.ui.MWFormulaDialog.less @@ -0,0 +1,12 @@ +.ve-ui-smjFormulaDialog-content { + .oo-ui-textInputWidget { + max-width: none; + } +} + +.ve-ui-smjFormulaDialog-preview { + text-align: center; + min-height: 4em; + line-height: 4em; + margin-bottom: 1em; +} \ No newline at end of file diff --git a/resources/ve/ve.ui.MWMathContextItem.js b/resources/ve/ve.ui.MWMathContextItem.js new file mode 100644 index 00000000..88f226a0 --- /dev/null +++ b/resources/ve/ve.ui.MWMathContextItem.js @@ -0,0 +1,14 @@ +ve.ui.MWMathContextItem = function VeUiMWMathContextItem() { + ve.ui.MWMathContextItem.super.apply(this, arguments); +}; + +OO.inheritClass(ve.ui.MWMathContextItem, ve.ui.MWFormulaContextItem); + +ve.ui.MWMathContextItem.static.name = 'smjMath'; +ve.ui.MWMathContextItem.static.icon = 'mathematics'; +ve.ui.MWMathContextItem.static.label = + OO.ui.deferMsg('simplemathjax-visualeditor-mwmathdialog-title'); + +ve.ui.MWMathContextItem.static.modelClasses = [ve.dm.MWMathNode]; +ve.ui.MWMathContextItem.static.commandName = 'smjMathDialog'; +ve.ui.contextItemFactory.register(ve.ui.MWMathContextItem); diff --git a/resources/ve/ve.ui.MWMathDialog.js b/resources/ve/ve.ui.MWMathDialog.js new file mode 100644 index 00000000..9601cf81 --- /dev/null +++ b/resources/ve/ve.ui.MWMathDialog.js @@ -0,0 +1,10 @@ +ve.ui.MWMathDialog = function VeUiMWMathDialog(config) { + ve.ui.MWMathDialog.super.call(this, config); +}; + +OO.inheritClass(ve.ui.MWMathDialog, ve.ui.MWFormulaDialog); + +ve.ui.MWMathDialog.static.name = 'smjMathDialog'; +ve.ui.MWMathDialog.static.title = OO.ui.deferMsg('simplemathjax-visualeditor-mwmathdialog-title'); +ve.ui.MWMathDialog.static.modelClasses = [ve.dm.MWMathNode]; +ve.ui.windowFactory.register(ve.ui.MWMathDialog); diff --git a/resources/ve/ve.ui.MWMathDialogTool.js b/resources/ve/ve.ui.MWMathDialogTool.js new file mode 100644 index 00000000..16f995df --- /dev/null +++ b/resources/ve/ve.ui.MWMathDialogTool.js @@ -0,0 +1,36 @@ +ve.ui.MWMathDialogTool = function VeUiMWMathDialogTool( toolGroup, config ) { + ve.ui.MWMathDialogTool.super.call( this, toolGroup, config ); +}; + +OO.inheritClass( ve.ui.MWMathDialogTool, ve.ui.FragmentWindowTool ); + +ve.ui.MWMathDialogTool.static.name = 'smjMath'; + +ve.ui.MWMathDialogTool.static.group = 'object'; + +ve.ui.MWMathDialogTool.static.icon = 'mathematics'; + +ve.ui.MWMathDialogTool.static.title = + OO.ui.deferMsg( 'simplemathjax-visualeditor-mwmathdialog-title' ); + +ve.ui.MWMathDialogTool.static.modelClasses = [ ve.dm.MWMathNode ]; + +ve.ui.MWMathDialogTool.static.commandName = 'smjMathDialog'; + +ve.ui.toolFactory.register( ve.ui.MWMathDialogTool ); + +ve.ui.commandRegistry.register( + new ve.ui.Command( + 'smjMathDialog', 'window', 'open', + { args: [ 'smjMathDialog' ], supportedSelections: [ 'linear' ] } + ) +); + +ve.ui.sequenceRegistry.register( + new ve.ui.Sequence( 'smjWikitextMath', 'smjMathDialog', '