diff --git a/.gitignore b/.gitignore index 2a70e71..6e429e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ *~ +# Core +core/node_modules + # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] diff --git a/core/.prettierignore b/core/.prettierignore new file mode 100644 index 0000000..1eae0cf --- /dev/null +++ b/core/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/core/.prettierrc b/core/.prettierrc new file mode 100644 index 0000000..b6c1a17 --- /dev/null +++ b/core/.prettierrc @@ -0,0 +1,11 @@ +{ + "arrowParens": "avoid", + "bracketSpacing": true, + "printWidth": 88, + "quoteProps": "as-needed", + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "es5", + "useTabs": false +} diff --git a/core/eslint.config.mjs b/core/eslint.config.mjs new file mode 100644 index 0000000..0fdde16 --- /dev/null +++ b/core/eslint.config.mjs @@ -0,0 +1,20 @@ +// @ts-check + +import eslint from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['dist', 'node_modules'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + { + languageOptions: { + globals: { + ...globals.browser, + }, + }, + } +); diff --git a/core/package-lock.json b/core/package-lock.json new file mode 100644 index 0000000..078599b --- /dev/null +++ b/core/package-lock.json @@ -0,0 +1,2054 @@ +{ + "name": "@doclang/viewer-core", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@doclang/viewer-core", + "version": "0.1.0", + "dependencies": { + "lit": "^3.3.3" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.8.1", + "globals": "^17.11.0", + "prettier": "^3.9.6", + "typescript": "^6.0.3", + "typescript-eslint": "^8.67.0", + "vite": "^8.2.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz", + "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.6.0.tgz", + "integrity": "sha512-VHb0ALPMTlgKjM6yIxxoQNnpKyUKLD04VzeQdsiXkMqkvYlAHxq9glGLmgbb889/1GsohSOAjvQYoiBppXFqrQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.144.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.144.0.tgz", + "integrity": "sha512-nuhZIOLuI6TFQ32I/WnUx+SCPY7SdSKwgnFHydAuoS1+Z4BRcaP+RRJmGzl9lw+0OFF7UmaESf7KQRXaNLHypg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.4.tgz", + "integrity": "sha512-jHC2cnyKz5xU2fhECtFl8OZ83cYNt13GZQD+0uMJ/X3o+ijmd56okHhTUwxVSHPx1IRVIJEZ1/1pPzeLCU6XKA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-Dc5mPD8F5F/FS8i01syd7FTF6yB2fVthH/TRkjwJkzUK6EpoxHtqvZQP5Zwq80/5z19TWYHIg1KOHboCgVx/aQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.4.tgz", + "integrity": "sha512-fpDm4oBo6SqLvWUYCmFhdde3U9KH2fRNNMeAnAPAIwxRL345xutL0EtEUcuoxsoazdJGv/MuDBQHlCDrtbvqOg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.4.tgz", + "integrity": "sha512-rSJoreDE/HoIzoaib6MTp5jQtCTdMHKIvItAKT/ImS6Y6Ww76oUaeMyp4Vc/fAgd/ehji068IxetHXAnqUwN9A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.4.tgz", + "integrity": "sha512-/jm8OGHgn7oGaJu3i/qZI9spUGcJ+y/lk43ttQ/iO1tOd9NissG6o97bighBCiL+BKRngmcDuR6ikfwYdJmVuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.4.tgz", + "integrity": "sha512-tIP06BeD9EqvECBrPZ+sqdPlYrT+aYaAiu1wYziVx5elRK/ftm33JxVDy2bXGbr6J0CrtirCkR87/X5a2euEng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.4.tgz", + "integrity": "sha512-Ql1Q0EQqVThvn9VAVlwNzsUvbSFtCMGjLpRRi4pk5i7NZZ4n5ISiLMjHYtus4VQ2PvkSw24zyaCVsiS+sXPj1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.4.tgz", + "integrity": "sha512-GjbjXD4XXfN19D0LZNbmiCBUoDiRACsYHr0yaIbbn8aFsXjHZifcYqu/W5Er5X2X990WjHXFrxarn5chzItorQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.4.tgz", + "integrity": "sha512-p5WR0NOwaRmJ/B1b6IjEFLLivwEsf3PrdBIhRbhTCQisbo2SvHHpG4ELB/+FgQNnB88LTOF86upmJmbvZdQ2lw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.4.tgz", + "integrity": "sha512-4/GyVjmhR+Tc6HLJvwc1sOhPqAZtySiSMesOZyX6JQ5XBxoTDEMKQzvo07NIK6nTon/SivlZqvhzvuVBNQhObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.4.tgz", + "integrity": "sha512-l9eeLsCNvPpmSXUej0etw/J1eqV0Jj1D5G/xG6YTijmE6dkv6E2QezgWbTfQk63v952DPqrjOCoiqxq7Bw0YUQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.4.tgz", + "integrity": "sha512-e0F355MSTMm3+UOqtV3L24gFUp2N5m1f8L/7d56deik6va+AXdrt9F8LbzGpeWGWRbZEDq4m8NVnJDeBtf9DZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.4.tgz", + "integrity": "sha512-AWLi0uBRYh6QlE7OKhiz+phZC0qwtij2QZmhmOdsLdFn64m7oMpooE9ICE3lhm9xMb4SpDo2WbHcxX1iFLFtqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.4.tgz", + "integrity": "sha512-UwSDJOg3dqCAejWdxclJjCsh3Qq4vLYMDxmyHqo1btz3stK2VqgwNd3mm5tuIwzSlGIQ/1H9Hr+Zn09mrezNqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/type-utils": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.67.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.67.0", + "@typescript-eslint/types": "^8.67.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.67.0", + "@typescript-eslint/tsconfig-utils": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "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/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz", + "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.11.0.tgz", + "integrity": "sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lit": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz", + "integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.3.tgz", + "integrity": "sha512-el8M6jK2o3RXBnrSHX3ZKrsN8zEV63pSExTO1wYJz7QndGYZ8353e2a5PPX+qHe2aGayfnchQmkAojaWAREOIA==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rolldown": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz", + "integrity": "sha512-rSr7irW0K7QRWzjdJXqZowkcRdDtjRduh43rBltnVKd0VFq839l1lJoDvGJb6gl7+4rTTCrPWu+YfujUL8Ug7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.144.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.2.4", + "@rolldown/binding-darwin-arm64": "1.2.4", + "@rolldown/binding-darwin-x64": "1.2.4", + "@rolldown/binding-freebsd-x64": "1.2.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.4", + "@rolldown/binding-linux-arm64-gnu": "1.2.4", + "@rolldown/binding-linux-arm64-musl": "1.2.4", + "@rolldown/binding-linux-ppc64-gnu": "1.2.4", + "@rolldown/binding-linux-s390x-gnu": "1.2.4", + "@rolldown/binding-linux-x64-gnu": "1.2.4", + "@rolldown/binding-linux-x64-musl": "1.2.4", + "@rolldown/binding-openharmony-arm64": "1.2.4", + "@rolldown/binding-win32-arm64-msvc": "1.2.4", + "@rolldown/binding-win32-x64-msvc": "1.2.4" + } + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz", + "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.67.0", + "@typescript-eslint/parser": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz", + "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.25", + "rolldown": "~1.2.1", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/core/package.json b/core/package.json new file mode 100644 index 0000000..c6f0cc1 --- /dev/null +++ b/core/package.json @@ -0,0 +1,31 @@ +{ + "name": "@doclang/viewer-core", + "version": "0.1.0", + "description": "TypeScript source for the DocLang viewer", + "type": "module", + "private": true, + "scripts": { + "build": "vite build", + "dev": "vite build --watch", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "check": "npm run format:check && npm run lint && npm run typecheck && npm run build" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.8.1", + "globals": "^17.11.0", + "prettier": "^3.9.6", + "typescript": "^6.0.3", + "typescript-eslint": "^8.67.0", + "vite": "^8.2.1" + }, + "engines": { + "node": ">=18" + }, + "dependencies": { + "lit": "^3.3.3" + } +} diff --git a/core/src/components/base/page-element.ts b/core/src/components/base/page-element.ts new file mode 100644 index 0000000..e86e222 --- /dev/null +++ b/core/src/components/base/page-element.ts @@ -0,0 +1,208 @@ +/** + * Base class for doclang web components that render document content. + * + * Supports three ways to provide the DocLang document: + * + * 1. `src` attribute — a URL (http:, data:, blob:) pointing to either a + * raw XML markup string or a .dclx/.zip archive. The component fetches + * and parses it automatically. + * + * 2. Inline child ` diff --git a/viewer.css b/viewer.css index ba12ead..69ad974 100644 --- a/viewer.css +++ b/viewer.css @@ -173,196 +173,6 @@ header h1 { white-space: nowrap; } -.pane-file { - min-width: 0; - overflow: hidden; -} - -.pane-file .pane-header, -.pane-file .pane-body { - min-width: 0; -} - -.pane-file-header { - justify-content: space-between; - padding-inline: 0.35rem; - gap: 0.35rem; -} - -.pane-file-header .pane-header-title { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.file-pane-close-all { - appearance: none; - border: 1px solid transparent; - border-radius: 0.25rem; - background: transparent; - color: var(--muted); - font: inherit; - font-size: 0.625rem; - font-weight: 600; - line-height: 1; - letter-spacing: 0.04em; - text-transform: uppercase; - padding: 0 0.15rem; - height: 1.25rem; - cursor: pointer; - white-space: nowrap; - flex-shrink: 0; - transition: color 0.12s ease, background 0.12s ease, border-color 0.12s ease; -} - -.file-pane-close-all:hover { - color: var(--text); - background: color-mix(in srgb, var(--text) 5%, transparent); -} - -.file-pane-close-all:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; -} - -#file-pane { - padding: 0.375rem var(--file-pane-pad-x); - overflow: auto; - min-width: 0; -} - -.file-view-list { - list-style: none; - margin: 0; - padding: 0; - min-width: 0; - display: flex; - flex-direction: column; - gap: 0.25rem; - width: 100%; -} - -.file-view-list > li { - width: 100%; - max-width: 100%; -} - -.file-view-item { - display: flex; - flex-direction: column; - align-items: center; - gap: 0.35rem; - width: 100%; - max-width: 100%; - box-sizing: border-box; - border: none; - border-radius: 0; - background: transparent; - color: var(--text); - font: inherit; - font-size: 0.8125rem; - line-height: 1.35; - text-align: center; - padding: 0.5rem var(--file-item-pad-x); - cursor: pointer; -} - -.file-view-thumb-wrap { - position: relative; - flex-shrink: 0; -} - -.file-view-close { - position: absolute; - top: -0.25rem; - right: -0.25rem; - appearance: none; - display: flex; - align-items: center; - justify-content: center; - width: 1rem; - height: 1rem; - padding: 0; - border: 1px solid var(--border); - border-radius: 50%; - background: var(--bg); - color: var(--muted); - font: inherit; - font-size: 0.75rem; - line-height: 1; - cursor: pointer; - box-shadow: 0 1px 2px rgb(0 0 0 / 8%); - z-index: 1; -} - -.file-view-close:hover { - color: var(--text); - background: var(--panel); -} - -.file-view-close:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 1px; -} - -.file-view-thumb { - flex-shrink: 0; - width: var(--file-thumb-size); - height: var(--file-thumb-size); - display: flex; - align-items: center; - justify-content: center; - background: var(--placeholder-bg); - border: 1px solid var(--border); - border-radius: 0.25rem; - overflow: hidden; -} - -.file-view-thumb img { - display: block; - width: 100%; - height: 100%; - object-fit: contain; -} - -.file-view-thumb-placeholder { - display: flex; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; - color: var(--muted); -} - -.file-view-thumb-placeholder svg { - display: block; - width: 1.875rem; - height: 1.875rem; -} - -.file-view-label { - width: 100%; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.file-view-item:hover { - background: var(--markup-hover); -} - -.file-view-item.is-active { - background: var(--markup-selected); - color: var(--accent); - font-weight: 500; -} - -.file-view-item:focus-visible { - outline: 2px solid var(--accent); - outline-offset: -2px; -} - .header-logo-link { display: block; flex-shrink: 0; @@ -708,1448 +518,110 @@ label.file-btn input { cursor: not-allowed; } -.pane-page-header { - justify-content: space-between; -} - -.pane-reading-header { - justify-content: space-between; +main.split { + display: none; + flex: 1 1 0; + min-height: 0; } -.pane-page-controls { - display: flex; - align-items: center; - gap: 0.5rem; - flex-shrink: 0; +body.viewer-loaded #main { + display: grid; } -.page-zoom-control { - display: flex; - align-items: center; - gap: 0.35rem; - user-select: none; +body.viewer-loaded #main.layout-stacked { + grid-template-columns: 1fr !important; } -.page-zoom-control-label { - font-size: 0.625rem; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; - color: var(--muted); - cursor: pointer; +body.viewer-loaded #main.layout-stacked .pane-splitter { + display: none !important; } -.page-zoom-control input[type="range"] { - width: 5.5rem; - height: 1rem; - margin: 0; - cursor: pointer; +body.viewer-loaded #main.layout-stacked .pane { + border-right: none; + border-bottom: 1px solid var(--border); } -.page-zoom-reset { - appearance: none; - border: 1px solid var(--border); - background: var(--bg); - color: var(--muted); - border-radius: 0.375rem; - padding: 0 0.45rem; - height: 1.25rem; - font: inherit; - font-size: 0.6875rem; - font-weight: 600; - font-variant-numeric: tabular-nums; - line-height: 1; - cursor: pointer; - min-width: 2.75rem; +body.viewer-loaded #main.layout-stacked .pane-layout-last { + border-bottom: none; } -.page-zoom-reset:not(:disabled):hover { - border-color: var(--accent); - color: var(--text); +body:not(.viewer-loaded) #page-nav, +body.markup-only #page-nav { + display: none !important; } -.page-zoom-reset:disabled { - opacity: 0.55; - cursor: default; +body.viewer-loaded #empty-state, +body.viewer-loaded doclang-empty-state { + display: none; } -.pane-header-title { - min-width: 0; +body.viewer-loaded #main:not(.layout-stacked) { + grid-template-rows: minmax(0, 1fr); } -.pane-settings-toggle { - border-radius: 0.25rem; - padding: 0 0.28rem 0 0.4rem; - height: 1.25rem; - font-size: 0.625rem; - font-weight: 600; - line-height: 1; - letter-spacing: 0.04em; - text-transform: uppercase; +body.viewer-loaded #main:not(.layout-stacked) .pane { + border-right: none; } -.pane-page-layout, -.pane-reading-layout { - position: relative; - flex: 1; - min-height: 0; - min-width: 0; +.pane { display: flex; flex-direction: column; + border-right: 1px solid var(--border); + min-height: 0; + min-width: 0; } -.pane-page-layout > .pane-body, -.pane-reading-layout > .pane-body { - flex: 1 1 auto; - min-width: 0; - min-height: 0; +.pane[hidden] { + display: none !important; } -.viewer-settings-layer { - position: absolute; - inset: 0; - z-index: 5; +.pane-layout-last { + border-right: none; } -.viewer-settings-layer[hidden] { - display: none; +.pane-splitter { + position: relative; + width: 1px; + margin: 0 -4px; + padding: 0 4px; + box-sizing: content-box; + cursor: col-resize; + touch-action: none; + z-index: 2; + background: transparent; } -.viewer-settings-scrim { +.pane-splitter::after { + content: ""; position: absolute; - inset: 0; - z-index: 0; - appearance: none; - border: none; - margin: 0; - padding: 0; - background: color-mix(in srgb, var(--bg) 28%, transparent); - cursor: default; + top: 0; + bottom: 0; + left: 4px; + width: 1px; + background: var(--border); + transition: background 0.15s ease, width 0.15s ease; } -.viewer-settings { - position: absolute; - top: 0.5rem; - right: 0.5rem; - z-index: 1; - width: 13rem; - max-height: calc(100% - 1rem); - display: flex; - flex-direction: column; - min-height: 0; - border: 1px solid var(--border); - border-radius: 0.5rem; - background: var(--panel); - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.14); - overflow: hidden; +.pane-splitter:hover::after, +.pane-splitter:focus-visible::after, +.pane-splitter.is-dragging::after { + width: 3px; + background: var(--accent); } -.viewer-settings-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.5rem; - padding: 0.5rem 0.75rem; - border-bottom: 1px solid var(--border); +.pane-splitter[hidden] { + display: none !important; } -.viewer-settings-title { - margin: 0; - font-size: 0.6875rem; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; - color: var(--muted); -} - -.viewer-settings-close { - appearance: none; - border: none; - background: transparent; - color: var(--muted); - width: 1.5rem; - height: 1.5rem; - border-radius: 0.25rem; - font-size: 1.1rem; - line-height: 1; - cursor: pointer; -} - -.viewer-settings-close:hover { - color: var(--text); - background: var(--bg); -} - -.viewer-settings-body { - padding: 0.75rem; - overflow: auto; -} - -.settings-option { - display: flex; - align-items: flex-start; - gap: 0.5rem; - font-size: 0.875rem; - cursor: pointer; - user-select: none; -} - -.settings-option-primary + .settings-subgroup { - margin-top: 0.65rem; -} - -.settings-option-primary { - font-weight: 600; -} - -.settings-option-heading { - cursor: default; -} - -.settings-subgroup { - display: flex; - flex-direction: column; - gap: 0.5rem; - padding-left: 0.85rem; -} - -.settings-reading-order-group { - gap: 0.35rem; - padding-left: 1.6rem; -} - -.settings-option-nested { - padding-left: 0; -} - -.settings-option-sub { - font-size: 0.8125rem; - color: var(--muted); -} - -.settings-option-disabled { - opacity: 0.45; - cursor: not-allowed; -} - -.settings-option-disabled input { - cursor: not-allowed; -} - -.settings-option input { - margin: 0.15rem 0 0; - flex-shrink: 0; - cursor: pointer; -} - -main.split { - display: none; - flex: 1 1 0; - min-height: 0; -} - -body.viewer-loaded #main { - display: grid; -} - -body.viewer-loaded #main.layout-stacked { - grid-template-columns: 1fr !important; -} - -body.viewer-loaded #main.layout-stacked .pane-splitter { - display: none !important; -} - -body.viewer-loaded #main.layout-stacked .pane { - border-right: none; - border-bottom: 1px solid var(--border); -} - -body.viewer-loaded #main.layout-stacked .pane-layout-last { - border-bottom: none; -} - -body:not(.viewer-loaded) #page-nav, -body.markup-only #page-nav { - display: none !important; -} - -body.viewer-loaded #empty-state { - display: none; -} - -body.viewer-loaded #main:not(.layout-stacked) { - grid-template-rows: minmax(0, 1fr); -} - -body.viewer-loaded #main:not(.layout-stacked) .pane { - border-right: none; -} - -.pane { - display: flex; - flex-direction: column; - border-right: 1px solid var(--border); - min-height: 0; - min-width: 0; -} - -.pane[hidden] { - display: none !important; -} - -.pane-layout-last { - border-right: none; -} - -.pane-splitter { - position: relative; - width: 1px; - margin: 0 -4px; - padding: 0 4px; - box-sizing: content-box; - cursor: col-resize; - touch-action: none; - z-index: 2; - background: transparent; -} - -.pane-splitter::after { - content: ""; - position: absolute; - top: 0; - bottom: 0; - left: 4px; - width: 1px; - background: var(--border); - transition: background 0.15s ease, width 0.15s ease; -} - -.pane-splitter:hover::after, -.pane-splitter:focus-visible::after, -.pane-splitter.is-dragging::after { - width: 3px; - background: var(--accent); -} - -.pane-splitter[hidden] { - display: none !important; -} - -body.pane-drag-active { - cursor: col-resize; - user-select: none; +body.pane-drag-active { + cursor: col-resize; + user-select: none; } body.pane-drag-active .pane-splitter { cursor: col-resize; } -.pane-file, -.pane-page-view { - min-width: 0; -} - -.pane-markup, -.pane-reading { - min-width: 0; -} - -.pane-header { - display: flex; - align-items: center; - gap: 0.5rem; - box-sizing: border-box; - height: 2.125rem; - padding: 0 0.75rem; - font-size: 0.75rem; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; - color: var(--muted); - border-bottom: 1px solid var(--border); - background: var(--panel); -} - -.pane-body { - flex: 1; - min-height: 0; - overflow: auto; - padding: 1rem; -} - -#markup-pane { - padding: 0; - background: var(--markup-bg); - color: var(--markup-fg); -} - -#markup-pane .placeholder { - margin: 1rem; -} - -#page-pane { - overflow: hidden; - display: flex; - flex-direction: column; - padding: 0.75rem; - min-height: 0; - min-width: 0; -} - -.page-view-port { - display: flex; - overflow: auto; - flex: 1 1 auto; - width: 100%; - height: 100%; - min-width: 0; - min-height: 0; - scrollbar-gutter: stable; -} - -#page-pane.can-pan { - cursor: grab; -} - -#page-pane.is-panning { - cursor: grabbing; - user-select: none; -} - -#page-pane:focus { - outline: none; -} - -#page-pane:focus-visible { - outline: 2px solid var(--accent); - outline-offset: -2px; -} - -.placeholder { - display: flex; - align-items: center; - justify-content: center; - min-height: 12rem; - color: var(--muted); - font-style: italic; - background: var(--placeholder-bg); - border: 1px dashed var(--border); - border-radius: 0.5rem; - padding: 2rem; - text-align: center; -} - -pre.markup { - margin: 0; - font-family: var(--font-mono); - font-size: 0.8125rem; - line-height: 1.5; - white-space: pre-wrap; - word-break: break-word; -} - -.markup { - margin: 0; - padding: 0.75rem 1rem 1rem; - font-family: var(--font-mono); - font-size: 0.8125rem; - line-height: 1.55; - tab-size: 2; - --markup-indent: 2ch; - --markup-gutter-width: 1rem; -} - -.markup-ghost-tag-part { - opacity: 0.35; - cursor: help; -} - -.cursor-hint { - position: fixed; - z-index: 200; - width: max-content; - max-width: 16rem; - padding: 0.3rem 0.5rem; - border-radius: 0.25rem; - background: var(--panel); - border: 1px solid var(--border); - color: var(--text); - font-family: var(--font-ui); - font-size: 0.75rem; - line-height: 1.35; - text-align: center; - white-space: normal; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); - pointer-events: none; -} - -.cursor-hint.cursor-hint-detail { - max-width: 24rem; - padding: 0.45rem 0.55rem; - text-align: left; -} - -.head-tooltip { - border-collapse: collapse; - width: 100%; - font-size: 0.6875rem; - line-height: 1.4; -} - -.head-tooltip th { - padding: 0.1rem 0.55rem 0.1rem 0; - color: var(--muted); - font-weight: 600; - text-align: left; - vertical-align: top; - white-space: nowrap; -} - -.head-tooltip td { - padding: 0.1rem 0; - vertical-align: top; - word-break: break-word; -} - -.head-tooltip .head-default { - color: var(--muted); - font-style: italic; - white-space: nowrap; -} - -.markup-el { - cursor: pointer; - border-radius: 0.2rem; -} - -.markup-el:hover { - background: var(--markup-hover); -} - -.markup-el.selected { - background: var(--markup-selected); - box-shadow: inset 0 0 0 1px var(--accent); -} - -.markup-line { - white-space: pre-wrap; - word-break: break-word; - cursor: pointer; - padding: 0 0.25rem; - border-radius: 0.15rem; -} - -.markup-line-content { - display: flex; - align-items: baseline; - min-width: 0; - padding-left: calc(var(--markup-depth, 0) * var(--markup-indent)); -} - -.markup-gutter { - flex: 0 0 var(--markup-gutter-width); - width: var(--markup-gutter-width); - display: flex; - align-items: center; - justify-content: center; -} - -.markup-line-body { - flex: 1 1 auto; - min-width: 0; -} - -.markup-fold-toggle { - appearance: none; - display: inline-flex; - align-items: center; - justify-content: center; - width: var(--markup-gutter-width); - height: 1rem; - margin: 0; - padding: 0; - border: none; - border-radius: 0.15rem; - background: transparent; - color: var(--muted); - cursor: pointer; - flex-shrink: 0; -} - -.markup-fold-toggle::before { - content: ""; - display: block; - width: 0; - height: 0; - border-top: 4px solid transparent; - border-bottom: 4px solid transparent; - border-left: 5px solid currentColor; - transform: rotate(90deg); - transition: transform 0.12s ease; -} - -.markup-el-foldable.markup-collapsed > .markup-line-open .markup-fold-toggle::before { - transform: rotate(0deg); -} - -.markup-fold-toggle:hover { - background: var(--markup-hover); - color: var(--markup-fg); -} - -.markup-fold-suffix { - display: none; -} - -.markup-el-foldable.markup-collapsed > .markup-line-open .markup-fold-suffix { - display: inline; -} - -.markup-el-foldable.markup-collapsed > .markup-fold-body { - display: none; -} - -.xml-tag { - color: var(--xml-tag); -} - -.xml-bracket { - color: var(--xml-bracket); -} - -.xml-attr-name { - color: var(--xml-attr-name); -} - -.xml-attr-value { - color: var(--xml-attr-value); -} - -.xml-attr-value-truncatable { - display: inline; -} - -.xml-attr-value-chip { - appearance: none; - display: inline-flex; - align-items: center; - gap: 0.3em; - margin: 0 0 0 0.35em; - padding: 0.08em 0.45em 0.08em 0.5em; - border: 1px solid var(--border); - border-radius: 0.25rem; - background: var(--placeholder-bg); - color: var(--muted); - font-family: var(--font-ui, system-ui, sans-serif); - font-size: 0.72em; - font-weight: 600; - line-height: 1.35; - cursor: pointer; - vertical-align: baseline; - white-space: nowrap; -} - -.xml-attr-value-chip::after { - content: ""; - width: 0; - height: 0; - border-top: 0.28em solid currentColor; - border-left: 0.22em solid transparent; - border-right: 0.22em solid transparent; - opacity: 0.75; - transition: transform 0.12s ease; -} - -.xml-attr-value-chip[aria-expanded="true"]::after { - transform: rotate(180deg); -} - -.xml-attr-value-chip:hover, -.xml-attr-value-chip[aria-expanded="true"] { - border-color: var(--accent); - color: var(--accent); - background: color-mix(in srgb, var(--accent) 8%, var(--placeholder-bg)); -} - -.markup-embedded-uri-panel { - cursor: text; -} - -.markup-embedded-uri-panel:hover { - background: transparent; -} - -.markup-embedded-uri-panel-body { - margin: 0.1rem 0 0.25rem; - padding: 0.45rem 0.6rem; - max-height: 8rem; - overflow: auto; - color: var(--xml-attr-value); - word-break: break-all; - background: var(--placeholder-bg); - border: 1px solid var(--border); - border-radius: 0.25rem; - user-select: text; -} - -.xml-text { - color: var(--xml-text); -} - -.xml-cdata { - color: var(--xml-cdata); -} - -.xml-cdata-delimiter { - color: var(--xml-cdata-delimiter); -} - -.page-view { - position: relative; - display: block; - width: fit-content; - flex-shrink: 0; - margin: auto; - line-height: 0; - border: 1px solid var(--border); - border-radius: 0.25rem; - overflow: hidden; -} - -.page-view img { - display: block; - width: auto; - height: auto; - max-width: none; - max-height: none; - border: none; - border-radius: 0; -} - -.page-view img:not([data-layout-ready]) { - opacity: 0; -} - -.page-view svg.overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - pointer-events: auto; - overflow: hidden; -} - -.overlay rect.bbox.bbox-hidden, -.overlay .overlay-badge.bbox-hidden { - display: none; -} - -.overlay rect.bbox { - stroke-width: 2; - vector-effect: non-scaling-stroke; - pointer-events: none; -} - -.overlay rect.bbox.selected { - fill: color-mix(in srgb, var(--kind-stroke, var(--accent)) 30%, transparent) !important; - stroke-width: 3.5; - filter: drop-shadow(0 0 3px color-mix(in srgb, var(--kind-stroke, var(--accent)) 60%, transparent)); -} - -.overlay rect.bbox.related:not(.selected) { - fill: color-mix(in srgb, var(--kind-stroke, var(--accent)) 30%, transparent) !important; -} - -.overlay rect.bbox.layer-furniture, -.overlay rect.bbox.layer-background { - fill: url(#layer-hatch) !important; - stroke: color-mix(in srgb, var(--kind-stroke, var(--muted)) 50%, transparent); -} - -.overlay .layer-hatch-stripe { - fill: var(--muted); - fill-opacity: 0.06; -} - -.overlay rect.bbox.layer-furniture.selected, -.overlay rect.bbox.layer-background.selected { - fill: color-mix(in srgb, var(--kind-stroke, var(--accent)) 22%, transparent) !important; -} - -.overlay rect.bbox.layer-furniture.related:not(.selected), -.overlay rect.bbox.layer-background.related:not(.selected) { - fill: color-mix(in srgb, var(--kind-stroke, var(--accent)) 18%, transparent) !important; -} - -/* text */ -.overlay .bbox-text { fill: color-mix(in srgb, var(--kind-text) 14%, transparent); stroke: var(--kind-text); } - -/* heading */ -.overlay .bbox-heading { fill: color-mix(in srgb, var(--kind-heading) 14%, transparent); stroke: var(--kind-heading); } - -/* list / ldiv */ -.overlay .bbox-list, -.overlay .bbox-ldiv { fill: color-mix(in srgb, var(--kind-list) 14%, transparent); stroke: var(--kind-list); } - -/* table */ -.overlay .bbox-table { fill: color-mix(in srgb, var(--kind-table) 14%, transparent); stroke: var(--kind-table); } - -/* index */ -.overlay .bbox-index { fill: color-mix(in srgb, var(--kind-index) 14%, transparent); stroke: var(--kind-index); } - -/* formula */ -.overlay .bbox-formula { fill: color-mix(in srgb, var(--kind-formula) 14%, transparent); stroke: var(--kind-formula); } - -/* code */ -.overlay .bbox-code { fill: color-mix(in srgb, var(--kind-code) 16%, transparent); stroke: var(--kind-code); } - -/* picture */ -.overlay .bbox-picture { fill: color-mix(in srgb, var(--kind-picture) 14%, transparent); stroke: var(--kind-picture); } - -/* group */ -.overlay .bbox-group { fill: color-mix(in srgb, var(--kind-group) 14%, transparent); stroke: var(--kind-group); } - -/* footnote */ -.overlay .bbox-footnote { fill: color-mix(in srgb, var(--kind-footnote) 14%, transparent); stroke: var(--kind-footnote); } - -/* page chrome */ -.overlay .bbox-page_header, -.overlay .bbox-page_footer { fill: color-mix(in srgb, var(--kind-page_header) 14%, transparent); stroke: var(--kind-page_header); } - -/* caption */ -.overlay .bbox-caption { fill: color-mix(in srgb, var(--kind-caption) 14%, transparent); stroke: var(--kind-caption); } - -.overlay line.caption-link { - color: var(--kind-caption); - stroke: currentColor; - stroke-width: 1.5; - vector-effect: non-scaling-stroke; - pointer-events: none; - fill: none; -} - -.overlay line.caption-link.bbox-hidden { - display: none; -} - -.overlay line.xref-link { - color: var(--kind-footnote); - stroke: currentColor; - stroke-width: 1.5; - vector-effect: non-scaling-stroke; - pointer-events: none; - fill: none; -} - -.overlay line.xref-link.bbox-hidden { - display: none; -} - -.overlay .fragment-link { - pointer-events: none; -} - -.overlay .fragment-link.bbox-hidden { - display: none; -} - -.overlay .fragment-link-path { - color: var(--overlay-fragment); - fill: none; - stroke: currentColor; - stroke-width: 1.5; - vector-effect: non-scaling-stroke; -} - -.overlay .fragment-link-path-dashed { - stroke-dasharray: 6 4; -} - -.overlay .fragment-nav { - pointer-events: none; -} - -.overlay .fragment-nav.bbox-hidden { - display: none; -} - -.overlay .fragment-nav-btn { - pointer-events: auto; - cursor: pointer; -} - -.overlay .fragment-nav-btn-disabled { - pointer-events: none; - cursor: default; - opacity: 0.35; -} - -.overlay .fragment-nav-btn-bg { - fill: var(--panel); - stroke: var(--overlay-fragment); - stroke-width: 1; - vector-effect: non-scaling-stroke; -} - -.overlay .fragment-nav-btn-label { - fill: var(--overlay-fragment); - font-family: var(--font-ui); - font-weight: 700; - text-anchor: middle; - dominant-baseline: central; - pointer-events: none; -} - -.overlay .fragment-nav-btn:not(.fragment-nav-btn-disabled):hover .fragment-nav-btn-bg { - fill: color-mix(in srgb, var(--overlay-fragment) 12%, var(--panel)); -} - -.overlay .fragment-link-label { - fill: var(--overlay-fragment); - font-family: var(--font-ui); - font-weight: 600; - font-style: italic; - text-anchor: middle; - dominant-baseline: middle; - paint-order: stroke fill; - stroke: var(--panel); - stroke-width: 3px; -} - -.overlay .reading-order-step { - color: var(--overlay-reading-order); - stroke: currentColor; - stroke-width: 1.5; - stroke-dasharray: 5 4; - vector-effect: non-scaling-stroke; - pointer-events: none; - fill: none; -} - -.overlay .reading-order-step.bbox-hidden, -.overlay .reading-order-badge.bbox-hidden { - display: none; -} - -.overlay .overlay-badge { - pointer-events: none; -} - -.overlay .overlay-badge-bg { - fill-opacity: 1; - stroke: none; -} - -.reading-order-badge .overlay-badge-bg { - fill: var(--overlay-reading-order); -} - -.overlay .element-badge { - opacity: 0.8; - pointer-events: auto; - cursor: help; -} - -.overlay .element-badge .overlay-badge-bg { - fill: var(--kind-stroke, var(--accent)); -} - -.overlay .overlay-badge-label { - fill: #ffffff; - fill-opacity: 1; - font-family: var(--font-ui); - font-weight: 700; - text-anchor: middle; - dominant-baseline: central; - pointer-events: none; -} - -/* form fields */ -.overlay .bbox-field { fill: color-mix(in srgb, var(--kind-field) 14%, transparent); stroke: var(--kind-field); } - -/* fallback */ -.overlay .bbox-default { fill: color-mix(in srgb, var(--kind-default) 12%, transparent); stroke: var(--kind-default); } - -@media (prefers-color-scheme: dark) { - .overlay .bbox-text { fill: color-mix(in srgb, var(--kind-text) 20%, transparent); } - - .overlay .bbox-heading { fill: color-mix(in srgb, var(--kind-heading) 20%, transparent); } - - .overlay .bbox-list, - .overlay .bbox-ldiv { fill: color-mix(in srgb, var(--kind-list) 20%, transparent); } - - .overlay .bbox-table { fill: color-mix(in srgb, var(--kind-table) 18%, transparent); } - - .overlay .bbox-index { fill: color-mix(in srgb, var(--kind-index) 18%, transparent); } - - .overlay .bbox-formula { fill: color-mix(in srgb, var(--kind-formula) 18%, transparent); } - - .overlay .bbox-code { fill: color-mix(in srgb, var(--kind-code) 20%, transparent); } - - .overlay .bbox-picture { fill: color-mix(in srgb, var(--kind-picture) 20%, transparent); } - - .overlay .bbox-group { fill: color-mix(in srgb, var(--kind-group) 20%, transparent); } - - .overlay .bbox-footnote { fill: color-mix(in srgb, var(--kind-footnote) 18%, transparent); } - - .overlay .bbox-page_header, - .overlay .bbox-page_footer { fill: color-mix(in srgb, var(--kind-page_header) 18%, transparent); } - - .overlay .bbox-caption { fill: color-mix(in srgb, var(--kind-caption) 18%, transparent); } - - .overlay .bbox-field { fill: color-mix(in srgb, var(--kind-field) 18%, transparent); } - - .overlay .bbox-default { fill: color-mix(in srgb, var(--kind-default) 18%, transparent); } -} - -.empty-state { - flex: 1 1 0; - display: flex; - align-items: center; - justify-content: center; - min-height: 0; - margin: 1rem; - padding: 2rem 1.5rem; - border: 2px dashed var(--border); - border-radius: 0.75rem; - background: var(--placeholder-bg); - text-align: center; - color: var(--muted); -} - -.empty-state-inner { - max-width: 28rem; -} - -.empty-state-loading { - display: none; - flex-direction: column; - align-items: center; -} - -body.demo-loading .empty-state-loading { - display: flex; -} - -body.demo-loading .empty-state-prompt { - display: none; -} - -.loading-spinner { - width: 2rem; - height: 2rem; - margin: 0 auto 1rem; - border: 2px solid var(--border); - border-top-color: var(--accent); - border-radius: 50%; - animation: loading-spin 0.7s linear infinite; -} - -@keyframes loading-spin { - to { - transform: rotate(360deg); - } -} - -.empty-state-title { - margin: 0 0 0.75rem; - font-size: 1.05rem; - color: var(--text); -} - -.empty-state p { - margin: 0.5rem 0; -} - -.empty-state-action { - margin: 0 0 0.75rem; -} - -.text-link { - color: var(--accent); - text-decoration: underline; -} - -.text-link:hover { - opacity: 0.85; -} - -.empty-state-meta { - font-size: 0.875rem; -} - -.rendered-doc { - max-width: 42rem; - margin: 0 auto; - line-height: 1.6; -} - -.rendered-el-virtual-text > p, -.rendered-el-virtual-text > div { - margin: 0; - display: inline; -} - -.rendered-doc li > .rendered-el-virtual-text { - display: inline; -} - -.rendered-doc li > .rendered-el-virtual-text > p, -.rendered-doc li > .rendered-el-virtual-text > div { - display: inline; - margin: 0; -} - -.rendered-table .rendered-el-virtual-text { - display: inline; - margin: 0; -} - -.rendered-table .rendered-el-virtual-text > p, -.rendered-table .rendered-el-virtual-text > div { - display: inline; - margin: 0; -} - -.rendered-el-virtual-text > div > .rendered-el { - display: block; - margin: 0.25rem 0; -} - -.rendered-el { - cursor: pointer; - border-radius: 0.25rem; -} - -.rendered-el:hover { - outline: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); -} - -.rendered-el.selected { - outline: 2px solid var(--accent); - outline-offset: 2px; - background: color-mix(in srgb, var(--accent) 8%, transparent); -} - -.rendered-doc p { - margin: 0 0 0.75rem; -} - -.rendered-doc h1, -.rendered-doc h2, -.rendered-doc h3, -.rendered-doc h4, -.rendered-doc h5, -.rendered-doc h6 { - margin: 1.25rem 0 0.5rem; - line-height: 1.25; -} - -.rendered-doc h1:first-child, -.rendered-doc h2:first-child, -.rendered-doc h3:first-child { - margin-top: 0; -} - -.rendered-doc ul, -.rendered-doc ol { - margin: 0 0 0.75rem; - padding-left: 1.5rem; -} - -.rendered-doc li { - margin: 0.25rem 0; -} - -.rendered-doc li > .rendered-el > p:first-child, -.rendered-doc li > .rendered-el > h1:first-child, -.rendered-doc li > .rendered-el > h2:first-child, -.rendered-doc li > .rendered-el > h3:first-child, -.rendered-doc li > .rendered-el > h4:first-child, -.rendered-doc li > .rendered-el > h5:first-child, -.rendered-doc li > .rendered-el > h6:first-child { - margin-top: 0; -} - -.rendered-marker { - margin-right: 0.35rem; -} - -.rendered-handwriting { - font-family: "Segoe Print", "Bradley Hand", cursive; -} - -.rendered-doc pre { - margin: 0 0 0.75rem; - padding: 0.75rem 1rem; - overflow-x: auto; - font-family: var(--font-mono); - font-size: 0.8125rem; - line-height: 1.5; - background: var(--placeholder-bg); - border: 1px solid var(--border); - border-radius: 0.375rem; -} - -.rendered-doc code { - font-family: var(--font-mono); - font-size: 0.875em; -} - -.rendered-doc pre code { - font-size: inherit; - background: none; - border: none; - padding: 0; -} - -.rendered-code-label { - display: block; - margin-bottom: 0.25rem; - font-size: 0.75rem; - font-weight: 600; - color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.04em; -} - -.rendered-formula { - display: block; - margin: 0 0 0.75rem; - padding: 0.5rem 0.75rem; - font-family: var(--font-mono); - font-size: 0.875rem; - background: var(--placeholder-bg); - border-left: 3px solid var(--border); - overflow-x: auto; -} - -.rendered-formula-inline { - display: inline; - font-family: var(--font-mono); - font-size: 0.875em; -} - -.rendered-doc figure { - margin: 0 0 0.75rem; -} - -.rendered-doc figure img { - max-width: 100%; - height: auto; - border: 1px solid var(--border); - border-radius: 0.25rem; -} - -.rendered-doc figure img.rendered-picture-unavailable { - display: inline-block; - min-width: 3rem; - min-height: 2.5rem; - object-fit: contain; - vertical-align: middle; -} - -.rendered-doc figcaption { - margin-top: 0.35rem; - font-size: 0.875rem; - color: var(--muted); -} - -.rendered-picture-contents { - margin-top: 0.5rem; - font-size: 0.875rem; - border: 1px solid var(--border); - border-radius: 0.375rem; - background: var(--placeholder-bg); -} - -.rendered-picture-contents summary { - padding: 0.4rem 0.65rem; - cursor: pointer; - font-weight: 600; - color: var(--muted); - user-select: none; -} - -.rendered-picture-contents summary:hover { - color: var(--text); -} - -.rendered-picture-contents-body { - padding: 0.5rem 0.75rem 0.75rem; - border-top: 1px solid var(--border); -} - -.rendered-picture-contents-body .rendered-el > p { - margin-bottom: 0.35rem; - font-size: 0.8125rem; -} - -.rendered-table { - width: 100%; - margin: 0 0 0.75rem; - border-collapse: collapse; - font-size: 0.875rem; -} - -.rendered-table caption { - caption-side: top; - margin-bottom: 0.35rem; - font-size: 0.875rem; - color: var(--muted); - text-align: left; -} - -.rendered-table th, -.rendered-table td { - border: 1px solid var(--border); - padding: 0.35rem 0.5rem; - vertical-align: top; - text-align: left; -} - -.rendered-table th { - background: color-mix(in srgb, var(--border) 35%, var(--panel)); - font-weight: 600; -} - -.rendered-table .rendered-table-cell-text { - display: inline; -} - -.rendered-table .rendered-el { - margin: 0; -} - -.rendered-table .rendered-el-virtual-text > p, -.rendered-table .rendered-el-virtual-text > div { - margin: 0; -} - -.rendered-table .rendered-table { - margin: 0.25rem 0 0; -} - -.rendered-page-header, -.rendered-page-footer { - font-size: 0.875rem; - color: var(--muted); -} - -.rendered-page-header { - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid var(--border); -} - -.rendered-page-footer { - margin-top: 1rem; - padding-top: 0.5rem; - border-top: 1px solid var(--border); -} - -.rendered-doc:not(.show-reading-furniture) .rendered-el[data-doclang-layer="furniture"] { - display: none; -} - -.rendered-doc:not(.show-reading-background) .rendered-el[data-doclang-layer="background"] { - display: none; -} - -.rendered-el[data-doclang-layer="furniture"], -.rendered-el[data-doclang-layer="background"] { - position: relative; -} - -.rendered-el[data-doclang-layer="furniture"]::before, -.rendered-el[data-doclang-layer="background"]::before { - content: ""; - position: absolute; - inset: 0; - border-radius: inherit; - pointer-events: none; - background: repeating-linear-gradient( - -45deg, - transparent 0 10px, - color-mix(in srgb, var(--muted) 6%, transparent) 10px 16px - ); -} - -.rendered-el.rendered-footnote > aside { - font-size: 0.875rem; - color: var(--muted); - border-left: 2px solid var(--border); - padding-left: 0.75rem; - margin: 0 0 0.75rem; -} - -.rendered-unsupported { - margin: 0 0 0.75rem; - padding: 0.5rem 0.75rem; - font-size: 0.8125rem; - font-style: italic; - color: var(--muted); - background: var(--placeholder-bg); - border: 1px dashed var(--border); - border-radius: 0.375rem; -} - -.rendered-field-region { - margin: 0 0 1rem; - padding: 0.75rem 1rem; - border: 1px solid var(--border); - border-radius: 0.5rem; - background: color-mix(in srgb, var(--kind-field) 6%, transparent); -} - -.rendered-field-region > .rendered-field-heading:first-child, -.rendered-field-region > .rendered-el.rendered-field_heading > .rendered-field-heading:first-child { - margin-top: 0; -} - -.rendered-field-item { - margin: 0.5rem 0; -} - -.rendered-field-item > .rendered-el.rendered-field-key, -.rendered-field-key { - font-weight: 600; -} - -.rendered-field-item > .rendered-el.rendered-field-key + .rendered-el, -.rendered-field-item > .rendered-el.rendered-field-key + .rendered-el-virtual-text { - margin-top: 0.15rem; -} - -.rendered-field-value-fillable .rendered-field-fillable-slot { - display: inline-block; - min-width: 6rem; - min-height: 1.25em; - border-bottom: 1px solid color-mix(in srgb, var(--text) 55%, transparent); - vertical-align: bottom; -} - -.rendered-field-hint { - font-size: 0.85em; - color: var(--muted); - font-style: italic; -} - -.rendered-field-heading { - color: var(--text); -} - -.rendered-checkbox { - margin: 0 0.25rem 0 0; - vertical-align: middle; - accent-color: var(--kind-field); -} - -.rendered-checkbox-wrap { - display: inline; -} @media (max-width: 1200px) { body.viewer-loaded #main.layout-stacked .pane { diff --git a/viewer.js b/viewer.js index 872111b..4b00ba3 100644 --- a/viewer.js +++ b/viewer.js @@ -1,5446 +1,6991 @@ -/* DocLang Archive Viewer — archive format: github.com/doclang-project/doclang spec.md#doclang-archive-format */ - -const SUPPORTED_FILE_EXTENSIONS = [".dclx", ".dclg"]; -const OPEN_FILE_HINT = `Open a DocLang file (${SUPPORTED_FILE_EXTENSIONS.join(", ")})`; -const VIRTUAL_TEXT_TAG_HINT = "DocLang virtual ; wrapping tags not included in source"; -const FRAGMENT_LINK_LABEL_CROSS_PAGE = "cross-page content"; -const FRAGMENT_LINK_LABEL_SAME_PAGE = "fragmented content"; -const FRAGMENT_NAV_HINT_PREV = "Previous fragment"; -const FRAGMENT_NAV_HINT_NEXT = "Next fragment"; -const DOCLANG_NS = "https://www.doclang.ai/ns/v0"; -const PAGE_IMAGE_RE = /^(\d+)\.(png|jpe?g|webp)$/i; -const NO_MARKUP = "(No markup to be shown.)"; -const NO_IMAGE = "(No page image available.)"; -const FILE_THUMB_PLACEHOLDER_SVG = ``; -const PICTURE_UNAVAILABLE_ALT = "Picture asset not available"; -const INVALID_PICTURE_SRC = "data:image/png;base64,NOT_A_VALID_IMAGE"; -/** Allow small embedded raster data URIs only; remote/blob/other schemes are rejected. */ -const SAFE_DATA_IMAGE_RE = /^data:image\/(png|jpe?g|webp|gif);base64,/i; -const MAX_DATA_IMAGE_URI_LENGTH = 2 * 1024 * 1024; // 2 MiB -const ZIP_MAX_ENTRIES = 5000; -const ZIP_MAX_ENTRY_COMPRESSED_BYTES = 128 * 1024 * 1024; // 128 MiB -const ZIP_MAX_ENTRY_UNCOMPRESSED_BYTES = 128 * 1024 * 1024; // 128 MiB -const ZIP_MAX_TOTAL_UNCOMPRESSED_BYTES = 512 * 1024 * 1024; // 512 MiB -const ZIP_MAX_COMPRESSION_RATIO = 100; // uncompressed / compressed -const LONG_EMBEDDED_URI_PREVIEW_LENGTH = 30; -const HEAD_TAGS = new Set([ - "label", "thread", "xref", "href", "layer", "location", "caption", "description", "summary", "custom", -]); -const SEMANTIC_TAGS = new Set([ - "text", "heading", "footnote", "page_header", "page_footer", "field_region", "list", "table", "index", - "formula", "code", "picture", "marker", "group", "field_heading", "field_item", "key", "value", "hint", - "caption", "page_break", -]); -const CELL_TOKENS = new Set(["fcel", "ecel", "ched", "rhed", "corn", "srow", "lcel", "ucel", "xcel", "nl"]); -const CELL_CONTENT_TAGS = new Set(["fcel", "ecel", "ched", "rhed", "corn", "srow"]); -const CELL_SPAN_TAGS = new Set(["lcel", "ucel", "xcel"]); -const OTSL_CONTAINER_TAGS = new Set(["table", "index", "tabular"]); -const RENDER_BLOCK_TAGS = new Set([ - "text", "heading", "field_heading", "footnote", "page_header", "page_footer", "list", "code", "formula", "picture", "group", - "field_region", "field_item", - "table", "index", "tabular", -]); -const RENDER_FORMAT_TAGS = new Set([ - "bold", "italic", "underline", "strikethrough", "superscript", "subscript", "handwriting", "rtl", "content", -]); -const FORMAT_HTML_TAG = { - bold: "strong", - italic: "em", - underline: "u", - strikethrough: "s", - superscript: "sup", - subscript: "sub", -}; - -function isTextLikeNode(node) { - return node.nodeType === Node.TEXT_NODE || node.nodeType === Node.CDATA_SECTION_NODE; -} - -function isWhitespaceOnlyText(node) { - return isTextLikeNode(node) && !node.textContent.trim(); -} - -function markupAttributes(el) { - return [...el.attributes] - .filter((a) => a.name !== "xmlns" || a.value !== DOCLANG_NS) - .map((a) => ({ name: a.name, value: a.value })); -} - -function xmlSpan(className, text, { ghost = false } = {}) { - const span = document.createElement("span"); - span.className = ghost ? `${className} markup-ghost-tag-part` : className; - span.textContent = text; - return span; -} - -function createMarkupLineRow(depth, { foldToggle = false } = {}) { - const line = createMarkupLine(); - line.style.setProperty("--markup-depth", String(depth)); - - const row = document.createElement("span"); - row.className = "markup-line-content"; - - const gutter = document.createElement("span"); - gutter.className = "markup-gutter"; - gutter.setAttribute("aria-hidden", "true"); - if (foldToggle) gutter.appendChild(createMarkupFoldToggle()); - row.appendChild(gutter); - - const body = document.createElement("span"); - body.className = "markup-line-body"; - row.appendChild(body); - - line.appendChild(row); - return { line, content: body }; -} - -function isTruncatableEmbeddedImageUri(value) { - if (!value || value.length <= LONG_EMBEDDED_URI_PREVIEW_LENGTH) return false; - return /^(data:image\/|blob:)/i.test(value); -} - -function formatCompactByteSize(bytes) { - if (bytes < 1024) return `${bytes} B`; - if (bytes < 1024 * 1024) { - const kb = bytes / 1024; - return kb < 10 ? `${kb.toFixed(1)} KB` : `${Math.round(kb)} KB`; - } - return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; -} - -function formatEmbeddedUriSizeLabel(value) { - if (/^blob:/i.test(value)) { - return value.length >= 1024 ? `${Math.round(value.length / 1024)} KB URL` : `${value.length} char URL`; - } - const comma = value.indexOf(","); - if (comma === -1) return "embedded data"; - const header = value.slice(0, comma); - const payload = value.slice(comma + 1).replace(/\s/g, ""); - const mime = /^data:([^;,]+)/i.exec(header)?.[1] ?? ""; - const shortMime = mime.startsWith("image/") ? mime.slice(6) : (mime || "data"); - const bytes = Math.floor(payload.replace(/=+$/, "").length * 3 / 4); - return `${shortMime} · ${formatCompactByteSize(bytes)}`; -} - -function createEmbeddedUriContinuationPanel(value, depth) { - const { line, content } = createMarkupLineRow(depth); - line.className = "markup-line markup-embedded-uri-panel"; - const body = document.createElement("div"); - body.className = "markup-embedded-uri-panel-body"; - body.textContent = value; - body.addEventListener("click", (e) => e.stopPropagation()); - content.appendChild(body); - return line; -} - -function createTruncatableMarkupAttrValue(value) { - const wrapper = document.createElement("span"); - wrapper.className = "xml-attr-value xml-attr-value-truncatable"; - wrapper.dataset.fullValue = value; - - const text = document.createElement("span"); - text.className = "xml-attr-value-text"; - text.textContent = value.slice(0, LONG_EMBEDDED_URI_PREVIEW_LENGTH); - - const toggle = document.createElement("button"); - toggle.type = "button"; - toggle.className = "xml-attr-value-chip"; - const sizeLabel = formatEmbeddedUriSizeLabel(value); - toggle.dataset.collapsedLabel = sizeLabel; - toggle.setAttribute("aria-expanded", "false"); - toggle.setAttribute("aria-label", `Show full value (${sizeLabel})`); - - const label = document.createElement("span"); - label.className = "xml-attr-value-chip-label"; - label.textContent = sizeLabel; - toggle.appendChild(label); - - wrapper.append(text, toggle); - return wrapper; -} - -function appendMarkupAttrValue(line, value) { - if (isTruncatableEmbeddedImageUri(value)) { - line.appendChild(createTruncatableMarkupAttrValue(value)); - } else { - line.appendChild(xmlSpan("xml-attr-value", value)); - } -} - -function toggleTruncatableMarkupAttrValue(toggle) { - const wrapper = toggle.closest(".xml-attr-value-truncatable"); - if (!wrapper) return; - - const markupLine = wrapper.closest(".markup-line"); - const fullValue = wrapper.dataset.fullValue ?? ""; - const label = toggle.querySelector(".xml-attr-value-chip-label"); - const collapsedLabel = toggle.dataset.collapsedLabel ?? label?.textContent ?? ""; - const expanded = toggle.getAttribute("aria-expanded") === "true"; - - if (expanded) { - toggle.setAttribute("aria-expanded", "false"); - toggle.setAttribute("aria-label", `Show full value (${collapsedLabel})`); - if (label) label.textContent = collapsedLabel; - markupLine?.nextElementSibling?.classList.contains("markup-embedded-uri-panel") - && markupLine.nextElementSibling.remove(); - return; - } - - toggle.setAttribute("aria-expanded", "true"); - toggle.setAttribute("aria-label", "Hide full value"); - if (label) label.textContent = "hide"; - if (!markupLine || markupLine.nextElementSibling?.classList.contains("markup-embedded-uri-panel")) { - return; - } - const depth = Number(markupLine.style.getPropertyValue("--markup-depth") || 0); - markupLine.insertAdjacentElement("afterend", createEmbeddedUriContinuationPanel(fullValue, depth)); -} - -function appendMarkupAttributes(line, attributes) { - for (const { name, value } of attributes) { - line.appendChild(document.createTextNode(" ")); - line.appendChild(xmlSpan("xml-attr-name", name)); - line.appendChild(xmlSpan("xml-bracket", '="')); - appendMarkupAttrValue(line, value); - line.appendChild(xmlSpan("xml-bracket", '"')); - } -} - -function appendMarkupTextContent(line, text) { - const cdataMatch = /^$/.exec(text); - if (cdataMatch) { - line.appendChild(xmlSpan("xml-cdata-delimiter", "")); - return; - } - line.appendChild(xmlSpan("xml-text", text)); -} - -function createMarkupLine() { - const line = document.createElement("div"); - line.className = "markup-line"; - return line; -} - -function appendOpenTagContent(line, tag, attributes, ghost = false) { - line.appendChild(xmlSpan("xml-bracket", "<", { ghost })); - line.appendChild(xmlSpan("xml-tag", tag, { ghost })); - appendMarkupAttributes(line, attributes); - line.appendChild(xmlSpan("xml-bracket", ">", { ghost })); -} - -function createMarkupFoldToggle() { - const btn = document.createElement("button"); - btn.type = "button"; - btn.className = "markup-fold-toggle"; - btn.setAttribute("aria-expanded", "true"); - btn.setAttribute("aria-label", "Collapse"); - return btn; -} - -function appendMarkupFoldableOpen(parent, depth, tag, attributes, tagHint) { - const ghost = Boolean(tagHint); - const { line, content } = createMarkupLineRow(depth, { foldToggle: true }); - line.classList.add("markup-line-open"); - appendOpenTagContent(content, tag, attributes, ghost); - - const suffix = document.createElement("span"); - suffix.className = "markup-fold-suffix"; - suffix.appendChild(xmlSpan("xml-bracket", "...", { ghost })); - suffix.appendChild(xmlSpan("xml-bracket", "", { ghost })); - content.appendChild(suffix); - parent.appendChild(line); -} - -function appendMarkupCloseTag(parent, depth, tag, tagHint) { - const ghost = Boolean(tagHint); - const { line, content } = createMarkupLineRow(depth); - content.appendChild(xmlSpan("xml-bracket", "", { ghost })); - parent.appendChild(line); -} - -function appendMarkupSelfClosingTag(parent, depth, tag, attributes) { - const { line, content } = createMarkupLineRow(depth); - content.appendChild(xmlSpan("xml-bracket", "<")); - content.appendChild(xmlSpan("xml-tag", tag)); - appendMarkupAttributes(content, attributes); - content.appendChild(xmlSpan("xml-bracket", "/>")); - parent.appendChild(line); -} - -function appendMarkupInlineElement(parent, depth, tag, attributes, text) { - const { line, content } = createMarkupLineRow(depth); - appendOpenTagContent(content, tag, attributes); - appendMarkupTextContent(content, text); - content.appendChild(xmlSpan("xml-bracket", "")); - parent.appendChild(line); -} - -function appendMarkupTextLine(parent, depth, text) { - const { line, content } = createMarkupLineRow(depth); - appendMarkupTextContent(content, text); - parent.appendChild(line); -} - -function formatMarkupTextNode(node) { - if (node.nodeType === Node.CDATA_SECTION_NODE) { - return ``; - } - return node.textContent.trim(); -} - -function serializeMarkupTextNodes(nodes) { - return [...nodes] - .filter((n) => isTextLikeNode(n) && !isWhitespaceOnlyText(n)) - .map(formatMarkupTextNode) - .join(""); -} -const PAGE_WHEEL_COOLDOWN_MS = 200; -const PAGE_WHEEL_PIXEL_THRESHOLD = 4; -const PAGE_WHEEL_GESTURE_MS = 100; -const OVERLAY_BADGE_FONT_SIZE = 11 * 1.5 * 0.8; -const OVERLAY_BADGE_PAD_X = 3; -const OVERLAY_BADGE_PAD_Y = 2; -const OVERLAY_BADGE_RADIUS_SCREEN_PX = 3; -/** Demo page size; overlay lengths are calibrated to match pre-fix sizing on these images. */ -const OVERLAY_REF_IMAGE_WIDTH = 1224; -const OVERLAY_REF_IMAGE_HEIGHT = 1584; -const PAGE_ZOOM_DEFAULT = 100; -const PAGE_PAN_DRAG_THRESHOLD = 5; -const PAGE_VIEW_BORDER_PX = 2; -const LAYOUT_STORAGE_KEY = "doclang-viewer-pane-layout"; -const PANE_MIN_RATIO = 0.12; -const PANE_KEYS = ["file", "page", "markup", "reading"]; -const DEFAULT_PANE_RATIOS = [1, 1, 1, 1]; -const DEFAULT_USER_PANE_VISIBLE = { file: false, page: true, markup: true, reading: true }; -const LAYOUT_STACK_BREAKPOINT_PX = 1200; - -/** @type {{ pageImages: Map, assetUrls: Map, currentPage: number, pageCount: number, segments: Element[][], defaultResolution: { width: number, height: number }, elementIds: Map, idToElement: Map, hasPageView: boolean, markupOnly: boolean, docRoot: Element, threadPagesById: Map>, elementPageByEl: Map, threadNavByElement: Map, pendingSelectElement: Element | null, readingOrder: Element[], readingOrderDisplayNumbers: Map, pageViewOverlay: { boxes: object[], readingOrderSteps: { order: number, box: object, elementId: string }[] } | null } | null} */ -let state = null; -let fileCatalog = []; -let activeFileIndex = -1; -let filePaneUserToggled = false; -/** @type {ResizeObserver | null} */ -let pagePaneResizeObserver = null; -/** @type {string | null} */ -let selectedElementId = null; -let showAllBboxes = true; -let showLayoutBadges = true; -let showCaptionLinks = false; -let showPictureContents = false; -let showTableContents = false; -let showFragmentLinks = false; -let showXrefLinks = false; -let showReadingOrder = false; -let showReadingOrderArrows = true; -let readingOrderGlobalNumbering = false; -let showReadingFurniture = true; -let showReadingBackground = true; -let pageSettingsOpen = false; -let readingSettingsOpen = false; -let pageZoomPercent = PAGE_ZOOM_DEFAULT; -/** @type {{ pointerId: number, startX: number, startY: number, scrollLeft: number, scrollTop: number, moved: boolean } | null} */ -let pagePanDrag = null; -let pagePanSuppressClick = false; -/** @type {{ paneW: number, paneH: number, imgW: number, imgH: number, fitScale: number } | null} */ -let pageLayoutCache = null; -/** @type {{ file: boolean, page: boolean, markup: boolean, reading: boolean }} */ -let userPaneVisible = { ...DEFAULT_USER_PANE_VISIBLE }; -/** @type {number[]} */ -let paneRatios = [...DEFAULT_PANE_RATIOS]; -/** @type {number | null} */ -let filePaneWidthPx = null; -let toolbarOptionsOpen = false; -/** @type {{ physicalSplitterIndex: number, leftKey: string, rightKey: string, startX: number, leftStart: number, rightStart: number, pointerId: number } | null} */ -let paneDrag = null; -/** @type {MediaQueryList | null} */ -let layoutStackQuery = null; -const els = { - openFileBtn: document.getElementById("open-file-btn"), - emptyStateFileTypes: document.getElementById("empty-state-file-types"), - docLabel: document.getElementById("doc-label"), - filePane: document.getElementById("file-pane"), - filePaneCloseAll: document.getElementById("btn-file-pane-close-all"), - pageNav: document.getElementById("page-nav"), - pageIndicator: document.getElementById("page-indicator"), - pageNumberInput: document.getElementById("page-number-input"), - pageCountIndicator: document.getElementById("page-count-indicator"), - btnPrev: document.getElementById("btn-prev"), - btnNext: document.getElementById("btn-next"), - showAllBboxes: document.getElementById("show-all-bboxes"), - showLayoutBadges: document.getElementById("show-layout-badges"), - showLayoutBadgesLabel: document.getElementById("show-layout-badges-label"), - settingsToggle: document.getElementById("btn-settings-toggle"), - readingSettingsToggle: document.getElementById("btn-reading-settings-toggle"), - pageSettingsLayer: document.getElementById("page-settings-layer"), - pageSettingsScrim: document.getElementById("page-settings-scrim"), - pageSettingsClose: document.getElementById("btn-page-settings-close"), - readingSettingsLayer: document.getElementById("reading-settings-layer"), - readingSettingsScrim: document.getElementById("reading-settings-scrim"), - readingSettingsClose: document.getElementById("btn-reading-settings-close"), - showReadingFurniture: document.getElementById("show-reading-furniture"), - showReadingFurnitureLabel: document.getElementById("show-reading-furniture-label"), - showReadingBackground: document.getElementById("show-reading-background"), - showReadingBackgroundLabel: document.getElementById("show-reading-background-label"), - showCaptionLinks: document.getElementById("show-caption-links"), - showCaptionLinksLabel: document.getElementById("show-caption-links-label"), - showPictureContents: document.getElementById("show-picture-contents"), - showPictureContentsLabel: document.getElementById("show-picture-contents-label"), - showTableContents: document.getElementById("show-table-contents"), - showTableContentsLabel: document.getElementById("show-table-contents-label"), - showFragmentLinks: document.getElementById("show-fragment-links"), - showFragmentLinksLabel: document.getElementById("show-fragment-links-label"), - showXrefLinks: document.getElementById("show-xref-links"), - showXrefLinksLabel: document.getElementById("show-xref-links-label"), - showReadingOrder: document.getElementById("show-reading-order"), - showReadingOrderLabel: document.getElementById("show-reading-order-label"), - readingOrderArrows: document.getElementById("reading-order-arrows"), - readingOrderArrowsLabel: document.getElementById("reading-order-arrows-label"), - readingOrderGlobal: document.getElementById("reading-order-global"), - readingOrderGlobalLabel: document.getElementById("reading-order-global-label"), - pageZoom: document.getElementById("page-zoom"), - pageZoomLabel: document.getElementById("page-zoom-label"), - pageZoomReset: document.getElementById("page-zoom-reset"), - main: document.getElementById("main"), - emptyState: document.getElementById("empty-state"), - emptyStateLoading: document.getElementById("empty-state-loading"), - emptyStatePrompt: document.getElementById("empty-state-prompt"), - markupPane: document.getElementById("markup-pane"), - renderedPane: document.getElementById("rendered-pane"), - pagePane: document.getElementById("page-pane"), - paneFile: document.querySelector(".pane-file"), - panePageView: document.querySelector(".pane-page-view"), - paneMarkup: document.querySelector(".pane-markup"), - paneReading: document.querySelector(".pane-reading"), - splitters: [ - document.getElementById("splitter-0"), - document.getElementById("splitter-1"), - document.getElementById("splitter-2"), - ], - toolbarOptionsBtn: document.getElementById("btn-toolbar-options"), - toolbarOptionsPanel: document.getElementById("toolbar-options-panel"), - toggleFilePane: document.getElementById("toggle-file-pane"), - toggleFilePaneLabel: document.getElementById("toggle-file-pane-label"), - togglePagePane: document.getElementById("toggle-page-pane"), - toggleMarkupPane: document.getElementById("toggle-markup-pane"), - toggleReadingPane: document.getElementById("toggle-reading-pane"), - togglePagePaneLabel: document.getElementById("toggle-page-pane-label"), - resetPaneLayoutBtn: document.getElementById("btn-reset-pane-layout"), -}; - -document.getElementById("btn-demo")?.addEventListener("click", loadDemo); -document.getElementById("demo-empty-link")?.addEventListener("click", (e) => { - e.preventDefault(); - loadDemo(); -}); -document.getElementById("home-link")?.addEventListener("click", (e) => { - e.preventDefault(); - resetViewer(); -}); -document.getElementById("input-archive")?.addEventListener("change", async (e) => { - const files = [...e.target.files].filter((f) => isArchiveFile(f) || isMarkupFile(f)); - if (!files.length) return; - await addFilesToCatalog(files, { replace: true }); - e.target.value = ""; -}); -els.btnPrev?.addEventListener("click", () => goToPage(state.currentPage - 1)); -els.btnNext?.addEventListener("click", () => goToPage(state.currentPage + 1)); -els.pageNumberInput?.addEventListener("keydown", (e) => { - if (e.key === "Enter") { - e.preventDefault(); - commitPageNumberInput(); - els.pageNumberInput?.blur(); - } else if (e.key === "Escape") { - e.preventDefault(); - resetPageNumberInput(); - els.pageNumberInput?.blur(); - } -}); -els.pageNumberInput?.addEventListener("blur", resetPageNumberInput); -els.pageNumberInput?.addEventListener("focus", (e) => { - e.target.select(); -}); -els.showAllBboxes?.addEventListener("change", () => { - showAllBboxes = els.showAllBboxes.checked; - syncLayoutSubtoggles(); - const img = els.pagePane?.querySelector(".page-view img"); - if (img) syncOverlayBadges(img); - applyBboxVisibility(); -}); -els.showLayoutBadges?.addEventListener("change", () => { - showLayoutBadges = els.showLayoutBadges.checked; - const img = els.pagePane?.querySelector(".page-view img"); - if (img) syncOverlayBadges(img); - applyBboxVisibility(); -}); -els.showCaptionLinks?.addEventListener("change", () => { - showCaptionLinks = els.showCaptionLinks.checked; - applyBboxVisibility(); -}); -els.showPictureContents?.addEventListener("change", () => { - showPictureContents = els.showPictureContents.checked; - applyBboxVisibility(); -}); -els.showTableContents?.addEventListener("change", () => { - showTableContents = els.showTableContents.checked; - applyBboxVisibility(); -}); -els.showFragmentLinks?.addEventListener("change", () => { - showFragmentLinks = els.showFragmentLinks.checked; - applyBboxVisibility(); -}); -els.showXrefLinks?.addEventListener("change", () => { - showXrefLinks = els.showXrefLinks.checked; - applyBboxVisibility(); -}); -els.showReadingOrder?.addEventListener("change", () => { - showReadingOrder = els.showReadingOrder.checked; - syncLayoutSubtoggles(); - const img = els.pagePane?.querySelector(".page-view img"); - if (img) syncOverlayBadges(img); - applyBboxVisibility(); -}); -els.readingOrderArrows?.addEventListener("change", () => { - showReadingOrderArrows = els.readingOrderArrows.checked; - applyBboxVisibility(); -}); -els.readingOrderGlobal?.addEventListener("change", () => { - readingOrderGlobalNumbering = els.readingOrderGlobal.checked; - if (state) renderPage(state.currentPage); -}); -els.settingsToggle?.addEventListener("click", () => setPageSettingsOpen(!pageSettingsOpen)); -els.readingSettingsToggle?.addEventListener("click", () => setReadingSettingsOpen(!readingSettingsOpen)); -els.pageSettingsClose?.addEventListener("click", () => setPageSettingsOpen(false)); -els.pageSettingsScrim?.addEventListener("click", () => setPageSettingsOpen(false)); -els.readingSettingsClose?.addEventListener("click", () => setReadingSettingsOpen(false)); -els.readingSettingsScrim?.addEventListener("click", () => setReadingSettingsOpen(false)); -document.addEventListener("keydown", (e) => { - if (e.key !== "Escape") return; - if (toolbarOptionsOpen) setToolbarOptionsOpen(false); - else if (pageSettingsOpen) setPageSettingsOpen(false); - else if (readingSettingsOpen) setReadingSettingsOpen(false); -}); -els.showReadingFurniture?.addEventListener("change", () => { - showReadingFurniture = els.showReadingFurniture.checked; - syncReadingLayerVisibility(); -}); -els.showReadingBackground?.addEventListener("change", () => { - showReadingBackground = els.showReadingBackground.checked; - syncReadingLayerVisibility(); -}); -loadLayoutPrefs(); -normalizePaneRatios(); -initToolbarOptions(); -initPaneSplitters(); -initLayoutStackListener(); -initFileTypeHints(); -initCursorHints(); -initBboxHints(); -initDragDrop(); -initFilePaneCloseAll(); -initPageWheelNav(); -initPageViewControls(); -let demoLoadInProgress = false; - -function setDemoLoading(loading) { - document.body.classList.toggle("demo-loading", loading); - const btnDemo = document.getElementById("btn-demo"); - if (btnDemo) btnDemo.disabled = loading; -} - -if (document.getElementById("btn-demo")) loadDemo(); - -async function loadDemo() { - if (demoLoadInProgress) return; - demoLoadInProgress = true; - setDemoLoading(true); - try { - const res = await fetch(DEMO_ARCHIVE_URL); - if (!res.ok) throw new Error(`HTTP ${res.status}`); - const label = DEMO_ARCHIVE_URL.split("/").pop() || "demo.dclx"; - await addArchiveBufferToCatalog(await res.arrayBuffer(), label, { replace: true }); - } catch (err) { - alert( - `Failed to load demo: ${err.message}\n\nServe this directory over HTTP (e.g. python3 -m http.server) and open the viewer from localhost.`, - ); - } finally { - demoLoadInProgress = false; - if (!document.body.classList.contains("viewer-loaded")) setDemoLoading(false); - } -} - -async function loadFromFileList(fileList) { - if (!fileList?.length) return; - const files = [...fileList]; - if (files.some((f) => f.name === "document.xml")) { - await appendFolderArchive(files); - return; - } - const supported = files.filter((f) => isArchiveFile(f) || isMarkupFile(f)); - if (supported.length) await addFilesToCatalog(supported, { replace: false }); -} - -function createFileCatalogEntry(file) { - return { - id: crypto.randomUUID(), - label: file.name, - kind: isMarkupFile(file) ? "markup" : "archive", - source: file, - currentPage: 1, - pageZoom: PAGE_ZOOM_DEFAULT, - snapshot: null, - thumbnailUrl: null, - }; -} - -function pageImageMimeFromExt(ext) { - const normalized = ext.toLowerCase().replace("jpeg", "jpg"); - if (normalized === "png") return "image/png"; - if (normalized === "webp") return "image/webp"; - return "image/jpeg"; -} - -function createPageImageObjectUrl(data, ext) { - return URL.createObjectURL(new Blob([data], { type: pageImageMimeFromExt(ext) })); -} - -function createFirstPageImageUrlFromFiles(files) { - let bestPage = Infinity; - /** @type {File | null} */ - let bestFile = null; - for (const f of files) { - const relPath = f.webkitRelativePath || f.name; - const parts = relPath.split("/"); - if (parts.length < 2 || parts[parts.length - 2] !== "pages") continue; - const m = PAGE_IMAGE_RE.exec(f.name); - if (!m) continue; - const pageNum = Number(m[1]); - if (pageNum < bestPage) { - bestPage = pageNum; - bestFile = f; - } - } - return bestFile ? URL.createObjectURL(bestFile) : null; -} - -async function createFirstPageImageUrlFromZip(source) { - const buffer = source instanceof File ? await source.arrayBuffer() : source; - const entries = await unzip(buffer, { - shouldExtract: (name) => /^pages\/\d+\.(png|jpe?g|webp)$/i.test(name), - }); - let bestPage = Infinity; - /** @type {{ name: string, data: Uint8Array } | null} */ - let bestEntry = null; - for (const e of entries) { - const m = e.name.match(/^pages\/(\d+)\.(png|jpe?g|webp)$/i); - if (!m) continue; - const pageNum = Number(m[1]); - if (pageNum < bestPage) { - bestPage = pageNum; - bestEntry = e; - } - } - if (!bestEntry) return null; - const ext = bestEntry.name.split(".").pop() ?? "png"; - return createPageImageObjectUrl(bestEntry.data, ext); -} - -async function resolveCatalogEntryThumbnail(entry) { - if (entry.thumbnailUrl) return entry.thumbnailUrl; - if (entry.kind === "markup") return null; - try { - if (entry.kind === "folder") { - entry.thumbnailUrl = createFirstPageImageUrlFromFiles(entry.source); - } else if (entry.kind === "archive") { - entry.thumbnailUrl = await createFirstPageImageUrlFromZip(entry.source); - } - } catch { - entry.thumbnailUrl = null; - } - return entry.thumbnailUrl; -} - -function enrichCatalogEntryThumbnail(entry) { - resolveCatalogEntryThumbnail(entry).then((url) => { - if (!fileCatalog.includes(entry)) { - if (url?.startsWith("blob:")) URL.revokeObjectURL(url); - return; - } - if (url) renderFileView(); - }); -} - -function revokeCatalogEntry(entry) { - if (entry?.thumbnailUrl?.startsWith("blob:")) { - URL.revokeObjectURL(entry.thumbnailUrl); - } - if (entry) entry.thumbnailUrl = null; -} - -function createFileViewThumbnail(entry) { - const thumb = document.createElement("span"); - thumb.className = "file-view-thumb"; - thumb.setAttribute("aria-hidden", "true"); - if (entry.thumbnailUrl) { - const img = document.createElement("img"); - img.src = entry.thumbnailUrl; - img.alt = ""; - thumb.appendChild(img); - } else { - const placeholder = document.createElement("span"); - placeholder.className = "file-view-thumb-placeholder"; - placeholder.innerHTML = FILE_THUMB_PLACEHOLDER_SVG; - thumb.appendChild(placeholder); - } - return thumb; -} - -async function addFilesToCatalog(files, { replace = false } = {}) { - if (replace) { - clearFileCatalog(); - filePaneUserToggled = false; - } - const startIndex = fileCatalog.length; - for (const file of files) { - const entry = createFileCatalogEntry(file); - fileCatalog.push(entry); - enrichCatalogEntryThumbnail(entry); - } - if (!fileCatalog.length) return; - await switchToFile(replace ? 0 : startIndex); -} - -async function appendFolderArchive(files) { - if (!files.some((f) => f.name === "document.xml")) { - alert("Archive must contain document.xml at its root."); - return; - } - const rootName = (files[0].webkitRelativePath || files[0].name).split("/")[0] || "archive"; - const entry = { - id: crypto.randomUUID(), - label: rootName, - kind: "folder", - source: files, - currentPage: 1, - pageZoom: PAGE_ZOOM_DEFAULT, - snapshot: null, - thumbnailUrl: null, - }; - fileCatalog.push(entry); - enrichCatalogEntryThumbnail(entry); - await switchToFile(fileCatalog.length - 1); -} - -async function addArchiveBufferToCatalog(buffer, label, { replace = false } = {}) { - if (replace) { - clearFileCatalog(); - filePaneUserToggled = false; - } - const entry = { - id: crypto.randomUUID(), - label, - kind: "archive", - source: buffer, - currentPage: 1, - pageZoom: PAGE_ZOOM_DEFAULT, - snapshot: null, - thumbnailUrl: null, - }; - fileCatalog.push(entry); - enrichCatalogEntryThumbnail(entry); - await switchToFile(replace ? 0 : fileCatalog.length - 1); -} - -async function extractArchiveFromFiles(files) { - const markupFile = files.find((f) => f.name === "document.xml"); - if (!markupFile) throw new Error("Archive must contain document.xml at its root."); - const markupXml = await markupFile.text(); - const pageImages = new Map(); - const assetUrls = new Map(); - for (const f of files) { - const relPath = f.webkitRelativePath || f.name; - const parts = relPath.split("/"); - if (parts.length >= 2 && parts[parts.length - 2] === "pages") { - const m = PAGE_IMAGE_RE.exec(f.name); - if (m) pageImages.set(Number(m[1]), URL.createObjectURL(f)); - } - const assetPath = archiveRelativeAssetPath(relPath); - if (assetPath) assetUrls.set(assetPath, URL.createObjectURL(f)); - } - return { markupXml, pageImages, assetUrls }; -} - -async function parseCatalogEntry(entry) { - try { - if (entry.kind === "markup") { - const text = entry.source instanceof File - ? await entry.source.text() - : new TextDecoder().decode(entry.source); - return buildDocumentState(text, new Map(), entry.label, new Map(), { markupOnly: true }); - } - if (entry.kind === "archive") { - const buffer = entry.source instanceof File - ? await entry.source.arrayBuffer() - : entry.source; - const { markupXml, pageImages, assetUrls } = await extractArchiveFromZipBuffer(buffer); - return buildDocumentState(markupXml, pageImages, entry.label, assetUrls, { markupOnly: false }); - } - if (entry.kind === "folder") { - const { markupXml, pageImages, assetUrls } = await extractArchiveFromFiles(entry.source); - return buildDocumentState(markupXml, pageImages, entry.label, assetUrls, { markupOnly: false }); - } - } catch (err) { - alert(`Failed to read ${entry.label}: ${err.message}`); - } - return null; -} - -function persistActiveFileViewState() { - if (activeFileIndex < 0 || !state) return; - const entry = fileCatalog[activeFileIndex]; - entry.currentPage = state.currentPage; - entry.pageZoom = pageZoomPercent; -} - -function releaseActiveDocument() { - if (activeFileIndex >= 0) { - const entry = fileCatalog[activeFileIndex]; - if (entry?.snapshot) { - revokeDocumentState(entry.snapshot); - entry.snapshot = null; - } - } - if (state) revokeDocumentState(state); - state = null; - selectedElementId = null; - pagePanDrag = null; - pagePanSuppressClick = false; -} - -function clearFileCatalog() { - releaseActiveDocument(); - for (const entry of fileCatalog) revokeCatalogEntry(entry); - fileCatalog = []; - activeFileIndex = -1; -} - -async function switchToFile(index) { - if (index < 0 || index >= fileCatalog.length) return; - - persistActiveFileViewState(); - releaseActiveDocument(); - - activeFileIndex = index; - const entry = fileCatalog[index]; - const docState = await parseCatalogEntry(entry); - if (!docState) { - revokeCatalogEntry(entry); - fileCatalog.splice(index, 1); - activeFileIndex = -1; - if (fileCatalog.length) { - await switchToFile(Math.min(index, fileCatalog.length - 1)); - } else { - resetViewer(); - } - return; - } - - entry.snapshot = docState; - docState.currentPage = entry.currentPage ?? 1; - activateDocument(docState, entry); -} - -function defaultFilePaneVisible() { - return fileCatalog.length > 1; -} - -function syncFilePaneDefault() { - if (!filePaneUserToggled) { - const wasVisible = userPaneVisible.file; - const shouldBeVisible = defaultFilePaneVisible(); - userPaneVisible.file = shouldBeVisible; - if (!wasVisible && shouldBeVisible) { - paneRatios = [...DEFAULT_PANE_RATIOS]; - normalizePaneRatios(); - filePaneWidthPx = null; - } - } -} - -async function closeCatalogFile(index) { - if (index < 0 || index >= fileCatalog.length) return; - - const wasActive = index === activeFileIndex; - const entry = fileCatalog[index]; - - if (wasActive) { - releaseActiveDocument(); - activeFileIndex = -1; - } - - revokeCatalogEntry(entry); - fileCatalog.splice(index, 1); - - if (!fileCatalog.length) { - resetViewer(); - return; - } - - if (wasActive) { - await switchToFile(Math.min(index, fileCatalog.length - 1)); - return; - } - - if (index < activeFileIndex) { - activeFileIndex -= 1; - } - updateFileView(); -} - -function renderFileView() { - if (!els.filePane) return; - els.filePane.replaceChildren(); - if (!fileCatalog.length) return; - - const list = document.createElement("ul"); - list.className = "file-view-list"; - list.setAttribute("role", "listbox"); - list.setAttribute("aria-label", "Open files"); - - fileCatalog.forEach((entry, index) => { - const item = document.createElement("li"); - const card = document.createElement("div"); - card.className = "file-view-item"; - card.title = entry.label; - card.tabIndex = 0; - card.setAttribute("role", "option"); - if (index === activeFileIndex) { - card.classList.add("is-active"); - card.setAttribute("aria-selected", "true"); - } else { - card.setAttribute("aria-selected", "false"); - } - - const thumbWrap = document.createElement("div"); - thumbWrap.className = "file-view-thumb-wrap"; - - const closeBtn = document.createElement("button"); - closeBtn.type = "button"; - closeBtn.className = "file-view-close"; - closeBtn.setAttribute("aria-label", `Close ${entry.label}`); - closeBtn.textContent = "×"; - closeBtn.addEventListener("click", (e) => { - e.stopPropagation(); - closeCatalogFile(index); - }); - - thumbWrap.append(createFileViewThumbnail(entry), closeBtn); - - const label = document.createElement("span"); - label.className = "file-view-label"; - label.textContent = entry.label; - - card.append(thumbWrap, label); - card.addEventListener("click", (e) => { - if (e.target.closest(".file-view-close")) return; - switchToFile(index); - }); - card.addEventListener("keydown", (e) => { - if (e.key === "Enter" || e.key === " ") { - e.preventDefault(); - switchToFile(index); - } - }); - - item.appendChild(card); - list.appendChild(item); - }); - - els.filePane.appendChild(list); -} - -function updateFileView() { - syncFilePaneDefault(); - syncFilePaneCloseAllButton(); - renderFileView(); - syncToolbarPaneCheckboxes(); - applyPaneLayout(); -} - -function syncFilePaneCloseAllButton() { - if (!els.filePaneCloseAll) return; - els.filePaneCloseAll.hidden = fileCatalog.length === 0; -} - -function initFilePaneCloseAll() { - els.filePaneCloseAll?.addEventListener("click", () => { - if (!fileCatalog.length) return; - const count = fileCatalog.length; - const message = count === 1 - ? `Remove "${fileCatalog[0].label}" from the viewer?` - : `Remove all ${count} open files from the viewer?`; - if (confirm(message)) resetViewer(); - }); -} - -function initPageWheelNav() { - if (!els.pagePane) return; - - let pixelAccum = 0; - let pixelGestureUntil = 0; - let lastFlipAt = 0; - - function wheelDir(e) { - if (e.deltaMode === 1) { - return e.deltaY > 0 ? 1 : e.deltaY < 0 ? -1 : 0; - } - if (e.deltaMode === 2) { - return Math.sign(e.deltaY); - } - const now = performance.now(); - if (now > pixelGestureUntil) pixelAccum = 0; - pixelGestureUntil = now + PAGE_WHEEL_GESTURE_MS; - pixelAccum += e.deltaY; - if (Math.abs(pixelAccum) >= PAGE_WHEEL_PIXEL_THRESHOLD) { - const dir = pixelAccum > 0 ? 1 : -1; - pixelAccum = 0; - return dir; - } - return 0; - } - - function tryFlipPage(dir) { - if (!dir || !state) return false; - const now = performance.now(); - if (now - lastFlipAt < PAGE_WHEEL_COOLDOWN_MS) return false; - const before = state.currentPage; - goToPage(state.currentPage + dir); - if (state.currentPage !== before) { - lastFlipAt = now; - return true; - } - return false; - } - - function isScrollAtTop(pane) { - return pane.scrollTop <= 0; - } - - function isScrollAtBottom(pane) { - return pane.scrollTop + pane.clientHeight >= pane.scrollHeight - 1; - } - - function onScrollPaneWheel(e, pane) { - if (!state || state.markupOnly || state.pageCount <= 1) return; - const dir = wheelDir(e); - if (!dir) return; - - const atTop = isScrollAtTop(pane); - const atBottom = isScrollAtBottom(pane); - const wantPrev = dir < 0 && atTop; - const wantNext = dir > 0 && atBottom; - if (!wantPrev && !wantNext) return; - - e.preventDefault(); - if (!tryFlipPage(dir)) return; - - requestAnimationFrame(() => { - pane.scrollTop = dir > 0 ? 0 : pane.scrollHeight; - }); - } - - els.pagePane.addEventListener( - "wheel", - (e) => { - if (!state?.hasPageView) return; - const pane = pageViewScrollPane(); - if (!pane) return; - const scrollable = pane.scrollHeight > pane.clientHeight || pane.scrollWidth > pane.clientWidth; - if (scrollable) { - onScrollPaneWheel(e, pane); - return; - } - e.preventDefault(); - const dir = wheelDir(e); - if (dir) tryFlipPage(dir); - }, - { passive: false }, - ); - - for (const pane of [els.markupPane, els.renderedPane]) { - if (!pane) continue; - pane.addEventListener("wheel", (e) => onScrollPaneWheel(e, pane), { passive: false }); - } - - els.pagePane.tabIndex = 0; - els.pagePane.setAttribute("role", "region"); - els.pagePane.setAttribute("aria-label", "Original page"); - - els.pagePane.addEventListener("pointerdown", (e) => { - if (state?.hasPageView) els.pagePane.focus({ preventScroll: true }); - }); - - els.pagePane.addEventListener("keydown", (e) => { - if (!state?.hasPageView) return; - - let dir = 0; - switch (e.key) { - case "ArrowDown": - case "PageDown": - case "ArrowRight": - dir = 1; - break; - case "ArrowUp": - case "PageUp": - case "ArrowLeft": - dir = -1; - break; - default: - return; - } - - e.preventDefault(); - tryFlipPage(dir); - }); -} - -function pageViewScrollPane() { - if (!els.pagePane) return null; - return els.pagePane.querySelector(".page-view-port") ?? els.pagePane; -} - -function isPagePaneScrollable() { - const pane = pageViewScrollPane(); - if (!pane) return false; - return pane.scrollWidth > pane.clientWidth || pane.scrollHeight > pane.clientHeight; -} - -function canStartPagePan(event) { - if (!(event.target instanceof Element)) return false; - if (!event.target.closest(".page-view")) return false; - return isPagePaneScrollable(); -} - -function updatePagePanePanCursor() { - if (!els.pagePane) return; - els.pagePane.classList.toggle("can-pan", isPagePaneScrollable() && !pagePanDrag); -} - -function initPageViewControls() { - if (!els.pagePane) return; - - els.pageZoom?.addEventListener("input", () => { - pageZoomPercent = Math.max(PAGE_ZOOM_DEFAULT, Number(els.pageZoom.value)); - if (Number(els.pageZoom.value) < PAGE_ZOOM_DEFAULT) { - els.pageZoom.value = String(pageZoomPercent); - } - els.pageZoom.setAttribute("aria-valuenow", String(pageZoomPercent)); - updatePageZoomResetButton(); - refreshPageViewLayout(); - }); - - els.pageZoomReset?.addEventListener("click", () => { - if (pageZoomPercent === PAGE_ZOOM_DEFAULT) return; - resetPageZoom(); - }); - - els.pagePane.addEventListener("pointerdown", (e) => { - if (e.button !== 0 || !canStartPagePan(e)) return; - const scrollPane = pageViewScrollPane(); - if (!scrollPane) return; - pagePanDrag = { - pointerId: e.pointerId, - startX: e.clientX, - startY: e.clientY, - scrollLeft: scrollPane.scrollLeft, - scrollTop: scrollPane.scrollTop, - moved: false, - }; - }); - - els.pagePane.addEventListener("pointermove", (e) => { - if (!pagePanDrag || e.pointerId !== pagePanDrag.pointerId) return; - const dx = e.clientX - pagePanDrag.startX; - const dy = e.clientY - pagePanDrag.startY; - if (!pagePanDrag.moved && Math.hypot(dx, dy) >= PAGE_PAN_DRAG_THRESHOLD) { - pagePanDrag.moved = true; - els.pagePane.classList.add("is-panning"); - els.pagePane.classList.remove("can-pan"); - els.pagePane.setPointerCapture(e.pointerId); - } - if (!pagePanDrag.moved) return; - const scrollPane = pageViewScrollPane(); - if (!scrollPane) return; - scrollPane.scrollLeft = pagePanDrag.scrollLeft + pagePanDrag.startX - e.clientX; - scrollPane.scrollTop = pagePanDrag.scrollTop + pagePanDrag.startY - e.clientY; - e.preventDefault(); - }); - - function endPagePan(e) { - if (!pagePanDrag || e.pointerId !== pagePanDrag.pointerId) return; - if (pagePanDrag.moved) pagePanSuppressClick = true; - pagePanDrag = null; - els.pagePane.classList.remove("is-panning"); - if (els.pagePane.hasPointerCapture(e.pointerId)) { - els.pagePane.releasePointerCapture(e.pointerId); - } - updatePagePanePanCursor(); - } - - els.pagePane.addEventListener("pointerup", endPagePan); - els.pagePane.addEventListener("pointercancel", endPagePan); -} - -function escapeHtml(text) { - return String(text) - .replace(/&/g, "&") - .replace(//g, ">"); -} - -function headTextPreview(el, maxLen = 72) { - const text = el.textContent?.replace(/\s+/g, " ").trim() ?? ""; - if (!text) return "—"; - return text.length > maxLen ? `${text.slice(0, maxLen)}…` : text; -} - -function virtualTextHeadLocations(el) { - const parent = el.parentElement; - if (!parent) return []; - const nodes = [...parent.childNodes]; - const idx = nodes.indexOf(el); - if (idx < 0) return []; - return parseElementHeadAt(nodes, idx + 1)?.locs ?? []; -} - -function elementHeadLocations(el) { - const own = headLocations(el); - return own.length === 4 ? own : virtualTextHeadLocations(el); -} - -function firstHeadChild(el, tag) { - return childElements(el).find((child) => localName(child) === tag) ?? null; -} - -const ELEMENT_LAYERS = new Set(["body", "background", "furniture"]); - -function layerFromHeadNodes(nodes, startIdx) { - let i = startIdx; - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType !== Node.ELEMENT_NODE) { - i += 1; - continue; - } - const tag = localName(node); - if (tag === "layer") { - const value = node.getAttribute("value") ?? "body"; - return ELEMENT_LAYERS.has(value) ? value : "body"; - } - if (tag === "location") break; - if (HEAD_TAGS.has(tag)) { - i += 1; - continue; - } - break; - } - return "body"; -} - -function elementLayer(el) { - const layerEl = firstHeadChild(el, "layer"); - if (layerEl) { - const value = layerEl.getAttribute("value") ?? "body"; - return ELEMENT_LAYERS.has(value) ? value : "body"; - } - if (headLocations(el).length === 4) return "body"; - const parent = el.parentElement; - if (!parent) return "body"; - const nodes = [...parent.childNodes]; - const idx = nodes.indexOf(el); - if (idx < 0) return "body"; - return layerFromHeadNodes(nodes, idx + 1); -} - -function layerClassForValue(layer) { - if (layer === "furniture") return "layer-furniture"; - if (layer === "background") return "layer-background"; - return ""; -} - -function applyElementLayerAttr(sourceEl, domEl) { - domEl.setAttribute("data-doclang-layer", elementLayer(sourceEl)); -} - -function syncReadingLayerCheckboxes() { - if (els.showReadingFurniture) els.showReadingFurniture.checked = showReadingFurniture; - if (els.showReadingBackground) els.showReadingBackground.checked = showReadingBackground; -} - -/** @returns {{ key: string, value: string, isDefault: boolean }[]} */ -function collectElementHeadInfo(el, defaultResolution) { - const labelEl = firstHeadChild(el, "label"); - const threadEl = firstHeadChild(el, "thread"); - const xrefEl = firstHeadChild(el, "xref"); - const hrefEl = firstHeadChild(el, "href"); - const layerEl = firstHeadChild(el, "layer"); - const captionEl = firstHeadChild(el, "caption"); - const descriptionEl = firstHeadChild(el, "description"); - const summaryEl = firstHeadChild(el, "summary"); - const customEl = firstHeadChild(el, "custom"); - const locs = elementHeadLocations(el); - const rows = [{ key: "element", value: elementLabel(el), isDefault: false }]; - - rows.push({ - key: "label", - value: labelEl?.getAttribute("value") ?? "undefined", - isDefault: !labelEl?.hasAttribute("value"), - }); - - if (threadEl) { - rows.push({ - key: "thread_id", - value: threadEl.getAttribute("thread_id") ?? "—", - isDefault: false, - }); - } else { - rows.push({ key: "thread", value: "—", isDefault: true }); - } - - if (xrefEl) { - rows.push({ - key: "xref", - value: `thread_id ${xrefEl.getAttribute("thread_id") ?? "—"}`, - isDefault: false, - }); - } else { - rows.push({ key: "xref", value: "—", isDefault: true }); - } - - if (hrefEl) { - rows.push({ - key: "href", - value: hrefEl.getAttribute("uri") ?? "—", - isDefault: false, - }); - } else { - rows.push({ key: "href", value: "—", isDefault: true }); - } - - rows.push({ - key: "layer", - value: layerEl?.getAttribute("value") ?? "body", - isDefault: !layerEl?.hasAttribute("value"), - }); - - const cornerLabels = ["x_min", "y_min", "x_max", "y_max"]; - if (locs.length === 4) { - for (let idx = 0; idx < 4; idx += 1) { - const loc = locs[idx]; - const axisDefault = idx % 2 === 0 ? defaultResolution.width : defaultResolution.height; - const resolution = locationResolution(loc, axisDefault); - const value = loc.getAttribute("value") ?? "0"; - rows.push({ - key: cornerLabels[idx], - value: `${value} @ ${resolution}`, - isDefault: false, - }); - } - } else { - for (const key of cornerLabels) { - rows.push({ key, value: "—", isDefault: false }); - } - } - - rows.push({ - key: "caption", - value: captionEl ? headTextPreview(captionEl) : "—", - isDefault: !captionEl, - }); - rows.push({ - key: "description", - value: descriptionEl ? headTextPreview(descriptionEl) : "—", - isDefault: !descriptionEl, - }); - rows.push({ - key: "summary", - value: summaryEl ? headTextPreview(summaryEl) : "—", - isDefault: !summaryEl, - }); - rows.push({ - key: "custom", - value: customEl ? headTextPreview(customEl) : "—", - isDefault: !customEl, - }); - - return rows; -} - -function elementHeadTooltipHtml(el, defaultResolution) { - const rows = collectElementHeadInfo(el, defaultResolution); - const body = rows - .map(({ key, value, isDefault }) => { - const rendered = escapeHtml(value); - const suffix = isDefault ? ' (default)' : ""; - return `${escapeHtml(key)}${rendered}${suffix}`; - }) - .join(""); - return `${body}
`; -} - -const cursorHintEl = document.getElementById("cursor-hint"); -const CURSOR_HINT_OFFSET = 10; -const CURSOR_HINT_MARGIN = 8; - -function hideCursorHint() { - cursorHintEl.hidden = true; - cursorHintEl.classList.remove("cursor-hint-detail"); - cursorHintEl.replaceChildren(); -} - -function showCursorHint(content, clientX, clientY, { detail = false } = {}) { - cursorHintEl.replaceChildren(); - if (typeof content === "string") { - cursorHintEl.textContent = content; - } else { - cursorHintEl.appendChild(content); - } - cursorHintEl.classList.toggle("cursor-hint-detail", detail); - cursorHintEl.hidden = false; - - let left = clientX + CURSOR_HINT_OFFSET; - let top = clientY + CURSOR_HINT_OFFSET; - const rect = cursorHintEl.getBoundingClientRect(); - if (left + rect.width > window.innerWidth - CURSOR_HINT_MARGIN) { - left = clientX - rect.width - CURSOR_HINT_OFFSET; - } - if (top + rect.height > window.innerHeight - CURSOR_HINT_MARGIN) { - top = clientY - rect.height - CURSOR_HINT_OFFSET; - } - cursorHintEl.style.left = `${Math.max(CURSOR_HINT_MARGIN, left)}px`; - cursorHintEl.style.top = `${Math.max(CURSOR_HINT_MARGIN, top)}px`; -} - -function showCursorHintHtml(html, clientX, clientY) { - cursorHintEl.innerHTML = html; - cursorHintEl.classList.add("cursor-hint-detail"); - cursorHintEl.hidden = false; - - let left = clientX + CURSOR_HINT_OFFSET; - let top = clientY + CURSOR_HINT_OFFSET; - const rect = cursorHintEl.getBoundingClientRect(); - if (left + rect.width > window.innerWidth - CURSOR_HINT_MARGIN) { - left = clientX - rect.width - CURSOR_HINT_OFFSET; - } - if (top + rect.height > window.innerHeight - CURSOR_HINT_MARGIN) { - top = clientY - rect.height - CURSOR_HINT_OFFSET; - } - cursorHintEl.style.left = `${Math.max(CURSOR_HINT_MARGIN, left)}px`; - cursorHintEl.style.top = `${Math.max(CURSOR_HINT_MARGIN, top)}px`; -} - -function initFileTypeHints() { - if (!els.emptyStateFileTypes) return; - const markup = SUPPORTED_FILE_EXTENSIONS.map((ext) => `${ext}`).join(", "); - els.emptyStateFileTypes.innerHTML = markup; -} - -function initCursorHints() { - els.markupPane?.addEventListener("mousemove", (e) => { - if (!e.target.closest(".markup-ghost-tag-part")) { - hideCursorHint(); - return; - } - showCursorHint(VIRTUAL_TEXT_TAG_HINT, e.clientX, e.clientY); - }); - els.markupPane?.addEventListener("mouseleave", hideCursorHint); - - els.openFileBtn?.addEventListener("mousemove", (e) => { - showCursorHint(OPEN_FILE_HINT, e.clientX, e.clientY); - }); - els.openFileBtn?.addEventListener("mouseleave", hideCursorHint); -} - -function initBboxHints() { - if (!els.pagePane) return; - - els.pagePane.addEventListener("mousemove", (e) => { - if (pagePanDrag?.moved || els.pagePane.classList.contains("is-panning")) { - hideCursorHint(); - return; - } - const navBtn = e.target.closest(".fragment-nav-btn:not(.fragment-nav-btn-disabled)"); - if (navBtn) { - const hint = navBtn.getAttribute("data-nav") === "prev" - ? FRAGMENT_NAV_HINT_PREV - : FRAGMENT_NAV_HINT_NEXT; - showCursorHint(hint, e.clientX, e.clientY); - return; - } - const badge = e.target.closest(".element-badge[data-element-id]"); - if (!badge || !state?.idToElement) { - hideCursorHint(); - return; - } - const elementId = badge.getAttribute("data-element-id"); - const xmlEl = state.idToElement.get(elementId); - if (!xmlEl) { - hideCursorHint(); - return; - } - showCursorHintHtml(elementHeadTooltipHtml(xmlEl, state.defaultResolution), e.clientX, e.clientY); - }); - els.pagePane.addEventListener("mouseleave", hideCursorHint); -} - -function initDragDrop() { - document.body.addEventListener("dragenter", (e) => { - if (!hasArchiveTransfer(e.dataTransfer)) return; - e.preventDefault(); - document.body.classList.add("drag-over"); - }); - - document.body.addEventListener("dragover", (e) => { - if (!hasArchiveTransfer(e.dataTransfer)) return; - e.preventDefault(); - e.dataTransfer.dropEffect = "copy"; - }); - - document.body.addEventListener("dragleave", (e) => { - if (!hasArchiveTransfer(e.dataTransfer)) return; - if (e.relatedTarget && document.body.contains(e.relatedTarget)) return; - document.body.classList.remove("drag-over"); - }); - - document.body.addEventListener("drop", async (e) => { - if (!hasArchiveTransfer(e.dataTransfer)) return; - e.preventDefault(); - document.body.classList.remove("drag-over"); - await loadFromDrop(e.dataTransfer); - }); -} - -function hasArchiveTransfer(dataTransfer) { - return [...dataTransfer.types].includes("Files"); -} - -function isArchiveFile(file) { - return /\.dclx$/i.test(file.name) || /\.zip$/i.test(file.name); -} - -function isMarkupFile(file) { - return /\.(?:dclg(?:\.xml)?|xml)$/i.test(file.name); -} - -async function loadFromDrop(dataTransfer) { - const files = [...dataTransfer.files]; - if (files.some((f) => f.name === "document.xml")) { - await appendFolderArchive(files); - return; - } - const supported = files.filter((f) => isArchiveFile(f) || isMarkupFile(f)); - if (supported.length) await addFilesToCatalog(supported, { replace: false }); -} - -function buildDocumentState(markupXml, pageImages, label, assetUrls, { markupOnly }) { - const doc = new DOMParser().parseFromString(markupXml, "application/xml"); - const parseError = doc.querySelector("parsererror"); - if (parseError) { - alert(`Invalid XML in ${label}`); - return null; - } - const root = doc.documentElement; - if (localName(root) !== "doclang") { - alert(`${label}: root element must be `); - return null; - } - - const head = childElements(root).find((el) => localName(el) === "head") ?? null; - const defaultResolution = readDefaultResolution(head); - const segments = markupOnly - ? [childElements(root).filter((el) => localName(el) !== "head")] - : splitIntoSegments(root); - const hasPageView = !markupOnly && pageImages.size > 0; - const maxImagePage = hasPageView ? Math.max(...pageImages.keys()) : 0; - const pageCount = markupOnly ? 1 : Math.max(segments.length, maxImagePage, 1); - const readingOrder = computeReadingOrder(root); - const elementPageByEl = buildElementPageMap(segments); - - return { - pageImages, - assetUrls, - currentPage: 1, - pageCount, - segments, - defaultResolution, - hasPageView, - markupOnly, - docRoot: root, - threadPagesById: buildThreadPagesById(root, elementPageByEl), - elementPageByEl, - threadNavByElement: buildThreadNavByElement(root), - pendingSelectElement: null, - readingOrder, - readingOrderDisplayNumbers: computeReadingOrderDisplayNumbers(readingOrder), - pageViewOverlay: null, - }; -} - -function activateDocument(docState, entry) { - state = docState; - resetPageLayoutCache(); - closeAllSettings(); - - pageZoomPercent = entry.pageZoom ?? PAGE_ZOOM_DEFAULT; - if (els.pageZoom) { - els.pageZoom.value = String(pageZoomPercent); - els.pageZoom.setAttribute("aria-valuenow", String(pageZoomPercent)); - } - updatePageZoomResetButton(); - const port = pageViewScrollPane(); - if (port) { - port.scrollLeft = 0; - port.scrollTop = 0; - } - - setDocLabel(entry.label); - setDocumentOpen(true, { markupOnly: state.markupOnly }); - setPageViewVisible(state.hasPageView); - renderPage(state.currentPage); - updateFileView(); -} - -function setDocLabel(label) { - if (!els.docLabel) return; - if (label) { - els.docLabel.textContent = label; - els.docLabel.hidden = false; - } else { - els.docLabel.textContent = ""; - els.docLabel.hidden = true; - } -} - -function setDocumentOpen(open, { markupOnly = false } = {}) { - document.body.classList.toggle("viewer-loaded", open); - document.body.classList.toggle("markup-only", open && markupOnly); - if (els.pageNav) els.pageNav.hidden = !open || markupOnly; - syncToolbarPaneCheckboxes(); - applyPaneLayout(); -} - -function applyReadingLayerClasses(root) { - root.classList.toggle("show-reading-furniture", showReadingFurniture); - root.classList.toggle("show-reading-background", showReadingBackground); -} - -function syncReadingLayerVisibility() { - const root = els.renderedPane?.querySelector(".rendered-doc"); - if (root) applyReadingLayerClasses(root); -} - -function closeAllSettings() { - setPageSettingsOpen(false); - setReadingSettingsOpen(false); -} - -function resetPageZoom() { - pageZoomPercent = PAGE_ZOOM_DEFAULT; - if (els.pageZoom) { - els.pageZoom.value = String(PAGE_ZOOM_DEFAULT); - els.pageZoom.setAttribute("aria-valuenow", String(PAGE_ZOOM_DEFAULT)); - } - updatePageZoomResetButton(); - const port = pageViewScrollPane(); - if (port) { - port.scrollLeft = 0; - port.scrollTop = 0; - } - refreshPageViewLayout(); -} - -function updatePageZoomResetButton() { - if (!els.pageZoomReset) return; - els.pageZoomReset.textContent = `${pageZoomPercent}%`; - els.pageZoomReset.disabled = pageZoomPercent === PAGE_ZOOM_DEFAULT; -} - -function resetViewer() { - setDemoLoading(false); - clearFileCatalog(); - filePaneUserToggled = false; - selectedElementId = null; - pagePanDrag = null; - pagePanSuppressClick = false; - showReadingFurniture = true; - showReadingBackground = true; - syncReadingLayerCheckboxes(); - resetPageZoom(); - setDocLabel(null); - setDocumentOpen(false); - document.body.classList.remove("has-page-view"); - closeAllSettings(); - setToolbarOptionsOpen(false); - if (els.markupPane) els.markupPane.innerHTML = ""; - if (els.renderedPane) els.renderedPane.innerHTML = ""; - if (els.pagePane) els.pagePane.innerHTML = ""; - setPageIndicator(1, 1); - if (els.btnPrev) els.btnPrev.disabled = true; - if (els.btnNext) els.btnNext.disabled = true; - if (els.filePane) els.filePane.replaceChildren(); - updateFileView(); - applyPaneLayout(); -} - -function setPageViewVisible(visible) { - document.body.classList.toggle("has-page-view", visible); - syncPagePaneControls(); - syncToolbarPaneCheckboxes(); - if (!visible) setPageSettingsOpen(false); - applyPaneLayout(); - syncLayoutSubtoggles(); -} - -function syncPagePaneControls() { - const pageVisible = isPaneVisible("page"); - if (els.settingsToggle) els.settingsToggle.hidden = !pageVisible; - if (els.pageZoomLabel) els.pageZoomLabel.hidden = !pageVisible; - if (pageVisible) updatePageZoomResetButton(); - if (els.pagePane) els.pagePane.tabIndex = pageVisible ? 0 : -1; -} - -function setPageSettingsOpen(open) { - pageSettingsOpen = open; - if (els.pageSettingsLayer) els.pageSettingsLayer.hidden = !open; - if (els.settingsToggle) els.settingsToggle.setAttribute("aria-expanded", String(open)); -} - -function setReadingSettingsOpen(open) { - readingSettingsOpen = open; - if (els.readingSettingsLayer) els.readingSettingsLayer.hidden = !open; - if (els.readingSettingsToggle) els.readingSettingsToggle.setAttribute("aria-expanded", String(open)); -} - -function syncLayoutSubtoggles() { - const layoutEnabled = Boolean(state?.hasPageView && showAllBboxes); - for (const label of [ - els.showLayoutBadgesLabel, - els.showPictureContentsLabel, - els.showTableContentsLabel, - els.showFragmentLinksLabel, - els.showCaptionLinksLabel, - els.showXrefLinksLabel, - els.showReadingOrderLabel, - ]) { - if (!label) continue; - label.classList.toggle("settings-option-disabled", !layoutEnabled); - const input = label.querySelector("input"); - if (input) input.disabled = !layoutEnabled; - } - - const readingOrderEnabled = layoutEnabled && showReadingOrder; - for (const label of [els.readingOrderArrowsLabel, els.readingOrderGlobalLabel]) { - if (!label) continue; - label.classList.toggle("settings-option-disabled", !readingOrderEnabled); - const input = label.querySelector("input"); - if (input) input.disabled = !readingOrderEnabled; - } -} - -function paneDef(key) { - const defs = { - file: { key: "file", el: els.paneFile, canShow: () => fileCatalog.length > 0 }, - page: { key: "page", el: els.panePageView, canShow: () => Boolean(state?.hasPageView) }, - markup: { key: "markup", el: els.paneMarkup, canShow: () => Boolean(state) }, - reading: { key: "reading", el: els.paneReading, canShow: () => Boolean(state) }, - }; - return defs[key]; -} - -function isPaneAvailable(key) { - const def = paneDef(key); - return def?.canShow() ?? false; -} - -function isPaneVisible(key) { - if (!isPaneAvailable(key)) return false; - return Boolean(userPaneVisible[key]); -} - -function visiblePaneKeys() { - return PANE_KEYS.filter((key) => isPaneVisible(key)); -} - -function paneMinRatio(key) { - return PANE_MIN_RATIO; -} - -function filePaneFitWidthPx() { - const probe = document.createElement("div"); - probe.style.cssText = "position:absolute;visibility:hidden;width:var(--file-pane-fit-width);"; - document.documentElement.appendChild(probe); - const px = probe.getBoundingClientRect().width; - probe.remove(); - return Math.ceil(px) || 108; -} - -function resolvedFilePaneWidthPx() { - const fit = filePaneFitWidthPx(); - return Math.max(fit, filePaneWidthPx ?? fit); -} - -function contentPaneFrWeights(keys) { - const contentKeys = keys.filter((key) => key !== "file"); - const weights = contentKeys.map((key) => paneRatios[paneRatioIndex(key)]); - const sum = weights.reduce((a, b) => a + b, 0) || contentKeys.length; - return weights.map((weight) => weight / sum); -} - -function paneRatioIndex(key) { - return PANE_KEYS.indexOf(key); -} - -function normalizePaneRatios() { - const sum = paneRatios.reduce((a, b) => a + b, 0); - if (sum <= 0) { - paneRatios = [...DEFAULT_PANE_RATIOS]; - return; - } - paneRatios = paneRatios.map((r) => r / sum); -} - -function visiblePaneRatios(keys) { - const weights = keys.map((key) => paneRatios[paneRatioIndex(key)]); - const sum = weights.reduce((a, b) => a + b, 0) || keys.length; - return weights.map((w) => w / sum); -} - -function loadLayoutPrefs() { - try { - const raw = localStorage.getItem(LAYOUT_STORAGE_KEY); - if (!raw) return; - const data = JSON.parse(raw); - if (data?.visible && typeof data.visible === "object") { - for (const key of PANE_KEYS) { - if (typeof data.visible[key] === "boolean") userPaneVisible[key] = data.visible[key]; - } - if (typeof data.visible.file === "boolean") filePaneUserToggled = true; - } - if (Array.isArray(data?.ratios)) { - const valid = data.ratios.every((n) => typeof n === "number" && n > 0); - if (valid && data.ratios.length === 4) { - paneRatios = [...data.ratios]; - normalizePaneRatios(); - } else if (valid && data.ratios.length === 3) { - paneRatios = [1, ...data.ratios]; - normalizePaneRatios(); - } - } - if (typeof data?.filePaneWidthPx === "number" && data.filePaneWidthPx > 0) { - filePaneWidthPx = data.filePaneWidthPx; - } - } catch { - /* ignore invalid stored layout */ - } -} - -function saveLayoutPrefs() { - try { - localStorage.setItem( - LAYOUT_STORAGE_KEY, - JSON.stringify({ visible: userPaneVisible, ratios: paneRatios, filePaneWidthPx }), - ); - } catch { - /* ignore quota / private mode */ - } -} - -function resetPaneLayout() { - filePaneUserToggled = false; - userPaneVisible = { - file: defaultFilePaneVisible(), - page: true, - markup: true, - reading: true, - }; - paneRatios = [...DEFAULT_PANE_RATIOS]; - normalizePaneRatios(); - filePaneWidthPx = null; - setReadingSettingsOpen(false); - syncPagePaneControls(); - syncToolbarPaneCheckboxes(); - saveLayoutPrefs(); - applyPaneLayout(); -} - -function isLayoutStacked() { - return Boolean(layoutStackQuery?.matches); -} - -function initLayoutStackListener() { - if (!els.main) return; - layoutStackQuery = window.matchMedia(`(max-width: ${LAYOUT_STACK_BREAKPOINT_PX}px)`); - const onChange = () => applyPaneLayout(); - layoutStackQuery.addEventListener("change", onChange); - onChange(); -} - -function setToolbarOptionsOpen(open) { - toolbarOptionsOpen = open; - if (els.toolbarOptionsPanel) els.toolbarOptionsPanel.hidden = !open; - if (els.toolbarOptionsBtn) els.toolbarOptionsBtn.setAttribute("aria-expanded", String(open)); -} - -function syncToolbarPaneCheckboxes() { - if (els.toggleFilePane) { - const available = isPaneAvailable("file"); - els.toggleFilePane.checked = available && userPaneVisible.file; - els.toggleFilePane.disabled = !available; - } - if (els.toggleFilePaneLabel) { - els.toggleFilePaneLabel.classList.toggle("toolbar-options-item-disabled", !isPaneAvailable("file")); - } - if (els.togglePagePane) { - const available = isPaneAvailable("page"); - els.togglePagePane.checked = available && userPaneVisible.page; - els.togglePagePane.disabled = !available; - } - if (els.togglePagePaneLabel) { - els.togglePagePaneLabel.classList.toggle("toolbar-options-item-disabled", !isPaneAvailable("page")); - } - if (els.toggleMarkupPane) { - els.toggleMarkupPane.checked = userPaneVisible.markup; - els.toggleMarkupPane.disabled = !state; - } - if (els.toggleReadingPane) { - els.toggleReadingPane.checked = userPaneVisible.reading; - els.toggleReadingPane.disabled = !state; - } - for (const label of [ - els.toggleFilePaneLabel, - els.togglePagePaneLabel, - document.getElementById("toggle-markup-pane-label"), - document.getElementById("toggle-reading-pane-label"), - ]) { - if (!label) continue; - const input = label.querySelector("input"); - label.classList.toggle("toolbar-options-item-disabled", Boolean(input?.disabled)); - } - if (els.resetPaneLayoutBtn) els.resetPaneLayoutBtn.disabled = !state; -} - -function paneKeysAdjacent(leftKey, rightKey) { - const leftIdx = PANE_KEYS.indexOf(leftKey); - const rightIdx = PANE_KEYS.indexOf(rightKey); - return leftIdx >= 0 && rightIdx === leftIdx + 1; -} - -function onlyHiddenPanesBetween(leftKey, rightKey) { - const leftIdx = PANE_KEYS.indexOf(leftKey); - const rightIdx = PANE_KEYS.indexOf(rightKey); - if (leftIdx < 0 || rightIdx <= leftIdx) return false; - for (let i = leftIdx + 1; i < rightIdx; i++) { - if (isPaneVisible(PANE_KEYS[i])) return false; - } - return true; -} - -function shouldShowSplitterBetween(leftKey, rightKey) { - if (!isPaneVisible(leftKey) || !isPaneVisible(rightKey)) return false; - if (paneKeysAdjacent(leftKey, rightKey)) return true; - return onlyHiddenPanesBetween(leftKey, rightKey); -} - -function splitterForLayoutGap(leftKey, rightKey) { - if (!shouldShowSplitterBetween(leftKey, rightKey)) return null; - const leftIdx = PANE_KEYS.indexOf(leftKey); - if (leftIdx < 0) return null; - return els.splitters[leftIdx] ?? null; -} - -function visiblePaneNeighborAfter(key) { - const idx = PANE_KEYS.indexOf(key); - if (idx < 0) return null; - for (let i = idx + 1; i < PANE_KEYS.length; i++) { - const neighbor = PANE_KEYS[i]; - if (isPaneVisible(neighbor)) return neighbor; - } - return null; -} - -function visiblePaneNeighborBefore(key) { - const idx = PANE_KEYS.indexOf(key); - if (idx < 0) return null; - for (let i = idx - 1; i >= 0; i--) { - const neighbor = PANE_KEYS[i]; - if (isPaneVisible(neighbor)) return neighbor; - } - return null; -} - -function resolvedPhysicalSplitterKeys(physicalSplitterIndex) { - const leftPhysical = PANE_KEYS[physicalSplitterIndex]; - const rightPhysical = PANE_KEYS[physicalSplitterIndex + 1]; - if (!leftPhysical || !rightPhysical) return null; - - const leftKey = isPaneVisible(leftPhysical) ? leftPhysical : visiblePaneNeighborBefore(rightPhysical); - const rightKey = isPaneVisible(rightPhysical) ? rightPhysical : visiblePaneNeighborAfter(leftPhysical); - if (!leftKey || !rightKey || leftKey === rightKey) return null; - if (!shouldShowSplitterBetween(leftKey, rightKey)) return null; - - const canonical = splitterForLayoutGap(leftKey, rightKey); - if (!canonical || canonical !== els.splitters[physicalSplitterIndex]) return null; - return { leftKey, rightKey }; -} - -function resetPaneGridStyles() { - for (const key of PANE_KEYS) { - const el = paneDef(key)?.el; - if (el) { - el.style.gridColumn = ""; - el.style.gridRow = ""; - } - } - for (const splitter of els.splitters) { - if (!splitter) continue; - splitter.style.gridColumn = ""; - splitter.style.gridRow = ""; - splitter.hidden = true; - } -} - -function setUserPaneVisible(key, visible) { - userPaneVisible[key] = visible; - if (key === "file") filePaneUserToggled = true; - if (key === "page") syncPagePaneControls(); - if (key === "reading" && !visible) setReadingSettingsOpen(false); - syncToolbarPaneCheckboxes(); - saveLayoutPrefs(); - applyPaneLayout(); -} - -function applyPaneLayout() { - if (!els.main) return; - - const stacked = document.body.classList.contains("viewer-loaded") && isLayoutStacked(); - els.main.classList.toggle("layout-stacked", stacked); - - for (const key of PANE_KEYS) { - const def = paneDef(key); - if (!def?.el) continue; - def.el.hidden = !isPaneVisible(key); - def.el.classList.remove("pane-layout-last"); - } - - if (!document.body.classList.contains("viewer-loaded")) { - resetPaneGridStyles(); - els.main.style.gridTemplateColumns = ""; - els.main.style.gridTemplateRows = ""; - return; - } - - let keys = visiblePaneKeys(); - if (!keys.length) { - userPaneVisible.markup = true; - keys = visiblePaneKeys(); - } - - const lastKey = keys[keys.length - 1]; - paneDef(lastKey)?.el?.classList.add("pane-layout-last"); - - resetPaneGridStyles(); - - if (stacked) { - els.main.style.gridTemplateRows = ""; - els.main.style.gridTemplateColumns = "1fr"; - let row = 1; - for (const key of keys) { - const def = paneDef(key); - if (!def?.el) continue; - def.el.style.gridRow = String(row++); - } - refreshPageViewLayout(); - if (els.readingSettingsToggle) { - els.readingSettingsToggle.hidden = !state || !isPaneVisible("reading"); - } - return; - } - - const contentFr = contentPaneFrWeights(keys); - const cols = []; - let contentFrIndex = 0; - keys.forEach((key, index) => { - if (key === "file") { - cols.push(`${resolvedFilePaneWidthPx()}px`); - } else { - cols.push(`minmax(0, ${contentFr[contentFrIndex++].toFixed(6)}fr)`); - } - if (index < keys.length - 1 && shouldShowSplitterBetween(keys[index], keys[index + 1])) { - cols.push("1px"); - } - }); - els.main.style.gridTemplateColumns = cols.join(" "); - els.main.style.gridTemplateRows = "minmax(0, 1fr)"; - - let col = 1; - keys.forEach((key, index) => { - const def = paneDef(key); - if (!def?.el) return; - def.el.style.gridColumn = String(col); - col += 1; - if (index < keys.length - 1) { - const leftKey = keys[index]; - const rightKey = keys[index + 1]; - if (!shouldShowSplitterBetween(leftKey, rightKey)) return; - const splitter = splitterForLayoutGap(leftKey, rightKey); - if (splitter) { - splitter.hidden = false; - splitter.style.gridColumn = String(col); - col += 1; - } - } - }); - - refreshPageViewLayout(); - if (els.readingSettingsToggle) { - els.readingSettingsToggle.hidden = !state || !isPaneVisible("reading"); - } -} - -function initToolbarOptions() { - if (!els.toolbarOptionsBtn || !els.toolbarOptionsPanel) return; - - els.toolbarOptionsBtn.addEventListener("click", (e) => { - e.stopPropagation(); - setToolbarOptionsOpen(!toolbarOptionsOpen); - }); - - document.addEventListener("click", (e) => { - if (!toolbarOptionsOpen) return; - if (e.target instanceof Node && els.toolbarOptionsPanel.contains(e.target)) return; - if (e.target instanceof Node && els.toolbarOptionsBtn.contains(e.target)) return; - setToolbarOptionsOpen(false); - }); - - const onToggle = (key, input) => { - input?.addEventListener("change", () => { - if (!state) return; - const nextKeys = PANE_KEYS.filter((k) => (k === key ? input.checked : userPaneVisible[k] && isPaneAvailable(k))); - if (!nextKeys.length) { - input.checked = true; - return; - } - setUserPaneVisible(key, input.checked); - }); - }; - - onToggle("file", els.toggleFilePane); - onToggle("page", els.togglePagePane); - onToggle("markup", els.toggleMarkupPane); - onToggle("reading", els.toggleReadingPane); - els.resetPaneLayoutBtn?.addEventListener("click", () => { - if (!state) return; - resetPaneLayout(); - }); - syncToolbarPaneCheckboxes(); -} - -function initPaneSplitters() { - for (const [index, splitter] of els.splitters.entries()) { - if (!splitter) continue; - splitter.addEventListener("pointerdown", (e) => startPaneDrag(e, index)); - } - window.addEventListener("pointermove", onPaneDragMove); - window.addEventListener("pointerup", endPaneDrag); - window.addEventListener("pointercancel", endPaneDrag); -} - -function startPaneDrag(e, physicalSplitterIndex) { - if (e.button !== 0 || isLayoutStacked() || !document.body.classList.contains("viewer-loaded")) return; - - const resolved = resolvedPhysicalSplitterKeys(physicalSplitterIndex); - if (!resolved) return; - - const { leftKey, rightKey } = resolved; - - normalizePaneRatios(); - const leftIndex = paneRatioIndex(leftKey); - const rightIndex = paneRatioIndex(rightKey); - const dragState = { - physicalSplitterIndex, - leftKey, - rightKey, - startX: e.clientX, - leftStart: paneRatios[leftIndex], - rightStart: paneRatios[rightIndex], - pointerId: e.pointerId, - }; - if (leftKey === "file") { - dragState.leftStartPx = resolvedFilePaneWidthPx(); - } else if (rightKey === "file") { - dragState.rightStartPx = resolvedFilePaneWidthPx(); - } - paneDrag = dragState; - - e.preventDefault(); - e.currentTarget.setPointerCapture(e.pointerId); - e.currentTarget.classList.add("is-dragging"); - document.body.classList.add("pane-drag-active"); -} - -function contentPaneAvailableWidthPx() { - if (!els.main) return 1; - const rect = els.main.getBoundingClientRect(); - const keys = visiblePaneKeys(); - let reserved = 0; - if (keys.includes("file")) reserved += resolvedFilePaneWidthPx(); - for (let i = 0; i < keys.length - 1; i++) { - if (shouldShowSplitterBetween(keys[i], keys[i + 1])) reserved += 1; - } - return Math.max(rect.width - reserved, 1); -} - -function onPaneDragMove(e) { - if (!paneDrag || e.pointerId !== paneDrag.pointerId || !els.main) return; - - if (paneDrag.leftKey === "file" && typeof paneDrag.leftStartPx === "number") { - filePaneWidthPx = Math.max(filePaneFitWidthPx(), paneDrag.leftStartPx + (e.clientX - paneDrag.startX)); - applyPaneLayout(); - return; - } - if (paneDrag.rightKey === "file" && typeof paneDrag.rightStartPx === "number") { - filePaneWidthPx = Math.max(filePaneFitWidthPx(), paneDrag.rightStartPx - (e.clientX - paneDrag.startX)); - applyPaneLayout(); - return; - } - - const keys = visiblePaneKeys(); - const contentFr = contentPaneFrWeights(keys); - const contentKeys = keys.filter((key) => key !== "file"); - const leftContentIndex = contentKeys.indexOf(paneDrag.leftKey); - if (leftContentIndex < 0 || leftContentIndex + 1 >= contentFr.length) return; - - const pairFrTotal = contentFr[leftContentIndex] + contentFr[leftContentIndex + 1]; - if (!(pairFrTotal > 0)) return; - - const pairPixels = Math.max(contentPaneAvailableWidthPx() * pairFrTotal, 1); - const deltaRatio = (e.clientX - paneDrag.startX) / pairPixels; - - const leftIndex = paneRatioIndex(paneDrag.leftKey); - const rightIndex = paneRatioIndex(paneDrag.rightKey); - const pairStoredTotal = paneDrag.leftStart + paneDrag.rightStart; - if (!(pairStoredTotal > 0) || leftIndex < 0 || rightIndex < 0) return; - - let nextLeft = paneDrag.leftStart + deltaRatio * pairStoredTotal; - const leftMin = Math.min(paneMinRatio(paneDrag.leftKey), pairStoredTotal / 2); - const rightMin = Math.min(paneMinRatio(paneDrag.rightKey), pairStoredTotal / 2); - nextLeft = Math.min(Math.max(nextLeft, leftMin), pairStoredTotal - rightMin); - paneRatios[leftIndex] = nextLeft; - paneRatios[rightIndex] = pairStoredTotal - nextLeft; - applyPaneLayout(); -} - -function endPaneDrag(e) { - if (!paneDrag || e.pointerId !== paneDrag.pointerId) return; - const splitter = els.splitters[paneDrag.physicalSplitterIndex]; - splitter?.classList.remove("is-dragging"); - if (splitter?.hasPointerCapture(e.pointerId)) splitter.releasePointerCapture(e.pointerId); - paneDrag = null; - document.body.classList.remove("pane-drag-active"); - normalizePaneRatios(); - saveLayoutPrefs(); -} - -function goToPage(n) { - if (!state) return; - setPageSettingsOpen(false); - const page = Math.min(Math.max(1, n), state.pageCount); - state.currentPage = page; - renderPage(page); -} - -function resetPageNumberInput() { - if (!els.pageNumberInput) return; - els.pageNumberInput.value = state ? String(state.currentPage) : "1"; -} - -function commitPageNumberInput() { - if (!state || !els.pageNumberInput) return; - const n = Number.parseInt(els.pageNumberInput.value.trim(), 10); - if (!Number.isFinite(n)) { - resetPageNumberInput(); - return; - } - goToPage(n); -} - -function setPageIndicator(pageNum, pageCount) { - if (!els.pageIndicator) return; - if (els.pageCountIndicator) els.pageCountIndicator.textContent = `\u00A0of ${pageCount}`; - if (els.pageNumberInput) { - const digits = Math.max(1, String(pageCount).length); - els.pageNumberInput.style.setProperty("--page-num-digits", String(digits)); - els.pageNumberInput.disabled = !state; - if (document.activeElement !== els.pageNumberInput) { - els.pageNumberInput.value = String(pageNum); - } - } -} - -function renderPage(pageNum) { - if (!state) return; - const { segments, pageImages, pageCount, defaultResolution } = state; - const idx = pageNum - 1; - const segment = segments[idx] ?? []; - selectedElementId = null; - - setPageIndicator(pageNum, pageCount); - if (els.btnPrev) els.btnPrev.disabled = pageNum <= 1; - if (els.btnNext) els.btnNext.disabled = pageNum >= pageCount; - - if (!els.markupPane || !els.renderedPane || !els.pagePane) return; - - els.markupPane.innerHTML = ""; - const elementIds = assignElementIds(segment); - state.elementIds = elementIds; - state.idToElement = invertElementIds(elementIds); - - if (segmentHasMarkup(segment)) { - els.markupPane.appendChild(buildMarkupView(segment, elementIds)); - els.renderedPane.innerHTML = ""; - els.renderedPane.appendChild(buildRenderedView(segment, elementIds)); - } else { - els.markupPane.innerHTML = `
${NO_MARKUP}
`; - els.renderedPane.innerHTML = `
${NO_MARKUP}
`; - } - - els.pagePane.innerHTML = ""; - if (state.hasPageView) { - const imageUrl = pageImages.get(pageNum); - if (imageUrl) { - const port = document.createElement("div"); - port.className = "page-view-port"; - const wrap = document.createElement("div"); - wrap.className = "page-view"; - const img = document.createElement("img"); - img.alt = `Page ${pageNum}`; - - const onImageReady = () => { - if (img.dataset.layoutGeneration === String(pageNum)) return; - img.dataset.layoutGeneration = String(pageNum); - - applyPageImageSize(img, els.pagePane); - - const boxes = collectBoundingBoxes(segment, defaultResolution, elementIds); - const existing = wrap.querySelector("svg.overlay"); - if (existing) existing.remove(); - const readingOrderSteps = collectReadingOrderSteps( - segment, - elementIds, - boxes, - state.readingOrder, - readingOrderGlobalNumbering, - state.readingOrderDisplayNumbers, - ); - state.pageViewOverlay = { boxes, readingOrderSteps }; - if (boxes.length) { - wrap.appendChild( - buildOverlay( - img, - boxes, - collectCaptionLinks(segment, elementIds, boxes), - collectXrefLinks(segment, elementIds, boxes), - readingOrderSteps, - collectFragmentLinks(segment, elementIds, boxes, pageNum, state.threadPagesById), - collectFragmentNavItems(segment, elementIds, boxes), - defaultResolution, - ), - ); - } - syncPageViewChrome(img); - const pending = state.pendingSelectElement; - if (pending) { - state.pendingSelectElement = null; - const id = findElementIdOnPage(pending); - if (id) selectElement(id); - } - }; - - img.addEventListener("load", onImageReady, { once: true }); - wrap.appendChild(img); - port.appendChild(wrap); - els.pagePane.appendChild(port); - img.src = imageUrl; - if (img.complete) onImageReady(); - - if (!pagePaneResizeObserver) { - pagePaneResizeObserver = new ResizeObserver(() => { - refreshPageViewLayout(); - }); - pagePaneResizeObserver.observe(els.pagePane); - } - } else { - els.pagePane.innerHTML = `
${NO_IMAGE}
`; - } - } -} - -function pageZoomFactor() { - return pageZoomPercent / PAGE_ZOOM_DEFAULT; -} - -/** - * Convert a legacy overlay length (image pixels as tuned on the demo pages) to SVG user - * units for the current page. Matches the old demo on-screen size, stays zoom-stable, - * and scales with page resolution so other images look the same. - */ -function overlayUserLength(baseUserPx, img = els.pagePane?.querySelector(".page-view img")) { - const zoom = pageZoomFactor(); - if (!(zoom > 0)) return baseUserPx; - if (!img?.naturalWidth || !img.naturalHeight || !els.pagePane) { - return baseUserPx / zoom; - } - - const { w: paneW, h: paneH } = paneContentSize(els.pagePane); - if (!(paneW > 0 && paneH > 0)) return baseUserPx / zoom; - - const refFit = Math.min( - (paneW - PAGE_VIEW_BORDER_PX) / OVERLAY_REF_IMAGE_WIDTH, - (paneH - PAGE_VIEW_BORDER_PX) / OVERLAY_REF_IMAGE_HEIGHT, - ); - const fitScale = getCachedFitScale(img, els.pagePane); - if (!(refFit > 0) || !(fitScale > 0)) return baseUserPx / zoom; - - return (baseUserPx * refFit) / (fitScale * zoom); -} - -function computePageFitScale(img, pane) { - return getCachedFitScale(img, pane); -} - -function resetPageLayoutCache() { - pageLayoutCache = null; -} - -function paneContentSize(pane) { - const style = getComputedStyle(pane); - const padX = parseFloat(style.paddingLeft) + parseFloat(style.paddingRight); - const padY = parseFloat(style.paddingTop) + parseFloat(style.paddingBottom); - return { - w: pane.clientWidth - padX, - h: pane.clientHeight - padY, - }; -} - -function getCachedFitScale(img, pane) { - const { w: paneW, h: paneH } = paneContentSize(pane); - const imgW = img.naturalWidth; - const imgH = img.naturalHeight; - if ( - pageLayoutCache && - pageLayoutCache.paneW === paneW && - pageLayoutCache.paneH === paneH && - pageLayoutCache.imgW === imgW && - pageLayoutCache.imgH === imgH - ) { - return pageLayoutCache.fitScale; - } - const fitScale = - paneW > 0 && paneH > 0 && imgW > 0 && imgH > 0 - ? Math.min( - (paneW - PAGE_VIEW_BORDER_PX) / imgW, - (paneH - PAGE_VIEW_BORDER_PX) / imgH, - ) - : 1; - pageLayoutCache = { paneW, paneH, imgW, imgH, fitScale }; - return fitScale; -} - -function applyPageImageSize(img, pane = els.pagePane) { - if (!img?.naturalWidth || !img.naturalHeight) return false; - pageZoomPercent = Math.max(PAGE_ZOOM_DEFAULT, pageZoomPercent); - const fitScale = getCachedFitScale(img, pane); - const scale = fitScale * (pageZoomPercent / PAGE_ZOOM_DEFAULT); - const w = Math.floor(img.naturalWidth * scale); - const h = Math.floor(img.naturalHeight * scale); - const nextW = `${w}px`; - const nextH = `${h}px`; - const unchanged = img.style.width === nextW && img.style.height === nextH; - if (!unchanged) { - img.style.width = nextW; - img.style.height = nextH; - img.style.maxWidth = "none"; - img.style.maxHeight = "none"; - } - img.dataset.layoutReady = "1"; - return !unchanged; -} - -function syncPageViewChrome(img) { - syncOverlayBadges(img); - updatePagePanePanCursor(); - applyBboxVisibility(); -} - -function applyPageLayout(img, pane = els.pagePane) { - applyPageImageSize(img, pane); - syncPageViewChrome(img); -} - -let pageLayoutFrame = 0; - -function refreshPageViewLayout() { - if (!els.pagePane) return; - cancelAnimationFrame(pageLayoutFrame); - pageLayoutFrame = requestAnimationFrame(() => { - pageLayoutFrame = 0; - const img = els.pagePane?.querySelector(".page-view img"); - if (img?.naturalWidth) applyPageLayout(img); - }); -} - -function overlayBadgeLayout(svg, text, fontSizeUser) { - const padXUser = overlayUserLength(OVERLAY_BADGE_PAD_X); - const padYUser = overlayUserLength(OVERLAY_BADGE_PAD_Y); - const probe = document.createElementNS("http://www.w3.org/2000/svg", "text"); - probe.setAttribute("class", "overlay-badge-label"); - probe.setAttribute("font-size", String(fontSizeUser)); - probe.setAttribute("font-weight", "700"); - probe.setAttribute("text-anchor", "start"); - probe.setAttribute("dominant-baseline", "text-before-edge"); - probe.setAttribute("visibility", "hidden"); - probe.textContent = String(text); - svg.appendChild(probe); - - let width; - let height; - try { - const bbox = probe.getBBox(); - width = bbox.width + padXUser * 2; - height = bbox.height + padYUser * 2; - } catch { - const label = String(text); - width = overlayUserLength(label.length * OVERLAY_BADGE_FONT_SIZE * 0.55 + OVERLAY_BADGE_PAD_X * 2); - height = overlayUserLength(OVERLAY_BADGE_FONT_SIZE * 1.1 + OVERLAY_BADGE_PAD_Y * 2); - } - probe.remove(); - return { width, height }; -} - -function appendOverlayBadge(svg, anchorX, anchorY, text, { extraClass, elementId }) { - const fontSize = overlayUserLength(OVERLAY_BADGE_FONT_SIZE); - const { width, height } = overlayBadgeLayout(svg, text, fontSize); - const radius = overlayUserLength(OVERLAY_BADGE_RADIUS_SCREEN_PX); - - const badge = document.createElementNS("http://www.w3.org/2000/svg", "g"); - badge.setAttribute("class", `overlay-badge ${extraClass}`); - badge.setAttribute("data-element-id", elementId); - - const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - bg.setAttribute("class", "overlay-badge-bg"); - bg.setAttribute("x", String(anchorX - width / 2)); - bg.setAttribute("y", String(anchorY - height / 2)); - bg.setAttribute("width", String(width)); - bg.setAttribute("height", String(height)); - bg.setAttribute("rx", String(radius)); - bg.setAttribute("ry", String(radius)); - badge.appendChild(bg); - - const label = document.createElementNS("http://www.w3.org/2000/svg", "text"); - label.setAttribute("class", "overlay-badge-label"); - label.setAttribute("x", String(anchorX)); - label.setAttribute("y", String(anchorY)); - label.setAttribute("font-size", String(fontSize)); - label.textContent = String(text); - badge.appendChild(label); - - svg.appendChild(badge); - return badge; -} - -function syncOverlayBadges(img) { - const svg = img.parentElement?.querySelector("svg.overlay"); - const meta = state?.pageViewOverlay; - if (!svg || !img.naturalWidth || !meta) return; - - svg.querySelectorAll(".element-badge, .reading-order-badge").forEach((badge) => badge.remove()); - - const fontSize = overlayUserLength(OVERLAY_BADGE_FONT_SIZE); - const badgeGap = overlayUserLength(2); - const readingOrderByElementId = new Map(); - if (showAllBboxes && showReadingOrder) { - for (const step of meta.readingOrderSteps ?? []) { - readingOrderByElementId.set(step.elementId, step); - } - } - - const boxes = sortedOverlayBoxes(meta.boxes ?? []); - - for (const b of boxes) { - const { x, y } = boxPixelRect(b, img); - let tagLayout = { width: 0 }; - if (showAllBboxes && showLayoutBadges) { - tagLayout = overlayBadgeLayout(svg, b.tag, fontSize); - appendOverlayBadge(svg, x, y, b.tag, { - extraClass: `element-badge ${kindClassForTag(b.kind)}`, - elementId: b.elementId, - }); - } - - const step = readingOrderByElementId.get(b.elementId); - if (step) { - const orderText = String(step.order); - const orderLayout = overlayBadgeLayout(svg, orderText, fontSize); - const orderAnchorX = showAllBboxes && showLayoutBadges - ? x + tagLayout.width / 2 + badgeGap + orderLayout.width / 2 - : x; - appendOverlayBadge(svg, orderAnchorX, y, orderText, { - extraClass: "reading-order-badge", - elementId: b.elementId, - }); - } - } -} - -function splitIntoSegments(root) { - const body = childElements(root).filter((el) => localName(el) !== "head"); - /** @type {Element[][]} */ - const segments = [[]]; - for (const el of body) { - if (localName(el) === "page_break") { - segments.push([]); - } else { - segments[segments.length - 1].push(el); - } - } - return segments.length ? segments : [[]]; -} - -function segmentHasMarkup(segment) { - return segment.some((el) => el.nodeType === Node.ELEMENT_NODE); -} - -function readDefaultResolution(head) { - const fallback = { width: 512, height: 512 }; - if (!head) return fallback; - const dr = childElements(head).find((el) => localName(el) === "default_resolution"); - if (!dr) return fallback; - const w = parseInt(dr.getAttribute("width") ?? "512", 10); - const h = parseInt(dr.getAttribute("height") ?? "512", 10); - return { - width: Number.isFinite(w) && w > 0 ? w : 512, - height: Number.isFinite(h) && h > 0 ? h : 512, - }; -} - -/** @returns {Map} */ -function assignElementIds(segment) { - const ids = new Map(); - let counter = 0; - walkElements(segment, (el) => { - ids.set(el, `el-${counter++}`); - }); - return ids; -} - -/** @returns {Map} */ -function invertElementIds(elementIds) { - const idToElement = new Map(); - for (const [el, id] of elementIds) idToElement.set(id, el); - return idToElement; -} - -function isSemanticElement(el) { - return SEMANTIC_TAGS.has(localName(el)); -} - -function isVirtualTextHost(el) { - const tag = localName(el); - return tag === "ldiv" || CELL_CONTENT_TAGS.has(tag); -} - -function xmlContains(target, ancestor) { - let node = target; - while (node) { - if (node === ancestor) return true; - node = node.parentNode; - } - return false; -} - -function findListVirtualTextHost(list, target) { - const nodes = [...list.childNodes]; - let i = skipContainerLevelHead(nodes, 0); - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType !== Node.ELEMENT_NODE || localName(node) !== "ldiv") { - i += 1; - continue; - } - const ldiv = node; - i += 1; - const end = skipUntilListItemBoundary(nodes, i); - if (target === ldiv || nodes.slice(i, end).some((n) => xmlContains(target, n))) { - return ldiv; - } - i = end; - } - return null; -} - -function findTableVirtualTextHost(container, target) { - const nodes = [...container.childNodes]; - let i = skipContainerLevelHead(nodes, 0); - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType !== Node.ELEMENT_NODE) { - i += 1; - continue; - } - const tag = localName(node); - if (tag === "nl" || CELL_SPAN_TAGS.has(tag) || !isCellToken(tag)) { - i += 1; - continue; - } - const cell = node; - i += 1; - const end = skipUntilCellBoundary(nodes, i); - if (target === cell || nodes.slice(i, end).some((n) => xmlContains(target, n))) { - return cell; - } - i = end; - } - return null; -} - -function isListOrOtslContainer(el) { - const tag = localName(el); - return tag === "list" || OTSL_CONTAINER_TAGS.has(tag); -} - -function findVirtualTextHost(xmlEl) { - let node = xmlEl; - while (node) { - const parent = node.parentElement; - if (!parent) return null; - const tag = localName(parent); - if (tag === "list") { - const host = findListVirtualTextHost(parent, xmlEl); - if (host) return host; - } - if (OTSL_CONTAINER_TAGS.has(tag)) { - const host = findTableVirtualTextHost(parent, xmlEl); - if (host) return host; - } - node = parent; - } - return null; -} - -function resolveSelectionElement(xmlEl) { - if (!xmlEl) return null; - if (isSemanticElement(xmlEl) || isVirtualTextHost(xmlEl)) return xmlEl; - - let node = xmlEl.parentElement; - while (node) { - const tag = localName(node); - if (tag === "doclang") break; - if (isSemanticElement(node) && !isListOrOtslContainer(node)) return node; - node = node.parentElement; - } - - const virtualHost = findVirtualTextHost(xmlEl); - if (virtualHost) return virtualHost; - - node = xmlEl.parentElement; - while (node) { - const tag = localName(node); - if (tag === "doclang") break; - if (isSemanticElement(node) || isVirtualTextHost(node)) return node; - node = node.parentElement; - } - - return null; -} - -function resolveSelectionElementId(rawElementId) { - if (!rawElementId || !state?.idToElement || !state.elementIds) return null; - const xmlEl = state.idToElement.get(rawElementId); - if (!xmlEl) return null; - const resolved = resolveSelectionElement(xmlEl); - return resolved ? state.elementIds.get(resolved) ?? null : null; -} - -function resolveMarkupClickTarget(eventTarget) { - if (!state) return null; - - const ghostText = eventTarget.closest(".markup-el-virtual-text"); - if (ghostText?.hasAttribute("data-element-id")) { - return ghostText.getAttribute("data-element-id"); - } - - const markupEl = eventTarget.closest(".markup-el[data-element-id]"); - if (!markupEl) return null; - return resolveSelectionElementId(markupEl.getAttribute("data-element-id")); -} - -function resolveRenderedClickTarget(eventTarget) { - const ghostText = eventTarget.closest(".rendered-el-virtual-text"); - if (ghostText?.hasAttribute("data-element-id")) { - return ghostText.getAttribute("data-element-id"); - } - - const renderedEl = eventTarget.closest(".rendered-el[data-element-id]"); - if (!renderedEl) return null; - return resolveSelectionElementId(renderedEl.getAttribute("data-element-id")); -} - -/** @returns {{ kind: string, tag: string, elementId: string, layer: string, x0: number, y0: number, x1: number, y1: number, resW: number, resH: number }[]} */ -function collectBoundingBoxes(segment, defaultResolution, elementIds) { - /** @type {{ kind: string, tag: string, elementId: string, layer: string, x0: number, y0: number, x1: number, y1: number, resW: number, resH: number }[]} */ - const boxes = []; - walkElements(segment, (el) => { - const locs = headLocations(el); - if (locs.length !== 4) return; - const elementId = elementIds.get(el); - if (!elementId) return; - pushBoundingBox( - boxes, - locs, - defaultResolution, - localName(el), - elementLabel(el), - elementId, - elementLayer(el), - ); - }); - walkElements(segment, (el) => { - const tag = localName(el); - if (tag === "list") collectListVirtualTextBoxes(el, defaultResolution, boxes, elementIds); - else if (tag === "table" || tag === "index" || tag === "tabular") { - collectTableVirtualTextBoxes(el, defaultResolution, boxes, elementIds); - } - }); - return boxes; -} - -/** @returns {{ captionBox: object, hostBox: object, captionElementId: string, hostElementId: string }[]} */ -function collectCaptionLinks(segment, elementIds, boxes) { - const boxById = new Map(boxes.map((b) => [b.elementId, b])); - /** @type {{ captionBox: object, hostBox: object, captionElementId: string, hostElementId: string }[]} */ - const links = []; - walkElements(segment, (el) => { - if (localName(el) !== "caption") return; - const captionId = elementIds.get(el); - const captionBox = captionId ? boxById.get(captionId) : null; - if (!captionBox) return; - const host = el.parentElement; - if (!host || headLocations(host).length !== 4) return; - const hostId = elementIds.get(host); - const hostBox = hostId ? boxById.get(hostId) : null; - if (!hostBox) return; - links.push({ - captionBox, - hostBox, - captionElementId: captionId, - hostElementId: hostId, - }); - }); - return links; -} - -/** @returns {{ fromBox: object, toBox: object, fromElementId: string, toElementId: string }[]} */ -function collectXrefLinks(segment, elementIds, boxes) { - const boxById = new Map(boxes.map((b) => [b.elementId, b])); - /** @type {Map} */ - const threadsById = new Map(); - - walkElements(segment, (el) => { - const elementId = elementIds.get(el); - const box = elementId ? boxById.get(elementId) : null; - if (!box) return; - for (const thread of childElements(el)) { - if (localName(thread) !== "thread") continue; - const threadId = thread.getAttribute("thread_id"); - if (!threadId) continue; - if (!threadsById.has(threadId)) threadsById.set(threadId, []); - threadsById.get(threadId).push({ elementId, box }); - } - }); - - /** @type {{ fromBox: object, toBox: object, fromElementId: string, toElementId: string }[]} */ - const links = []; - walkElements(segment, (el) => { - const xrefs = childElements(el).filter((c) => localName(c) === "xref"); - if (!xrefs.length) return; - - const from = findNearestLocatedBox(el, elementIds, boxById); - if (!from) return; - - for (const xref of xrefs) { - const threadId = xref.getAttribute("thread_id"); - if (!threadId) continue; - for (const { elementId: toId, box: toBox } of threadsById.get(threadId) ?? []) { - if (toId === from.elementId) continue; - links.push({ - fromBox: from.box, - toBox, - fromElementId: from.elementId, - toElementId: toId, - }); - } - } - }); - return links; -} - -/** @returns {{ fromBox: object, toBox: object | null, fromElementId: string, toElementId: string | null, threadId: string, targetCorner?: "tl" | "br" }[]} */ -function collectFragmentLinks(segment, elementIds, boxes, pageNum, threadPagesById) { - const boxById = new Map(boxes.map((b) => [b.elementId, b])); - /** @type {Map} */ - const onPage = new Map(); - - walkElements(segment, (el) => { - const thread = firstHeadChild(el, "thread"); - const threadId = thread?.getAttribute("thread_id"); - if (!threadId) return; - const elementId = elementIds.get(el); - const box = elementId ? boxById.get(elementId) : null; - if (!box) return; - if (!onPage.has(threadId)) onPage.set(threadId, []); - onPage.get(threadId).push({ elementId, box }); - }); - - const segmentEls = new Set(); - walkElements(segment, (el) => segmentEls.add(el)); - - /** @type {{ fromBox: object, toBox: object | null, fromElementId: string, toElementId: string | null, threadId: string, targetCorner?: "tl" | "br" }[]} */ - const links = []; - - for (const [threadId, members] of onPage) { - if (members.length >= 2) { - for (let i = 0; i < members.length - 1; i += 1) { - links.push({ - fromBox: members[i].box, - toBox: members[i + 1].box, - fromElementId: members[i].elementId, - toElementId: members[i + 1].elementId, - threadId, - }); - } - continue; - } - - if (members.length !== 1) continue; - const threadPages = threadPagesById.get(threadId); - if (!threadPages) continue; - const hasPrevious = [...threadPages].some((p) => p < pageNum); - const hasFollowing = [...threadPages].some((p) => p > pageNum); - if (!hasPrevious && !hasFollowing) continue; - - const base = { - fromBox: members[0].box, - toBox: null, - fromElementId: members[0].elementId, - toElementId: null, - threadId, - }; - if (hasPrevious) links.push({ ...base, targetCorner: "tl" }); - if (hasFollowing) links.push({ ...base, targetCorner: "br" }); - } - - return links; -} - -function isReadingOrderUnit(el) { - const tag = localName(el); - if (tag === "caption") return true; - if (HEAD_TAGS.has(tag) || tag === "location" || tag === "h_thread" || tag === "page_break") return false; - if (tag === "nl" || CELL_SPAN_TAGS.has(tag)) return false; - if (RENDER_FORMAT_TAGS.has(tag) || tag === "src" || tag === "checkbox") return false; - return true; -} - -/** @returns {Map} */ -function buildThreadsById(docRoot) { - const map = new Map(); - const roots = childElements(docRoot).filter((el) => localName(el) !== "head"); - walkElements(roots, (el) => { - for (const child of childElements(el)) { - if (localName(child) !== "thread") continue; - const threadId = child.getAttribute("thread_id"); - if (!threadId) continue; - if (!map.has(threadId)) map.set(threadId, []); - map.get(threadId).push(el); - } - }); - return map; -} - -/** @returns {Map} */ -function buildElementPageMap(segments) { - const elementPage = new Map(); - segments.forEach((segment, idx) => { - const pageNum = idx + 1; - walkElements(segment, (el) => elementPage.set(el, pageNum)); - }); - return elementPage; -} - -/** @returns {Map>} */ -function buildThreadPagesById(docRoot, elementPageByEl) { - /** @type {Map>} */ - const threadPagesById = new Map(); - for (const [threadId, elements] of buildThreadsById(docRoot)) { - const pages = new Set(); - for (const el of elements) { - const page = elementPageByEl.get(el); - if (page) pages.add(page); - } - if (pages.size) threadPagesById.set(threadId, pages); - } - return threadPagesById; -} - -/** @returns {Map} */ -function buildThreadNavByElement(docRoot) { - /** @type {Map} */ - const nav = new Map(); - for (const [, elements] of buildThreadsById(docRoot)) { - for (let i = 0; i < elements.length; i += 1) { - nav.set(elements[i], { - prev: i > 0 ? elements[i - 1] : null, - next: i < elements.length - 1 ? elements[i + 1] : null, - }); - } - } - return nav; -} - -/** @returns {{ elementId: string, box: object, hasPrev: boolean, hasNext: boolean }[]} */ -function collectFragmentNavItems(segment, elementIds, boxes) { - if (!state?.threadNavByElement) return []; - const boxById = new Map(boxes.map((b) => [b.elementId, b])); - /** @type {{ elementId: string, box: object, hasPrev: boolean, hasNext: boolean }[]} */ - const items = []; - walkElements(segment, (el) => { - const nav = state.threadNavByElement.get(el); - if (!nav || (!nav.prev && !nav.next)) return; - const elementId = elementIds.get(el); - const box = elementId ? boxById.get(elementId) : null; - if (!box) return; - items.push({ - elementId, - box, - hasPrev: nav.prev !== null, - hasNext: nav.next !== null, - }); - }); - return items; -} - -function findElementIdOnPage(el) { - if (!state?.elementIds) return null; - for (const [node, id] of state.elementIds) { - if (node === el) return id; - } - return null; -} - -function navigateThreadFragment(elementId, direction) { - const el = state?.idToElement?.get(elementId); - if (!el) return; - const nav = state?.threadNavByElement?.get(el); - if (!nav) return; - const target = direction === "prev" ? nav.prev : nav.next; - if (!target) return; - const page = state.elementPageByEl.get(target); - if (!page) return; - if (page === state.currentPage) { - const id = findElementIdOnPage(target); - if (id) selectElement(id); - return; - } - state.pendingSelectElement = target; - goToPage(page); -} - -/** @returns {Element[]} */ -function computeReadingOrder(docRoot) { - const bodyChildren = childElements(docRoot).filter((el) => localName(el) !== "head"); - const threadsById = buildThreadsById(docRoot); - const consumedViaXref = new Set(); - /** @type {Element[]} */ - const order = []; - - function record(el) { - if (!isReadingOrderUnit(el)) return; - order.push(el); - } - - function consumeThread(threadId) { - for (const target of threadsById.get(threadId) ?? []) { - if (consumedViaXref.has(target)) continue; - consumedViaXref.add(target); - visitElement(target); - } - } - - function walkChildren(parent) { - for (const child of parent.childNodes) { - if (child.nodeType !== Node.ELEMENT_NODE) continue; - const tag = localName(child); - if (tag === "xref") { - const threadId = child.getAttribute("thread_id"); - if (threadId) consumeThread(threadId); - continue; - } - if (tag === "page_break") continue; - visitElement(child); - } - } - - function visitElement(el) { - record(el); - walkChildren(el); - } - - for (const el of bodyChildren) { - if (localName(el) === "page_break") continue; - if (consumedViaXref.has(el)) continue; - visitElement(el); - } - - return order; -} - -function hasVirtualTextLocations(el) { - const parent = el.parentElement; - if (!parent) return false; - const nodes = [...parent.childNodes]; - const idx = nodes.indexOf(el); - if (idx < 0) return false; - return parseElementHeadAt(nodes, idx + 1) !== null; -} - -function isVirtualTextOverlayUnit(el) { - if (headLocations(el).length === 4) return false; - if (!hasVirtualTextLocations(el)) return false; - const tag = localName(el); - if (tag === "ldiv" && localName(el.parentElement) === "list") return true; - if (isCellToken(tag) && tag !== "nl" && !CELL_SPAN_TAGS.has(tag)) { - return OTSL_CONTAINER_TAGS.has(localName(el.parentElement)); - } - return false; -} - -function isPictureContentElement(el) { - if (!el) return false; - const tag = localName(el); - if (tag === "picture" || tag === "caption") return false; - let node = el.parentElement; - while (node) { - if (localName(node) === "picture") return true; - node = node.parentElement; - } - return false; -} - -function isTableContentElement(el) { - if (!el) return false; - const tag = localName(el); - if (OTSL_CONTAINER_TAGS.has(tag) || tag === "caption") return false; - let node = el.parentElement; - while (node) { - if (OTSL_CONTAINER_TAGS.has(localName(node))) return true; - node = node.parentElement; - } - return false; -} - -function isPictureOrTableContentElement(el) { - return isPictureContentElement(el) || isTableContentElement(el); -} - -function isReadingOrderOverlayUnit(el) { - if (!isReadingOrderUnit(el)) return false; - if (isPictureOrTableContentElement(el)) return false; - if (headLocations(el).length === 4) return true; - return isVirtualTextOverlayUnit(el); -} - -/** @returns {Map} */ -function computeReadingOrderDisplayNumbers(readingOrder) { - const numbers = new Map(); - let n = 0; - for (const el of readingOrder) { - if (!isReadingOrderOverlayUnit(el)) continue; - n += 1; - numbers.set(el, n); - } - return numbers; -} - -/** @returns {{ order: number, box: object, elementId: string }[]} */ -function collectReadingOrderSteps( - segment, - elementIds, - boxes, - readingOrder, - globalNumbering = true, - displayNumbers = null, -) { - const boxById = new Map(boxes.map((b) => [b.elementId, b])); - /** @type {{ order: number, box: object, elementId: string }[]} */ - const steps = []; - let pageOrder = 0; - - readingOrder.forEach((el) => { - if (isPictureOrTableContentElement(el)) return; - const elementId = elementIds.get(el); - if (!elementId) return; - const box = boxById.get(elementId); - if (!box) return; - pageOrder += 1; - steps.push({ - order: globalNumbering ? (displayNumbers?.get(el) ?? pageOrder) : pageOrder, - box, - elementId, - }); - }); - - return steps; -} - -function pushBoundingBox(boxes, locs, defaultResolution, kind, tag, elementId, layer = "body") { - const [x0el, y0el, x1el, y1el] = locs; - const resW = locationResolution(x0el, defaultResolution.width); - const resH = locationResolution(y0el, defaultResolution.height); - boxes.push({ - kind, - tag, - elementId, - layer, - x0: parseInt(x0el.getAttribute("value") ?? "0", 10), - y0: parseInt(y0el.getAttribute("value") ?? "0", 10), - x1: parseInt(x1el.getAttribute("value") ?? "0", 10), - y1: parseInt(y1el.getAttribute("value") ?? "0", 10), - resW, - resH, - }); -} - -function collectListVirtualTextBoxes(list, defaultResolution, boxes, elementIds) { - const nodes = [...list.childNodes]; - let i = skipContainerLevelHead(nodes, 0); - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType !== Node.ELEMENT_NODE || localName(node) !== "ldiv") { - i += 1; - continue; - } - i += 1; - const head = parseElementHeadAt(nodes, i); - if (head) { - const elementId = elementIds.get(node); - if (elementId) { - pushBoundingBox(boxes, head.locs, defaultResolution, "text", "text", elementId, elementLayer(node)); - } - i = head.nextIndex; - } - i = skipUntilListItemBoundary(nodes, i); - } -} - -function collectTableVirtualTextBoxes(container, defaultResolution, boxes, elementIds) { - const nodes = [...container.childNodes]; - let i = skipContainerLevelHead(nodes, 0); - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType !== Node.ELEMENT_NODE) { - i += 1; - continue; - } - const tag = localName(node); - if (!isCellToken(tag)) { - i += 1; - continue; - } - if (tag === "nl") { - i += 1; - continue; - } - i += 1; - const head = parseElementHeadAt(nodes, i); - if (head) { - const elementId = elementIds.get(node); - if (elementId) { - pushBoundingBox(boxes, head.locs, defaultResolution, "text", "text", elementId, elementLayer(node)); - } - i = head.nextIndex; - } - i = skipUntilCellBoundary(nodes, i); - } -} - -function skipContainerLevelHead(nodes, startIdx) { - let i = startIdx; - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType !== Node.ELEMENT_NODE) { - i += 1; - continue; - } - const tag = localName(node); - if (tag === "ldiv" || isCellToken(tag)) break; - if (HEAD_TAGS.has(tag) || tag === "location") { - i += 1; - continue; - } - break; - } - return i; -} - -function skipUntilListItemBoundary(nodes, startIdx) { - let i = startIdx; - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType === Node.ELEMENT_NODE && localName(node) === "ldiv") break; - i += 1; - } - return i; -} - -function skipUntilCellBoundary(nodes, startIdx) { - let i = startIdx; - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType === Node.ELEMENT_NODE && isCellToken(localName(node))) break; - i += 1; - } - return i; -} - -function isCellToken(tag) { - return CELL_TOKENS.has(tag); -} - -/** @returns {{ locs: Element[], nextIndex: number } | null} */ -function parseElementHeadAt(nodes, startIdx) { - const locs = []; - let i = startIdx; - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType !== Node.ELEMENT_NODE) { - i += 1; - continue; - } - const tag = localName(node); - if (tag === "location") { - locs.push(node); - i += 1; - if (locs.length === 4) return { locs, nextIndex: i }; - continue; - } - if (locs.length) break; - if (HEAD_TAGS.has(tag)) { - i += 1; - continue; - } - break; - } - return locs.length === 4 ? { locs, nextIndex: i } : null; -} - -function walkElements(nodes, fn) { - for (const node of nodes) { - if (node.nodeType !== Node.ELEMENT_NODE) continue; - fn(node); - walkElements(childElements(node), fn); - } -} - -function headLocations(el) { - return parseElementHeadAt([...el.childNodes], 0)?.locs ?? []; -} - -/** @returns {{ elementId: string, box: object } | null} */ -function findNearestLocatedBox(el, elementIds, boxById) { - let node = el; - while (node) { - const elementId = elementIds.get(node); - const box = elementId ? boxById.get(elementId) : null; - if (box) return { elementId, box }; - if (localName(node) === "doclang") break; - node = node.parentElement; - } - return null; -} - -function locationResolution(el, axisDefault) { - const r = parseInt(el.getAttribute("resolution") ?? String(axisDefault), 10); - return Number.isFinite(r) && r > 0 ? r : axisDefault; -} - -function headingLevel(el) { - return Math.min(Math.max(parseInt(el.getAttribute("level") ?? "1", 10) || 1, 1), 6); -} - -function elementLabel(el) { - if (isVirtualTextOverlayUnit(el)) return "text"; - const tag = localName(el); - if (tag === "heading" || tag === "field_heading") return `${tag}[${headingLevel(el)}]`; - const level = el.getAttribute("level"); - if (level) return `${tag}[${level}]`; - const cls = el.getAttribute("class"); - if (cls) return `${tag}.${cls}`; - return tag; -} - -function elementKindKey(kind) { - if (kind.startsWith("field_") || kind === "key" || kind === "value" || kind === "hint") return "field"; - if (kind === "tabular") return "table"; - const known = new Set([ - "text", "heading", "list", "ldiv", "table", "index", "formula", "code", "picture", - "group", "footnote", "page_header", "page_footer", "caption", - ]); - return known.has(kind) ? kind : "default"; -} - -function kindClassForTag(tag) { - return `kind-${elementKindKey(tag)}`; -} - -function bboxClassForKind(kind) { - return elementKindKey(kind); -} - -function boxPixelRect(b, img) { - const x = (b.x0 / b.resW) * img.naturalWidth; - const y = (b.y0 / b.resH) * img.naturalHeight; - const w = ((b.x1 - b.x0) / b.resW) * img.naturalWidth; - const h = ((b.y1 - b.y0) / b.resH) * img.naturalHeight; - return { x, y, w, h, area: w * h }; -} - -function overlayBoxPaintPriority(box) { - if (box.kind === "text") return 0; - if (box.kind === "list" || box.kind === "table" || box.kind === "index" || box.kind === "tabular") { - return 2; - } - return 1; -} - -function overlayLayerPriority(layer) { - if (layer === "background") return 0; - if (layer === "furniture") return 1; - return 2; -} - -function compareOverlayBoxPaintOrder(a, b) { - const byLayer = overlayLayerPriority(a.layer ?? "body") - overlayLayerPriority(b.layer ?? "body"); - if (byLayer !== 0) return byLayer; - const byPriority = overlayBoxPaintPriority(a) - overlayBoxPaintPriority(b); - if (byPriority !== 0) return byPriority; - if (selectedElementId) { - const aSelected = a.elementId === selectedElementId; - const bSelected = b.elementId === selectedElementId; - if (aSelected !== bSelected) return aSelected ? 1 : -1; - } - return 0; -} - -function sortedOverlayBoxes(boxes) { - return [...boxes].sort(compareOverlayBoxPaintOrder); -} - -function boxCenter(rect) { - return { x: rect.x + rect.w / 2, y: rect.y + rect.h / 2 }; -} - -function ensureArrowMarker(defs, markerId) { - if (defs.querySelector(`#${markerId}`)) return; - const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker"); - marker.setAttribute("id", markerId); - marker.setAttribute("viewBox", "0 0 6 6"); - marker.setAttribute("refX", "6"); - marker.setAttribute("refY", "3"); - marker.setAttribute("markerWidth", "5"); - marker.setAttribute("markerHeight", "5"); - marker.setAttribute("orient", "auto"); - const arrowPath = document.createElementNS("http://www.w3.org/2000/svg", "path"); - arrowPath.setAttribute("d", "M0,0 L6,3 L0,6 Z"); - arrowPath.setAttribute("fill", "currentColor"); - marker.appendChild(arrowPath); - defs.appendChild(marker); -} - -function alignDashedLineToEnd(line, start, end) { - const dash = 6; - const gap = 4; - const period = dash + gap; - const len = Math.hypot(end.x - start.x, end.y - start.y); - if (!len) return; - line.setAttribute("stroke-dasharray", `${dash} ${gap}`); - const offset = len % period; - if (offset > 0.01) line.setAttribute("stroke-dashoffset", String(offset)); -} - -function ensureLayerHatchPatterns(defs) { - if (defs.querySelector("#layer-hatch")) return; - - const pattern = document.createElementNS("http://www.w3.org/2000/svg", "pattern"); - pattern.setAttribute("id", "layer-hatch"); - pattern.setAttribute("width", "16"); - pattern.setAttribute("height", "16"); - pattern.setAttribute("patternUnits", "userSpaceOnUse"); - pattern.setAttribute("patternTransform", "rotate(45 8 8)"); - const stripe = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - stripe.setAttribute("class", "layer-hatch-stripe"); - stripe.setAttribute("x", "10"); - stripe.setAttribute("y", "-4"); - stripe.setAttribute("width", "6"); - stripe.setAttribute("height", "24"); - pattern.appendChild(stripe); - defs.appendChild(pattern); -} - -function ensureOverlayDefs(svg) { - let defs = svg.querySelector("defs"); - if (!defs) { - defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); - svg.insertBefore(defs, svg.firstChild); - } - return defs; -} - -function appendOverlayLinks(svg, img, links, { markerId, linkClass, fromIdAttr, toIdAttr }) { - if (!links.length) return; - - const defs = ensureOverlayDefs(svg); - ensureArrowMarker(defs, markerId); - - for (const link of links) { - const from = boxPixelRect(link.fromBox ?? link.captionBox, img); - const to = boxPixelRect(link.toBox ?? link.hostBox, img); - const start = boxCenter(from); - const end = boxCenter(to); - - const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); - line.setAttribute("class", linkClass); - line.setAttribute("x1", String(start.x)); - line.setAttribute("y1", String(start.y)); - line.setAttribute("x2", String(end.x)); - line.setAttribute("y2", String(end.y)); - line.setAttribute("marker-end", `url(#${markerId})`); - line.setAttribute(fromIdAttr, link.fromElementId ?? link.captionElementId); - line.setAttribute(toIdAttr, link.toElementId ?? link.hostElementId); - svg.appendChild(line); - } -} - -function appendCaptionLinks(svg, img, captionLinks) { - appendOverlayLinks(svg, img, captionLinks, { - markerId: "caption-arrowhead", - linkClass: "caption-link", - fromIdAttr: "data-caption-id", - toIdAttr: "data-host-id", - }); -} - -function appendXrefLinks(svg, img, xrefLinks) { - appendOverlayLinks(svg, img, xrefLinks, { - markerId: "xref-arrowhead", - linkClass: "xref-link", - fromIdAttr: "data-xref-from-id", - toIdAttr: "data-xref-to-id", - }); -} - -function docPointToPixel(xDoc, yDoc, resW, resH, img) { - return { - x: (xDoc / resW) * img.naturalWidth, - y: (yDoc / resH) * img.naturalHeight, - }; -} - -function pageCornerTarget(img, defaultResolution, corner) { - const { width: resW, height: resH } = defaultResolution; - const inset = overlayUserLength(5); - const tl = docPointToPixel(0, 0, resW, resH, img); - const br = docPointToPixel(resW, resH, resW, resH, img); - if (corner === "tl") { - return { - x: Math.min(tl.x + inset, br.x - inset), - y: Math.min(tl.y + inset, br.y - inset), - }; - } - return { - x: Math.max(br.x - inset, tl.x + inset), - y: Math.max(br.y - inset, tl.y + inset), - }; -} - -function elementThreadId(el) { - return firstHeadChild(el, "thread")?.getAttribute("thread_id") ?? null; -} - -/** @returns {Set} */ -function fragmentPeerElementIds(elementId) { - const peers = new Set(); - if (!elementId || !state?.elementIds || !state.idToElement) return peers; - const el = state.idToElement.get(elementId); - const threadId = el ? elementThreadId(el) : null; - if (!threadId) return peers; - for (const [node, id] of state.elementIds) { - if (elementThreadId(node) === threadId) peers.add(id); - } - return peers; -} - -function isFragmentLinkRelevant(linkEl, peerIds) { - const fromId = linkEl.getAttribute("data-fragment-from-id"); - const toId = linkEl.getAttribute("data-fragment-to-id"); - if (fromId && peerIds.has(fromId)) return true; - if (toId && peerIds.has(toId)) return true; - return false; -} - -function appendFragmentLinks(svg, img, links, defaultResolution) { - if (!links.length) return; - - const defs = ensureOverlayDefs(svg); - ensureArrowMarker(defs, "fragment-arrowhead"); - const fontSize = overlayUserLength(OVERLAY_BADGE_FONT_SIZE); - - for (const link of links) { - const fromRect = boxPixelRect(link.fromBox, img); - - const group = document.createElementNS("http://www.w3.org/2000/svg", "g"); - group.setAttribute("class", "fragment-link"); - group.setAttribute("data-thread-id", link.threadId); - group.setAttribute("data-fragment-from-id", link.fromElementId); - if (link.toElementId) group.setAttribute("data-fragment-to-id", link.toElementId); - - let labelAt; - if (link.toBox) { - const toRect = boxPixelRect(link.toBox, img); - const start = boxCenter(fromRect); - const end = boxCenter(toRect); - - const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); - line.setAttribute("class", "fragment-link-path fragment-link-path-dashed"); - line.setAttribute("x1", String(start.x)); - line.setAttribute("y1", String(start.y)); - line.setAttribute("x2", String(end.x)); - line.setAttribute("y2", String(end.y)); - line.setAttribute("marker-end", "url(#fragment-arrowhead)"); - alignDashedLineToEnd(line, start, end); - group.appendChild(line); - labelAt = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 }; - } else { - const corner = link.targetCorner ?? "br"; - const cornerPoint = pageCornerTarget(img, defaultResolution, corner); - const elementAnchor = boxCenter(fromRect); - const incoming = corner === "tl"; - const start = incoming ? cornerPoint : elementAnchor; - const end = incoming ? elementAnchor : cornerPoint; - - const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); - line.setAttribute("class", "fragment-link-path fragment-link-path-dashed"); - line.setAttribute("x1", String(start.x)); - line.setAttribute("y1", String(start.y)); - line.setAttribute("x2", String(end.x)); - line.setAttribute("y2", String(end.y)); - line.setAttribute("marker-end", "url(#fragment-arrowhead)"); - alignDashedLineToEnd(line, start, end); - group.appendChild(line); - labelAt = { x: (start.x + end.x) / 2, y: (start.y + end.y) / 2 }; - } - - const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); - text.setAttribute("class", "fragment-link-label"); - text.setAttribute("x", String(labelAt.x)); - text.setAttribute("y", String(labelAt.y)); - text.setAttribute("font-size", String(fontSize)); - text.textContent = link.toBox ? FRAGMENT_LINK_LABEL_SAME_PAGE : FRAGMENT_LINK_LABEL_CROSS_PAGE; - group.appendChild(text); - - svg.appendChild(group); - } -} - -function appendFragmentNavButtons(svg, img, items) { - if (!items.length) return; - - const btnSize = overlayUserLength(13 * 1.5); - const gap = overlayUserLength(2); - const inset = overlayUserLength(3); - const fontSize = overlayUserLength(10 * 1.5); - const radius = overlayUserLength(2 * 1.5); - - for (const item of items) { - const { x, y, w, h } = boxPixelRect(item.box, img); - const group = document.createElementNS("http://www.w3.org/2000/svg", "g"); - group.setAttribute("class", "fragment-nav"); - group.setAttribute("data-element-id", item.elementId); - - const rowY = y + h - inset - btnSize; - const nextX = x + w - inset - btnSize; - const prevX = nextX - gap - btnSize; - - appendFragmentNavButton(group, prevX, rowY, btnSize, radius, fontSize, "prev", "‹", item.hasPrev); - appendFragmentNavButton(group, nextX, rowY, btnSize, radius, fontSize, "next", "›", item.hasNext); - - svg.appendChild(group); - } -} - -function appendFragmentNavButton(group, x, y, size, radius, fontSize, direction, label, enabled) { - const btn = document.createElementNS("http://www.w3.org/2000/svg", "g"); - btn.setAttribute("class", `fragment-nav-btn fragment-nav-btn-${direction}${enabled ? "" : " fragment-nav-btn-disabled"}`); - btn.setAttribute("data-nav", direction); - if (enabled) { - btn.setAttribute("role", "button"); - btn.setAttribute( - "aria-label", - direction === "prev" ? FRAGMENT_NAV_HINT_PREV : FRAGMENT_NAV_HINT_NEXT, - ); - } - - const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - bg.setAttribute("class", "fragment-nav-btn-bg"); - bg.setAttribute("x", String(x)); - bg.setAttribute("y", String(y)); - bg.setAttribute("width", String(size)); - bg.setAttribute("height", String(size)); - bg.setAttribute("rx", String(radius)); - bg.setAttribute("ry", String(radius)); - btn.appendChild(bg); - - const text = document.createElementNS("http://www.w3.org/2000/svg", "text"); - text.setAttribute("class", "fragment-nav-btn-label"); - text.setAttribute("x", String(x + size / 2)); - text.setAttribute("y", String(y + size / 2)); - text.setAttribute("font-size", String(fontSize)); - text.textContent = label; - btn.appendChild(text); - - group.appendChild(btn); -} - -function appendReadingOrderOverlay(svg, img, steps) { - if (!steps.length) return; - - if (steps.length >= 2) { - const defs = ensureOverlayDefs(svg); - ensureArrowMarker(defs, "reading-order-arrowhead"); - - for (let i = 0; i < steps.length - 1; i += 1) { - const from = boxPixelRect(steps[i].box, img); - const to = boxPixelRect(steps[i + 1].box, img); - const start = boxCenter(from); - const end = boxCenter(to); - - const line = document.createElementNS("http://www.w3.org/2000/svg", "line"); - line.setAttribute("class", "reading-order-step"); - line.setAttribute("x1", String(start.x)); - line.setAttribute("y1", String(start.y)); - line.setAttribute("x2", String(end.x)); - line.setAttribute("y2", String(end.y)); - line.setAttribute("marker-end", "url(#reading-order-arrowhead)"); - svg.appendChild(line); - } - } -} - -function imageCoordsFromEvent(svg, evt) { - const pt = svg.createSVGPoint(); - pt.x = evt.clientX; - pt.y = evt.clientY; - const ctm = svg.getScreenCTM()?.inverse(); - if (!ctm) return null; - const p = pt.matrixTransform(ctm); - return { x: p.x, y: p.y }; -} - -function hitTestBoxes(boxes, img, x, y) { - let best = null; - let bestArea = Infinity; - for (const b of boxes) { - const { x: bx, y: by, w, h, area } = boxPixelRect(b, img); - if (x >= bx && x <= bx + w && y >= by && y <= by + h && area < bestArea) { - best = b; - bestArea = area; - } - } - return best; -} - -function buildOverlay(img, boxes, captionLinks = [], xrefLinks = [], readingOrderSteps = [], fragmentLinks = [], fragmentNavItems = [], defaultResolution = { width: 512, height: 512 }) { - const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); - svg.classList.add("overlay"); - svg.setAttribute("viewBox", `0 0 ${img.naturalWidth} ${img.naturalHeight}`); - svg.setAttribute("overflow", "hidden"); - - appendCaptionLinks(svg, img, captionLinks); - appendXrefLinks(svg, img, xrefLinks); - appendFragmentLinks(svg, img, fragmentLinks, defaultResolution); - appendReadingOrderOverlay(svg, img, readingOrderSteps); - - const defs = ensureOverlayDefs(svg); - ensureLayerHatchPatterns(defs); - - for (const b of sortedOverlayBoxes(boxes)) { - const { x, y, w, h } = boxPixelRect(b, img); - const cls = bboxClassForKind(b.kind); - const kindClass = kindClassForTag(b.kind); - const layerClass = layerClassForValue(b.layer ?? "body"); - - const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); - rect.setAttribute( - "class", - `bbox bbox-${cls} ${kindClass}${layerClass ? ` ${layerClass}` : ""}`, - ); - rect.setAttribute("x", String(x)); - rect.setAttribute("y", String(y)); - rect.setAttribute("width", String(Math.max(w, 1))); - rect.setAttribute("height", String(Math.max(h, 1))); - rect.setAttribute("data-element-id", b.elementId); - svg.appendChild(rect); - } - - appendFragmentNavButtons(svg, img, fragmentNavItems); - - svg.addEventListener("click", (e) => { - if (pagePanSuppressClick) { - pagePanSuppressClick = false; - return; - } - const navBtn = e.target.closest(".fragment-nav-btn:not(.fragment-nav-btn-disabled)"); - if (navBtn) { - e.stopPropagation(); - const navGroup = navBtn.closest(".fragment-nav"); - const elementId = navGroup?.getAttribute("data-element-id"); - const direction = navBtn.getAttribute("data-nav"); - if (elementId && direction) navigateThreadFragment(elementId, direction); - return; - } - const badge = e.target.closest(".overlay-badge[data-element-id]"); - if (badge) { - const elementId = resolveSelectionElementId(badge.getAttribute("data-element-id")); - if (elementId) selectElement(elementId); - return; - } - const coords = imageCoordsFromEvent(svg, e); - if (!coords) return; - const hit = hitTestBoxes(boxes, img, coords.x, coords.y); - if (hit) selectElement(hit.elementId); - else clearSelection(); - }); - - svg.addEventListener("mousemove", (e) => { - const coords = imageCoordsFromEvent(svg, e); - svg.style.cursor = coords && hitTestBoxes(boxes, img, coords.x, coords.y) ? "pointer" : ""; - }); - svg.addEventListener("mouseleave", () => { - svg.style.cursor = ""; - }); - - return svg; -} - -function selectElement(elementId) { - if (!elementId) return; - selectedElementId = elementId; - applySelection(); -} - -function clearSelection() { - selectedElementId = null; - applySelection(); -} - -function isPictureContentOverlayElement(elementId) { - return isPictureContentElement(state?.idToElement?.get(elementId) ?? null); -} - -function isTableContentOverlayElement(elementId) { - return isTableContentElement(state?.idToElement?.get(elementId) ?? null); -} - -function isContentsOptionHidden(elementId, clickVisible) { - if (clickVisible) return false; - if (!showPictureContents && isPictureContentOverlayElement(elementId)) return true; - if (!showTableContents && isTableContentOverlayElement(elementId)) return true; - return false; -} - -function applyBboxVisibility() { - if (!state?.hasPageView || !els.pagePane) return; - - const peerIds = selectedElementId ? fragmentPeerElementIds(selectedElementId) : new Set(); - - for (const el of els.pagePane.querySelectorAll(".bbox")) { - el.classList.remove("related"); - const elementId = el.getAttribute("data-element-id"); - const clickVisible = elementId === selectedElementId || peerIds.has(elementId); - if (showAllBboxes) { - if (isContentsOptionHidden(elementId, clickVisible)) { - el.classList.add("bbox-hidden"); - } else { - el.classList.remove("bbox-hidden"); - if (peerIds.has(elementId)) { - el.classList.add("related"); - } - } - continue; - } - if (elementId === selectedElementId) { - el.classList.remove("bbox-hidden"); - } else if (peerIds.has(elementId)) { - el.classList.remove("bbox-hidden"); - el.classList.add("related"); - } else { - el.classList.add("bbox-hidden"); - } - } - - for (const el of els.pagePane.querySelectorAll(".element-badge")) { - const elementId = el.getAttribute("data-element-id"); - const clickVisible = elementId === selectedElementId || peerIds.has(elementId); - if (!showAllBboxes || !showLayoutBadges) { - el.classList.add("bbox-hidden"); - continue; - } - if (isContentsOptionHidden(elementId, clickVisible)) { - el.classList.add("bbox-hidden"); - } else { - el.classList.remove("bbox-hidden"); - } - } - - for (const el of els.pagePane.querySelectorAll(".caption-link")) { - if (!showAllBboxes || !showCaptionLinks) { - el.classList.add("bbox-hidden"); - continue; - } - el.classList.remove("bbox-hidden"); - } - - for (const el of els.pagePane.querySelectorAll(".xref-link")) { - if (!showAllBboxes || !showXrefLinks) { - el.classList.add("bbox-hidden"); - continue; - } - el.classList.remove("bbox-hidden"); - } - - for (const el of els.pagePane.querySelectorAll(".fragment-link")) { - const clickVisible = Boolean(selectedElementId && isFragmentLinkRelevant(el, peerIds)); - const optionVisible = showAllBboxes && showFragmentLinks; - el.classList.toggle("bbox-hidden", !(clickVisible || optionVisible)); - } - - for (const el of els.pagePane.querySelectorAll(".fragment-nav")) { - const elementId = el.getAttribute("data-element-id"); - const clickVisible = elementId === selectedElementId || peerIds.has(elementId); - const optionVisible = showAllBboxes && showFragmentLinks; - el.classList.toggle("bbox-hidden", !(clickVisible || optionVisible)); - } - - for (const el of els.pagePane.querySelectorAll(".reading-order-badge")) { - const elementId = el.getAttribute("data-element-id"); - const clickVisible = elementId === selectedElementId || peerIds.has(elementId); - if (!showAllBboxes || !showReadingOrder) { - el.classList.add("bbox-hidden"); - continue; - } - if ( - isPictureContentOverlayElement(elementId) - || isTableContentOverlayElement(elementId) - || isContentsOptionHidden(elementId, clickVisible) - ) { - el.classList.add("bbox-hidden"); - continue; - } - el.classList.remove("bbox-hidden"); - } - - for (const el of els.pagePane.querySelectorAll(".reading-order-step")) { - if (!showAllBboxes || !showReadingOrder || !showReadingOrderArrows) { - el.classList.add("bbox-hidden"); - continue; - } - el.classList.remove("bbox-hidden"); - } -} - -function findMarkupElementForSelection(elementId) { - if (!els.markupPane) return null; - return ( - els.markupPane.querySelector(`.markup-el-virtual-text[data-element-id="${elementId}"]`) || - els.markupPane.querySelector(`[data-element-id="${elementId}"]`) - ); -} - -function findRenderedElementForSelection(elementId) { - if (!els.renderedPane) return null; - const direct = - els.renderedPane.querySelector(`.rendered-el-virtual-text[data-element-id="${elementId}"]`) || - els.renderedPane.querySelector(`.rendered-el[data-element-id="${elementId}"]`); - if (direct) return direct; - - const xmlEl = state?.idToElement?.get(elementId); - const threadId = xmlEl ? elementThreadId(xmlEl) : null; - if (!threadId) return null; - - const merged = els.renderedPane.querySelector( - `.rendered-fragment-merged[data-thread-id="${threadId}"]`, - ); - if (!merged) return null; - - const primaryId = merged.getAttribute("data-element-id"); - if (!primaryId || primaryId === elementId) return merged; - return fragmentPeerElementIds(primaryId).has(elementId) ? merged : null; -} - -function revealRenderedSelectionContext(renderedEl) { - const pictureContents = renderedEl.closest(".rendered-picture-contents"); - if (pictureContents && !pictureContents.open) { - pictureContents.open = true; - } - revealReadingLayerForSelection(renderedEl); -} - -function revealReadingLayerForSelection(renderedEl) { - const layer = renderedEl.getAttribute("data-doclang-layer"); - if (!layer || layer === "body") return; - let changed = false; - if (layer === "furniture" && !showReadingFurniture) { - showReadingFurniture = true; - changed = true; - } else if (layer === "background" && !showReadingBackground) { - showReadingBackground = true; - changed = true; - } - if (!changed) return; - syncReadingLayerCheckboxes(); - syncReadingLayerVisibility(); -} - -function applySelection() { - els.markupPane?.querySelectorAll(".markup-el.selected").forEach((el) => { - el.classList.remove("selected"); - }); - els.renderedPane?.querySelectorAll(".rendered-el.selected").forEach((el) => { - el.classList.remove("selected"); - }); - if (!els.pagePane) return; - els.pagePane.querySelectorAll(".bbox.selected, .overlay-badge.selected").forEach((el) => { - el.classList.remove("selected"); - }); - - if (!selectedElementId) { - const img = els.pagePane.querySelector(".page-view img"); - if (img) syncOverlayBadges(img); - applyBboxVisibility(); - return; - } - - const markupEl = findMarkupElementForSelection(selectedElementId); - if (markupEl) { - markupEl.classList.add("selected"); - markupEl.scrollIntoView({ block: "nearest", behavior: "smooth" }); - } - - const renderedEl = findRenderedElementForSelection(selectedElementId); - if (renderedEl) { - revealRenderedSelectionContext(renderedEl); - renderedEl.classList.add("selected"); - renderedEl.scrollIntoView({ block: "nearest", behavior: "smooth" }); - } - - if (state?.hasPageView) { - for (const el of els.pagePane.querySelectorAll(`[data-element-id="${selectedElementId}"]`)) { - el.classList.add("selected"); - } - } - - const img = els.pagePane.querySelector(".page-view img"); - if (img) syncOverlayBadges(img); - applyBboxVisibility(); -} - -function buildMarkupView(segment, elementIds) { - const root = document.createElement("div"); - root.className = "markup"; - for (const el of segment) { - if (el.nodeType === Node.ELEMENT_NODE) { - root.appendChild(buildMarkupElement(el, 0, elementIds)); - } - } - root.addEventListener("click", (e) => { - const attrToggle = e.target.closest(".xml-attr-value-chip"); - if (attrToggle) { - e.stopPropagation(); - toggleTruncatableMarkupAttrValue(attrToggle); - return; - } - const toggle = e.target.closest(".markup-fold-toggle"); - if (toggle) { - e.stopPropagation(); - const block = toggle.closest(".markup-el-foldable"); - if (block) { - const collapsed = block.classList.toggle("markup-collapsed"); - toggle.setAttribute("aria-expanded", String(!collapsed)); - toggle.setAttribute("aria-label", collapsed ? "Expand" : "Collapse"); - } - return; - } - const elementId = resolveMarkupClickTarget(e.target); - if (elementId) selectElement(elementId); - }); - return root; -} - -function sliceHasMarkupContent(nodes) { - for (const node of nodes) { - if (isTextLikeNode(node) && !isWhitespaceOnlyText(node)) return true; - if (node.nodeType === Node.ELEMENT_NODE) return true; - } - return false; -} - -function isVirtualTextSkippableNode(node) { - if (isWhitespaceOnlyText(node)) return true; - if (node.nodeType !== Node.ELEMENT_NODE) return false; - const tag = localName(node); - return tag === "location" || HEAD_TAGS.has(tag); -} - -function shouldWrapVirtualText(contentNodes) { - if (!sliceHasMarkupContent(contentNodes)) return false; - - for (const node of contentNodes) { - if (isVirtualTextSkippableNode(node)) continue; - if (isTextLikeNode(node)) return true; - if (node.nodeType === Node.ELEMENT_NODE && !isSemanticElement(node)) return true; - } - return false; -} - -function appendRenderedSliceContent(container, hostEl, contentNodes, elementIds) { - if (!sliceHasMarkupContent(contentNodes)) return; - if (shouldWrapVirtualText(contentNodes)) { - container.appendChild(renderVirtualTextBlock(hostEl, contentNodes, elementIds)); - return; - } - for (const node of contentNodes) { - if (isVirtualTextSkippableNode(node)) continue; - if (node.nodeType === Node.ELEMENT_NODE && RENDER_BLOCK_TAGS.has(localName(node))) { - const rendered = renderBlockElement(node, elementIds, { inline: false }); - if (rendered) container.appendChild(rendered); - } else { - appendRenderedNode(container, node, elementIds, { inline: true }); - } - } -} - -function appendMarkupNodesFromSlice(parent, depth, nodes, elementIds) { - for (const child of nodes) { - if (isTextLikeNode(child)) { - const text = formatMarkupTextNode(child); - if (text) appendMarkupTextLine(parent, depth, text); - } else if (child.nodeType === Node.ELEMENT_NODE) { - parent.appendChild(buildMarkupElement(child, depth, elementIds)); - } - } -} - -function appendMarkupVirtualText(parent, depth, hostEl, contentNodes, elementIds) { - if (!shouldWrapVirtualText(contentNodes)) { - appendMarkupNodesFromSlice(parent, depth, contentNodes, elementIds); - return; - } - - const block = document.createElement("div"); - block.className = "markup-el markup-el-virtual-text"; - const elementId = elementIds.get(hostEl); - if (elementId) block.setAttribute("data-element-id", elementId); - - appendMarkupFoldableOpen(block, depth, "text", [], VIRTUAL_TEXT_TAG_HINT); - block.classList.add("markup-el-foldable"); - - const foldBody = document.createElement("div"); - foldBody.className = "markup-fold-body"; - const children = document.createElement("div"); - children.className = "markup-children"; - appendMarkupNodesFromSlice(children, depth + 1, contentNodes, elementIds); - foldBody.appendChild(children); - appendMarkupCloseTag(foldBody, depth, "text", VIRTUAL_TEXT_TAG_HINT); - block.appendChild(foldBody); - parent.appendChild(block); -} - -function buildMarkupFoldableElement(el, depth, elementIds, buildBody) { - const tag = localName(el); - const block = document.createElement("div"); - block.className = "markup-el"; - const elementId = elementIds.get(el); - if (elementId) block.setAttribute("data-element-id", elementId); - - const attributes = markupAttributes(el); - appendMarkupFoldableOpen(block, depth, tag, attributes); - block.classList.add("markup-el-foldable"); - - const foldBody = document.createElement("div"); - foldBody.className = "markup-fold-body"; - const children = document.createElement("div"); - children.className = "markup-children"; - buildBody(children, depth + 1); - foldBody.appendChild(children); - appendMarkupCloseTag(foldBody, depth, tag); - block.appendChild(foldBody); - return block; -} - -function buildMarkupList(el, depth, elementIds) { - return buildMarkupFoldableElement(el, depth, elementIds, (children, childDepth) => { - const nodes = [...el.childNodes]; - let i = 0; - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType === Node.ELEMENT_NODE && localName(node) === "ldiv") break; - if (isTextLikeNode(node) && isWhitespaceOnlyText(node)) { - i += 1; - continue; - } - if (isTextLikeNode(node)) break; - if (node.nodeType === Node.ELEMENT_NODE) { - const tag = localName(node); - if (HEAD_TAGS.has(tag) || tag === "location") { - children.appendChild(buildMarkupElement(node, childDepth, elementIds)); - i += 1; - continue; - } - } - break; - } - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType !== Node.ELEMENT_NODE || localName(node) !== "ldiv") { - appendMarkupNodesFromSlice(children, childDepth, [node], elementIds); - i += 1; - continue; - } - const ldiv = node; - children.appendChild(buildMarkupElement(ldiv, childDepth, elementIds)); - i += 1; - const end = skipUntilListItemBoundary(nodes, i); - appendMarkupVirtualText(children, childDepth, ldiv, nodes.slice(i, end), elementIds); - i = end; - } - }); -} - -function buildMarkupOtslContainer(el, depth, elementIds) { - return buildMarkupFoldableElement(el, depth, elementIds, (children, childDepth) => { - const nodes = [...el.childNodes]; - let i = 0; - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType === Node.ELEMENT_NODE && isCellToken(localName(node))) break; - if (isTextLikeNode(node) && isWhitespaceOnlyText(node)) { - i += 1; - continue; - } - if (isTextLikeNode(node)) break; - if (node.nodeType === Node.ELEMENT_NODE) { - const tag = localName(node); - if (HEAD_TAGS.has(tag) || tag === "location" || tag === "h_thread") { - children.appendChild(buildMarkupElement(node, childDepth, elementIds)); - i += 1; - continue; - } - } - break; - } - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType !== Node.ELEMENT_NODE) { - appendMarkupNodesFromSlice(children, childDepth, [node], elementIds); - i += 1; - continue; - } - const tag = localName(node); - if (tag === "nl") { - appendMarkupSelfClosingTag(children, childDepth, "nl", []); - i += 1; - continue; - } - if (!isCellToken(tag)) { - appendMarkupNodesFromSlice(children, childDepth, [node], elementIds); - i += 1; - continue; - } - const cell = node; - children.appendChild(buildMarkupElement(cell, childDepth, elementIds)); - i += 1; - if (CELL_SPAN_TAGS.has(tag)) continue; - const end = skipUntilCellBoundary(nodes, i); - appendMarkupVirtualText(children, childDepth, cell, nodes.slice(i, end), elementIds); - i = end; - } - }); -} - -function buildMarkupElement(el, depth, elementIds) { - const tag = localName(el); - if (tag === "list") return buildMarkupList(el, depth, elementIds); - if (OTSL_CONTAINER_TAGS.has(tag)) return buildMarkupOtslContainer(el, depth, elementIds); - - const block = document.createElement("div"); - block.className = "markup-el"; - const elementId = elementIds.get(el); - if (elementId) block.setAttribute("data-element-id", elementId); - - const attributes = markupAttributes(el); - - if (!el.childNodes.length) { - appendMarkupSelfClosingTag(block, depth, tag, attributes); - return block; - } - - const meaningfulText = [...el.childNodes].filter((n) => isTextLikeNode(n) && !isWhitespaceOnlyText(n)); - const textOnly = meaningfulText.length > 0 && meaningfulText.every(isTextLikeNode) && !childElements(el).length; - if (textOnly) { - const text = serializeMarkupTextNodes(el.childNodes); - if (text) { - appendMarkupInlineElement(block, depth, tag, attributes, text); - return block; - } - } - - appendMarkupFoldableOpen(block, depth, tag, attributes); - block.classList.add("markup-el-foldable"); - - const foldBody = document.createElement("div"); - foldBody.className = "markup-fold-body"; - const children = document.createElement("div"); - children.className = "markup-children"; - for (const child of el.childNodes) { - if (isTextLikeNode(child)) { - const text = formatMarkupTextNode(child); - if (text) appendMarkupTextLine(children, depth + 1, text); - } else if (child.nodeType === Node.ELEMENT_NODE) { - children.appendChild(buildMarkupElement(child, depth + 1, elementIds)); - } - } - foldBody.appendChild(children); - appendMarkupCloseTag(foldBody, depth, tag); - block.appendChild(foldBody); - return block; -} - -/** @returns {Map} */ -function collectIntraPageThreads(segment) { - /** @type {Map} */ - const byThread = new Map(); - for (const el of segment) { - if (el.nodeType !== Node.ELEMENT_NODE) continue; - if (localName(el) === "page_break") continue; - const threadId = elementThreadId(el); - if (!threadId) continue; - if (!byThread.has(threadId)) byThread.set(threadId, []); - byThread.get(threadId).push(el); - } - for (const [threadId, members] of byThread) { - if (members.length < 2) byThread.delete(threadId); - } - return byThread; -} - -function findLastTextNode(node) { - if (isTextLikeNode(node)) return node; - if (node.nodeType !== Node.ELEMENT_NODE) return null; - for (let i = node.childNodes.length - 1; i >= 0; i -= 1) { - const found = findLastTextNode(node.childNodes[i]); - if (found) return found; - } - return null; -} - -function trimParentTrailingForFragmentJoin(parent) { - const lastText = findLastTextNode(parent); - if (!lastText) return; - let value = lastText.textContent ?? ""; - value = value.replace(/\s+$/u, ""); - if (value.endsWith("-")) value = value.slice(0, -1); - if (!value) { - lastText.parentNode?.removeChild(lastText); - trimParentTrailingForFragmentJoin(parent); - return; - } - lastText.textContent = value; -} - -function appendMergedTextFragments(parent, fragments, elementIds) { - for (let i = 0; i < fragments.length; i += 1) { - if (i > 0) trimParentTrailingForFragmentJoin(parent); - appendRenderedBody(parent, fragments[i], elementIds, { - inline: true, - trimLeading: i > 0, - }); - } -} - -function renderMergedIntraPageFragments(fragments, elementIds) { - const first = fragments[0]; - const tag = localName(first); - const firstId = elementIds.get(first); - const threadId = elementThreadId(first); - - let node; - if (tag === "text") { - node = document.createElement("p"); - appendMergedTextFragments(node, fragments, elementIds); - } else if (tag === "list") { - const listClass = first.getAttribute("class") ?? "unordered"; - node = document.createElement(listClass === "ordered" ? "ol" : "ul"); - for (const el of fragments) { - appendListItemsFromElement(node, el, elementIds); - } - } else { - node = document.createElement("div"); - node.className = "rendered-fragment-merged-body"; - for (const el of fragments) { - appendRenderedBodyBlocks(node, el, elementIds); - } - } - - const wrap = wrapRendered(first, node, firstId, "rendered-fragment-merged"); - if (threadId) wrap.setAttribute("data-thread-id", threadId); - return wrap; -} - -function buildRenderedView(segment, elementIds) { - const intraPageThreads = collectIntraPageThreads(segment); - const skipElements = new Set(); - /** @type {Map} */ - const mergeGroups = new Map(); - - for (const [, members] of intraPageThreads) { - mergeGroups.set(members[0], members); - for (let i = 1; i < members.length; i += 1) { - skipElements.add(members[i]); - } - } - - const root = document.createElement("article"); - root.className = "rendered-doc"; - for (const el of segment) { - if (el.nodeType !== Node.ELEMENT_NODE) continue; - if (localName(el) === "page_break") continue; - if (skipElements.has(el)) continue; - - const mergeGroup = mergeGroups.get(el); - const rendered = mergeGroup - ? renderMergedIntraPageFragments(mergeGroup, elementIds) - : renderBlockElement(el, elementIds, { inline: false }); - if (rendered) root.appendChild(rendered); - } - root.addEventListener("click", (e) => { - const elementId = resolveRenderedClickTarget(e.target); - if (elementId) selectElement(elementId); - }); - applyReadingLayerClasses(root); - return root; -} - -function wrapRendered(el, node, elementId, extraClass) { - const tag = localName(el); - const wrap = document.createElement("div"); - wrap.className = `rendered-el rendered-${tag}${extraClass ? ` ${extraClass}` : ""}`; - if (elementId) wrap.setAttribute("data-element-id", elementId); - applyElementLayerAttr(el, wrap); - wrap.appendChild(node); - return wrap; -} - -function renderBlockElement(el, elementIds, ctx) { - const tag = localName(el); - const elementId = elementIds.get(el); - - switch (tag) { - case "text": { - const p = document.createElement("p"); - appendRenderedBody(p, el, elementIds, { inline: true }); - return wrapRendered(el, p, elementId); - } - case "heading": { - const h = document.createElement(`h${headingLevel(el)}`); - appendRenderedBody(h, el, elementIds, { inline: true }); - return wrapRendered(el, h, elementId); - } - case "field_heading": { - const h = document.createElement(`h${headingLevel(el)}`); - h.className = "rendered-field-heading"; - appendRenderedBody(h, el, elementIds, { inline: true }); - return wrapRendered(el, h, elementId); - } - case "footnote": { - const aside = document.createElement("aside"); - appendRenderedBody(aside, el, elementIds, { inline: false }); - return wrapRendered(el, aside, elementId); - } - case "page_header": { - const header = document.createElement("header"); - header.className = "rendered-page-header"; - appendRenderedBody(header, el, elementIds, { inline: true }); - return wrapRendered(el, header, elementId); - } - case "page_footer": { - const footer = document.createElement("footer"); - footer.className = "rendered-page-footer"; - appendRenderedBody(footer, el, elementIds, { inline: true }); - return wrapRendered(el, footer, elementId); - } - case "list": - return renderList(el, elementIds); - case "table": - case "index": - case "tabular": - return renderOtslContainer(el, elementIds); - case "code": - return renderCode(el, elementIds, ctx); - case "formula": - return renderFormula(el, elementIds, ctx); - case "picture": - return renderPicture(el, elementIds); - case "group": { - const figure = document.createElement("figure"); - figure.className = "rendered-group"; - appendRenderedBodyBlocks(figure, el, elementIds); - const captionEl = readCaptionElement(el); - if (captionEl) { - figure.appendChild(renderEmbeddedCaption(captionEl, elementIds, "figcaption")); - } - return wrapRendered(el, figure, elementId); - } - case "field_region": { - const div = document.createElement("div"); - div.className = "rendered-field-region"; - appendRenderedBodyBlocks(div, el, elementIds); - return wrapRendered(el, div, elementId); - } - case "field_item": { - const div = document.createElement("div"); - div.className = "rendered-field-item"; - appendRenderedBodyBlocks(div, el, elementIds); - return wrapRendered(el, div, elementId); - } - default: - return renderUnsupported(el, elementIds); - } -} - -function renderUnsupported(el, elementIds) { - const stub = document.createElement("div"); - stub.className = "rendered-unsupported"; - stub.textContent = `<${localName(el)}> — not yet rendered`; - return wrapRendered(el, stub, elementIds.get(el)); -} - -function skipElementHeadNodes(nodes, startIdx) { - let i = startIdx; - while (i < nodes.length && isWhitespaceOnlyText(nodes[i])) i += 1; - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType === Node.ELEMENT_NODE && HEAD_TAGS.has(localName(node))) { - i += 1; - while (i < nodes.length && isWhitespaceOnlyText(nodes[i])) i += 1; - continue; - } - break; - } - return i; -} - -function readCaptionElement(el) { - return childElements(el).find((c) => localName(c) === "caption") ?? null; -} - -function renderEmbeddedCaption(captionEl, elementIds, tagName) { - const node = document.createElement(tagName); - node.classList.add("rendered-el", "rendered-caption"); - const elementId = elementIds.get(captionEl); - if (elementId) node.setAttribute("data-element-id", elementId); - appendRenderedBody(node, captionEl, elementIds, { inline: true }); - return node; -} - -function appendRenderedBody(parent, el, elementIds, ctx) { - const nodes = [...el.childNodes]; - let i = skipElementHeadNodes(nodes, 0); - while (i < nodes.length) { - appendRenderedNode(parent, nodes[i], elementIds, ctx); - i += 1; - } -} - -function appendRenderedBodyBlocks(parent, el, elementIds) { - const nodes = [...el.childNodes]; - let i = skipElementHeadNodes(nodes, 0); - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType === Node.ELEMENT_NODE && RENDER_BLOCK_TAGS.has(localName(node))) { - const rendered = renderBlockElement(node, elementIds, { inline: false }); - if (rendered) parent.appendChild(rendered); - } else { - appendRenderedNode(parent, node, elementIds, { inline: false }); - } - i += 1; - } -} - -function renderMarkerElement(el, elementIds, ctx) { - const marker = document.createElement("span"); - marker.className = "rendered-marker rendered-el"; - const elementId = elementIds.get(el); - if (elementId) marker.setAttribute("data-element-id", elementId); - appendRenderedBody(marker, el, elementIds, ctx); - return marker; -} - -function renderCheckboxElement(el, elementIds) { - const cb = document.createElement("input"); - cb.type = "checkbox"; - cb.disabled = true; - cb.checked = (el.getAttribute("class") ?? "unselected") === "selected"; - cb.className = "rendered-checkbox"; - - const wrap = document.createElement("span"); - wrap.className = "rendered-checkbox-wrap rendered-el"; - const elementId = elementIds.get(el); - if (elementId) wrap.setAttribute("data-element-id", elementId); - applyElementLayerAttr(el, wrap); - wrap.appendChild(cb); - return wrap; -} - -function renderFieldKeyElement(el, elementIds, ctx) { - const node = document.createElement("span"); - node.className = "rendered-field-key rendered-el"; - const elementId = elementIds.get(el); - if (elementId) node.setAttribute("data-element-id", elementId); - applyElementLayerAttr(el, node); - appendRenderedBody(node, el, elementIds, { ...ctx, inline: true }); - return node; -} - -function renderFieldValueElement(el, elementIds, ctx) { - const valueClass = el.getAttribute("class") ?? "read_only"; - const node = document.createElement("span"); - node.className = `rendered-field-value rendered-field-value-${valueClass} rendered-el`; - const elementId = elementIds.get(el); - if (elementId) node.setAttribute("data-element-id", elementId); - applyElementLayerAttr(el, node); - appendRenderedBody(node, el, elementIds, { ...ctx, inline: true }); - if ( - valueClass === "fillable" - && !node.textContent.trim() - && !node.querySelector(".rendered-checkbox-wrap, img, .rendered-marker") - ) { - const slot = document.createElement("span"); - slot.className = "rendered-field-fillable-slot"; - slot.setAttribute("aria-hidden", "true"); - node.appendChild(slot); - } - return node; -} - -function renderFieldHintElement(el, elementIds, ctx) { - const node = document.createElement("span"); - node.className = "rendered-field-hint rendered-el"; - const elementId = elementIds.get(el); - if (elementId) node.setAttribute("data-element-id", elementId); - applyElementLayerAttr(el, node); - appendRenderedBody(node, el, elementIds, { ...ctx, inline: true }); - return node; -} - -function appendRenderedNode(parent, node, elementIds, ctx) { - if (isTextLikeNode(node)) { - let text = node.textContent; - if (!text || !text.trim()) return; - if (ctx.trimLeading) { - text = text.replace(/^\s+/u, ""); - ctx.trimLeading = false; - if (!text) return; - } - parent.appendChild(document.createTextNode(text)); - return; - } - if (node.nodeType !== Node.ELEMENT_NODE) return; - - const tag = localName(node); - if (HEAD_TAGS.has(tag)) return; - - if (RENDER_FORMAT_TAGS.has(tag)) { - parent.appendChild(renderFormatElement(node, elementIds, ctx)); - return; - } - - if (tag === "code" || tag === "formula") { - const rendered = renderBlockElement(node, elementIds, { inline: true }); - if (rendered) parent.appendChild(rendered); - return; - } - - if (RENDER_BLOCK_TAGS.has(tag)) { - const rendered = renderBlockElement(node, elementIds, ctx); - if (rendered) parent.appendChild(rendered); - return; - } - - if (tag === "marker") { - parent.appendChild(renderMarkerElement(node, elementIds, ctx)); - return; - } - - if (tag === "checkbox") { - parent.appendChild(renderCheckboxElement(node, elementIds)); - return; - } - - if (tag === "key") { - parent.appendChild(renderFieldKeyElement(node, elementIds, ctx)); - return; - } - - if (tag === "value") { - parent.appendChild(renderFieldValueElement(node, elementIds, ctx)); - return; - } - - if (tag === "hint") { - parent.appendChild(renderFieldHintElement(node, elementIds, ctx)); - return; - } - - if (tag === "ldiv") return; - if (isCellToken(tag) || tag === "src" || tag === "tabular") return; - - appendRenderedBody(parent, node, elementIds, ctx); -} - -function renderFormatElement(el, elementIds, ctx) { - const tag = localName(el); - if (tag === "content") { - const span = document.createElement("span"); - span.textContent = el.textContent ?? ""; - return span; - } - - let node; - if (tag === "handwriting") { - node = document.createElement("span"); - node.className = "rendered-handwriting"; - } else if (tag === "rtl") { - node = document.createElement("bdi"); - node.setAttribute("dir", "rtl"); - } else { - node = document.createElement(FORMAT_HTML_TAG[tag] ?? "span"); - } - - appendRenderedBody(node, el, elementIds, ctx); - return node; -} - -function renderCode(el, elementIds, ctx) { - const labelEl = childElements(el).find((c) => localName(c) === "label"); - const labelValue = labelEl?.getAttribute("value"); - - const code = document.createElement("code"); - appendRenderedBody(code, el, elementIds, { inline: ctx.inline }); - - if (ctx.inline) { - code.classList.add("rendered-el"); - const id = elementIds.get(el); - if (id) code.setAttribute("data-element-id", id); - return code; - } - - const pre = document.createElement("pre"); - if (labelValue && labelValue !== "undefined") { - const label = document.createElement("span"); - label.className = "rendered-code-label"; - label.textContent = labelValue; - pre.appendChild(label); - } - pre.appendChild(code); - return wrapRendered(el, pre, elementIds.get(el)); -} - -function renderFormula(el, elementIds, ctx) { - const span = document.createElement("span"); - span.className = ctx.inline ? "rendered-formula-inline" : "rendered-formula"; - appendRenderedBody(span, el, elementIds, { inline: true }); - if (ctx.inline) { - span.classList.add("rendered-el"); - const id = elementIds.get(el); - if (id) span.setAttribute("data-element-id", id); - return span; - } - return wrapRendered(el, span, elementIds.get(el)); -} - -function markPictureUnavailable(img) { - img.classList.add("rendered-picture-unavailable"); - img.alt = "\u00A0"; - img.setAttribute("aria-label", PICTURE_UNAVAILABLE_ALT); -} - -function appendPictureFigureImage(figure, uri, captionEl, elementIds) { - const img = document.createElement("img"); - figure.appendChild(img); - - const resolved = uri ? resolveArchiveUri(uri) : null; - if (resolved) { - img.alt = ""; - img.src = resolved; - img.addEventListener("error", () => markPictureUnavailable(img), { once: true }); - } else { - markPictureUnavailable(img); - img.src = INVALID_PICTURE_SRC; - } - - if (captionEl) { - figure.appendChild(renderEmbeddedCaption(captionEl, elementIds, "figcaption")); - } -} - -function renderPicture(el, elementIds) { - const figure = document.createElement("figure"); - const captionEl = readCaptionElement(el); - const srcEl = childElements(el).find((c) => localName(c) === "src") ?? null; - const uri = srcEl?.getAttribute("uri")?.trim() || null; - - appendPictureFigureImage(figure, uri, captionEl, elementIds); - - const nodes = [...el.childNodes]; - let i = skipElementHeadNodes(nodes, 0); - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType !== Node.ELEMENT_NODE) { - i += 1; - continue; - } - const tag = localName(node); - if (tag === "src") { - i += 1; - continue; - } - if (tag === "tabular") { - const rendered = renderOtslContainer(node, elementIds); - if (rendered) figure.appendChild(rendered); - i += 1; - continue; - } - break; - } - - const bodyInner = document.createElement("div"); - bodyInner.className = "rendered-picture-contents-body"; - appendPictureBodyContent(bodyInner, nodes, i, elementIds); - if (bodyInner.textContent.trim()) { - const details = document.createElement("details"); - details.className = "rendered-picture-contents"; - const summary = document.createElement("summary"); - summary.textContent = "Picture contents"; - details.appendChild(summary); - details.appendChild(bodyInner); - figure.appendChild(details); - } - - return wrapRendered(el, figure, elementIds.get(el)); -} - -function appendPictureBodyContent(container, nodes, startIdx, elementIds) { - let i = startIdx; - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType === Node.ELEMENT_NODE && RENDER_BLOCK_TAGS.has(localName(node))) { - const rendered = renderBlockElement(node, elementIds, { inline: false }); - if (rendered) container.appendChild(rendered); - } else { - appendRenderedNode(container, node, elementIds, { inline: false }); - } - i += 1; - } -} - -function renderVirtualTextBlock(hostEl, contentNodes, elementIds) { - const hasBlock = contentNodes.some( - (n) => n.nodeType === Node.ELEMENT_NODE && RENDER_BLOCK_TAGS.has(localName(n)), - ); - const inner = document.createElement(hasBlock ? "div" : "p"); - for (const node of contentNodes) { - if (node.nodeType === Node.ELEMENT_NODE && RENDER_BLOCK_TAGS.has(localName(node))) { - const rendered = renderBlockElement(node, elementIds, { inline: false }); - if (rendered) inner.appendChild(rendered); - } else { - appendRenderedNode(inner, node, elementIds, { inline: true }); - } - } - const wrap = document.createElement("div"); - wrap.className = "rendered-el rendered-text rendered-el-virtual-text"; - const elementId = elementIds.get(hostEl); - if (elementId) wrap.setAttribute("data-element-id", elementId); - applyElementLayerAttr(hostEl, wrap); - wrap.appendChild(inner); - return wrap; -} - -function appendListItemsFromElement(list, el, elementIds) { - const nodes = [...el.childNodes]; - let i = skipContainerLevelHead(nodes, 0); - - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType !== Node.ELEMENT_NODE || localName(node) !== "ldiv") { - i += 1; - continue; - } - - const ldiv = node; - i += 1; - const li = document.createElement("li"); - - for (const child of childElements(ldiv)) { - const childTag = localName(child); - if (childTag === "marker") { - li.appendChild(renderMarkerElement(child, elementIds, { inline: true })); - } else if (childTag === "checkbox") { - li.appendChild(renderCheckboxElement(child, elementIds)); - } - } - - const contentStart = i; - const head = parseElementHeadAt(nodes, i); - if (head) i = head.nextIndex; - - while (i < nodes.length) { - const contentNode = nodes[i]; - if (contentNode.nodeType === Node.ELEMENT_NODE && localName(contentNode) === "ldiv") break; - i += 1; - } - - const contentNodes = nodes.slice(contentStart, i); - appendRenderedSliceContent(li, ldiv, contentNodes, elementIds); - - list.appendChild(li); - } -} - -function renderList(el, elementIds) { - const listClass = el.getAttribute("class") ?? "unordered"; - const list = document.createElement(listClass === "ordered" ? "ol" : "ul"); - appendListItemsFromElement(list, el, elementIds); - return wrapRendered(el, list, elementIds.get(el)); -} - -function isHeaderCellKind(kind) { - return kind === "ched" || kind === "rhed" || kind === "corn" || kind === "srow"; -} - -function skipOtslContainerHead(nodes, startIdx) { - let i = startIdx; - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType !== Node.ELEMENT_NODE) { - i += 1; - continue; - } - const tag = localName(node); - if (isCellToken(tag)) break; - if (HEAD_TAGS.has(tag) || tag === "h_thread" || tag === "location") { - i += 1; - continue; - } - break; - } - return i; -} - -/** @returns {{ kind: string, token: Element, contentNodes: Node[] }[][]} */ -function parseOtslRows(container) { - const nodes = [...container.childNodes]; - let i = skipOtslContainerHead(nodes, 0); - /** @type {{ kind: string, token: Element, contentNodes: Node[] }[][]} */ - const rows = []; - /** @type {{ kind: string, token: Element, contentNodes: Node[] }[]} */ - let currentRow = []; - - while (i < nodes.length) { - const node = nodes[i]; - if (node.nodeType !== Node.ELEMENT_NODE) { - i += 1; - continue; - } - const tag = localName(node); - if (tag === "nl") { - rows.push(currentRow); - currentRow = []; - i += 1; - continue; - } - if (!isCellToken(tag)) { - i += 1; - continue; - } - - if (CELL_SPAN_TAGS.has(tag)) { - currentRow.push({ kind: tag, token: node, contentNodes: [] }); - i += 1; - continue; - } - - i += 1; - const head = parseElementHeadAt(nodes, i); - if (head) i = head.nextIndex; - const contentStart = i; - i = skipUntilCellBoundary(nodes, i); - currentRow.push({ kind: tag, token: node, contentNodes: nodes.slice(contentStart, i) }); - } - - if (currentRow.length) rows.push(currentRow); - return rows; -} - -function findVerticalCellOrigin(grid, row, col) { - for (let r = row - 1; r >= 0; r -= 1) { - const cell = grid[r]?.[col]; - if (!cell || cell.covered) continue; - return { cell, row: r, col }; - } - return null; -} - -function findHorizontalCellOrigin(grid, row, col) { - for (let c = col - 1; c >= 0; c -= 1) { - const cell = grid[row]?.[c]; - if (!cell || cell.covered) continue; - return { cell, row, col: c }; - } - return null; -} - -function nextFreeColumn(grid, row, col) { - let c = col; - while (grid[row]?.[c]?.covered) c += 1; - return c; -} - -/** @returns {{ kind: string, token: Element, contentNodes: Node[], colspan: number, rowspan: number, covered?: boolean }[][]} */ -function buildOtslGrid(rows) { - /** @type {{ kind: string, token: Element, contentNodes: Node[], colspan: number, rowspan: number, covered?: boolean }[][]} */ - const grid = []; - - for (let rowIdx = 0; rowIdx < rows.length; rowIdx += 1) { - if (!grid[rowIdx]) grid[rowIdx] = []; - let col = 0; - - for (const parsed of rows[rowIdx]) { - col = nextFreeColumn(grid, rowIdx, col); - - if (parsed.kind === "lcel") { - const origin = findHorizontalCellOrigin(grid, rowIdx, col); - if (origin) origin.cell.colspan += 1; - grid[rowIdx][col] = { kind: "lcel", token: parsed.token, contentNodes: [], colspan: 0, rowspan: 0, covered: true }; - col += 1; - continue; - } - - if (parsed.kind === "ucel") { - const origin = findVerticalCellOrigin(grid, rowIdx, col); - if (origin) origin.cell.rowspan += 1; - grid[rowIdx][col] = { kind: "ucel", token: parsed.token, contentNodes: [], colspan: 0, rowspan: 0, covered: true }; - col += 1; - continue; - } - - if (parsed.kind === "xcel") { - const vOrigin = findVerticalCellOrigin(grid, rowIdx, col); - const hOrigin = findHorizontalCellOrigin(grid, rowIdx, col); - if (vOrigin && hOrigin && vOrigin.cell === hOrigin.cell) { - vOrigin.cell.rowspan += 1; - vOrigin.cell.colspan += 1; - } else { - if (vOrigin) vOrigin.cell.rowspan += 1; - if (hOrigin) hOrigin.cell.colspan += 1; - } - grid[rowIdx][col] = { kind: "xcel", token: parsed.token, contentNodes: [], colspan: 0, rowspan: 0, covered: true }; - col += 1; - continue; - } - - grid[rowIdx][col] = { - kind: parsed.kind, - token: parsed.token, - contentNodes: parsed.contentNodes, - colspan: 1, - rowspan: 1, - covered: false, - }; - col += 1; - } - } - - return grid; -} - -function appendTableCellContent(container, nodes, elementIds, cellToken) { - appendRenderedSliceContent(container, cellToken, nodes, elementIds); -} - -function renderOtslContainer(el, elementIds) { - const table = document.createElement("table"); - table.className = "rendered-table"; - - const captionEl = readCaptionElement(el); - if (captionEl) { - table.appendChild(renderEmbeddedCaption(captionEl, elementIds, "caption")); - } - - const grid = buildOtslGrid(parseOtslRows(el)); - const tbody = document.createElement("tbody"); - - for (const row of grid) { - const tr = document.createElement("tr"); - for (const cell of row) { - if (!cell || cell.covered) continue; - const cellTag = isHeaderCellKind(cell.kind) ? "th" : "td"; - const td = document.createElement(cellTag); - if (cell.colspan > 1) td.colSpan = cell.colspan; - if (cell.rowspan > 1) td.rowSpan = cell.rowspan; - appendTableCellContent(td, cell.contentNodes, elementIds, cell.token); - tr.appendChild(td); - } - if (tr.childNodes.length) tbody.appendChild(tr); - } - - if (tbody.childNodes.length) table.appendChild(tbody); - return wrapRendered(el, table, elementIds.get(el)); -} - -function serializeSegment(segment) { - return segment.map((el) => serializeElement(el, 0)).join("\n"); -} - -function serializeElement(el, depth) { - const pad = " ".repeat(depth); - const tag = localName(el); - const attrs = [...el.attributes] - .filter((a) => a.name !== "xmlns" || a.value !== DOCLANG_NS) - .map((a) => `${a.name}="${a.value}"`) - .join(" "); - const attrStr = attrs ? ` ${attrs}` : ""; - - if (!el.childNodes.length) return `${pad}<${tag}${attrStr}/>`; - - const meaningfulText = [...el.childNodes].filter((n) => isTextLikeNode(n) && !isWhitespaceOnlyText(n)); - const textOnly = meaningfulText.length > 0 && meaningfulText.every(isTextLikeNode) && !childElements(el).length; - if (textOnly) { - const text = serializeMarkupTextNodes(el.childNodes); - if (text) { - return `${pad}<${tag}${attrStr}>${text}`; - } - } - - const parts = [`${pad}<${tag}${attrStr}>`]; - for (const child of el.childNodes) { - if (isTextLikeNode(child)) { - const text = formatMarkupTextNode(child); - if (text) parts.push(" ".repeat(depth + 1) + text); - } else if (child.nodeType === Node.ELEMENT_NODE) { - parts.push(serializeElement(child, depth + 1)); - } - } - parts.push(`${pad}`); - return parts.join("\n"); -} - -function childElements(el) { - return [...el.children]; -} - -function localName(el) { - return el.localName || el.tagName.replace(/^.*:/, ""); -} - -function normalizeArchivePath(path) { - return path.replace(/\\/g, "/").replace(/^\.\//, ""); -} - -function archiveRelativeAssetPath(path) { - const norm = normalizeArchivePath(path); - const idx = norm.indexOf("assets/"); - return idx === -1 ? null : norm.slice(idx); -} - -function mimeFromAssetPath(path) { - const ext = path.split(".").pop()?.toLowerCase() ?? ""; - switch (ext) { - case "png": - return "image/png"; - case "jpg": - case "jpeg": - return "image/jpeg"; - case "webp": - return "image/webp"; - case "gif": - return "image/gif"; - case "svg": - return "image/svg+xml"; - default: - return "application/octet-stream"; - } -} - -/** - * Resolve a picture @uri to a safe img src. - * Only archive asset blob URLs and small raster data: URIs are allowed. - * Remote, protocol-relative, blob:, and unresolved relative URIs are rejected. - * @returns {string | null} - */ -function resolveArchiveUri(uri) { - if (!uri) return null; - const trimmed = uri.trim(); - if (!trimmed) return null; - - if (SAFE_DATA_IMAGE_RE.test(trimmed)) { - return trimmed.length <= MAX_DATA_IMAGE_URI_LENGTH ? trimmed : null; - } - - // Reject any other scheme (http, https, blob, javascript, …) and protocol-relative URLs. - if (/^[a-z][a-z0-9+.-]*:/i.test(trimmed) || trimmed.startsWith("//")) return null; - - return state?.assetUrls?.get(normalizeArchivePath(trimmed)) ?? null; -} - -function revokeDocumentState(docState) { - if (!docState) return; - for (const url of docState.pageImages.values()) { - if (url.startsWith("blob:")) URL.revokeObjectURL(url); - } - for (const url of docState.assetUrls.values()) { - if (url.startsWith("blob:")) URL.revokeObjectURL(url); - } -} - -function revokeArchiveUrls() { - revokeDocumentState(state); -} - -async function extractArchiveFromZipBuffer(buffer) { - const entries = await unzip(buffer); - const markupEntry = findArchiveEntry(entries, "document.xml"); - if (!markupEntry) { - throw new Error("Archive must contain document.xml"); - } - const markupXml = new TextDecoder().decode(markupEntry.data); - const pageImages = new Map(); - const assetUrls = new Map(); - for (const e of entries) { - const m = e.name.match(/^pages\/(\d+)\.(png|jpe?g|webp)$/i); - if (m) { - const ext = m[2].toLowerCase().replace("jpeg", "jpg"); - const mime = ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg"; - pageImages.set(Number(m[1]), URL.createObjectURL(new Blob([e.data], { type: mime }))); - continue; - } - if (e.name.startsWith("assets/") && !e.name.endsWith("/")) { - assetUrls.set( - e.name, - URL.createObjectURL(new Blob([e.data], { type: mimeFromAssetPath(e.name) })), - ); - } - } - return { markupXml, pageImages, assetUrls }; -} - -async function unzip(buffer, { shouldExtract } = {}) { - if (typeof DecompressionStream === "undefined") { - throw new Error("ZIP decompression requires a browser with DecompressionStream support"); - } - - const bytes = new Uint8Array(buffer); - const view = new DataView(buffer); - const eocdOffset = findEndOfCentralDirectory(bytes); - const entryCount = view.getUint16(eocdOffset + 10, true); - const centralDirOffset = view.getUint32(eocdOffset + 16, true); - - if (entryCount > ZIP_MAX_ENTRIES) { - throw new Error("ZIP archive has too many entries"); - } - if (centralDirOffset >= bytes.length) { - throw new Error("Invalid ZIP central directory offset"); - } - - /** @type {{ name: string, data: Uint8Array }[]} */ - const entries = []; - let offset = centralDirOffset; - let totalUncompressed = 0; - - for (let i = 0; i < entryCount; i += 1) { - if (offset + 46 > bytes.length || view.getUint32(offset, true) !== 0x02014b50) { - throw new Error("Invalid ZIP central directory"); - } - - const compression = view.getUint16(offset + 10, true); - const compressedSize = view.getUint32(offset + 20, true); - const uncompressedSize = view.getUint32(offset + 24, true); - const nameLength = view.getUint16(offset + 28, true); - const extraLength = view.getUint16(offset + 30, true); - const commentLength = view.getUint16(offset + 32, true); - const localHeaderOffset = view.getUint32(offset + 42, true); - const nameEnd = offset + 46 + nameLength; - if (nameEnd > bytes.length) { - throw new Error("Invalid ZIP entry name"); - } - const rawName = new TextDecoder().decode(bytes.subarray(offset + 46, nameEnd)); - const name = normalizeZipEntryName(rawName); - - offset = nameEnd + extraLength + commentLength; - if (offset > bytes.length) { - throw new Error("Invalid ZIP central directory"); - } - - if (!name || name.endsWith("/") || isIgnoredArchiveEntry(name)) continue; - if (shouldExtract && !shouldExtract(name)) continue; - - if (compressedSize > ZIP_MAX_ENTRY_COMPRESSED_BYTES || uncompressedSize > ZIP_MAX_ENTRY_UNCOMPRESSED_BYTES) { - throw new Error(`ZIP entry exceeds size limit: ${name}`); - } - if ( - compressedSize > 0 && - uncompressedSize > 0 && - uncompressedSize / compressedSize > ZIP_MAX_COMPRESSION_RATIO - ) { - throw new Error(`ZIP entry compression ratio exceeds limit: ${name}`); - } - if (totalUncompressed + uncompressedSize > ZIP_MAX_TOTAL_UNCOMPRESSED_BYTES) { - throw new Error("ZIP archive exceeds total uncompressed size limit"); - } - - if (localHeaderOffset + 30 > bytes.length || view.getUint32(localHeaderOffset, true) !== 0x04034b50) { - throw new Error(`Invalid ZIP local header: ${name}`); - } - const localNameLength = view.getUint16(localHeaderOffset + 26, true); - const localExtraLength = view.getUint16(localHeaderOffset + 28, true); - const dataOffset = localHeaderOffset + 30 + localNameLength + localExtraLength; - if (dataOffset + compressedSize > bytes.length) { - throw new Error(`ZIP entry data out of bounds: ${name}`); - } - const compressed = bytes.subarray(dataOffset, dataOffset + compressedSize); - const data = await decompressZipEntry(compressed, compression, uncompressedSize); - totalUncompressed += data.length; - if (totalUncompressed > ZIP_MAX_TOTAL_UNCOMPRESSED_BYTES) { - throw new Error("ZIP archive exceeds total uncompressed size limit"); - } - entries.push({ name, data }); - } - - return entries; -} - -function findEndOfCentralDirectory(bytes) { - for (let i = bytes.length - 22; i >= 0; i -= 1) { - if ( - bytes[i] === 0x50 && - bytes[i + 1] === 0x4b && - bytes[i + 2] === 0x05 && - bytes[i + 3] === 0x06 - ) { - return i; - } - } - throw new Error("Invalid ZIP archive"); -} - -function concatUint8Arrays(chunks, totalLength) { - const out = new Uint8Array(totalLength); - let offset = 0; - for (const chunk of chunks) { - out.set(chunk, offset); - offset += chunk.byteLength; - } - return out; -} - -async function decompressZipEntry(data, method, uncompressedSize) { - if (method === 0) { - if (data.length > ZIP_MAX_ENTRY_UNCOMPRESSED_BYTES) { - throw new Error("ZIP entry exceeds size limit"); - } - return data; - } - if (method === 8) { - const stream = new Blob([data]).stream().pipeThrough(new DecompressionStream("deflate-raw")); - const reader = stream.getReader(); - const chunks = []; - let total = 0; - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - total += value.byteLength; - if (total > ZIP_MAX_ENTRY_UNCOMPRESSED_BYTES) { - throw new Error("ZIP entry exceeds size limit"); - } - if (data.length > 0 && total / data.length > ZIP_MAX_COMPRESSION_RATIO) { - throw new Error("ZIP entry compression ratio exceeds limit"); - } - chunks.push(value); - } - } catch (err) { - try { - await reader.cancel(); - } catch { - /* ignore cancel errors */ - } - throw err; - } - - const out = concatUint8Arrays(chunks, total); - if (uncompressedSize && out.length !== uncompressedSize) { - return out.slice(0, Math.min(out.length, uncompressedSize)); - } - return out; - } - throw new Error(`Unsupported ZIP compression method ${method}`); -} - -function normalizeZipEntryName(name) { - return name.replace(/\\/g, "/").replace(/^\.\//, ""); -} - -/** Match archive root files by exact name; skip macOS metadata entries. */ -function findArchiveEntry(entries, fileName) { - return entries.find((e) => e.name === fileName); -} - -function isIgnoredArchiveEntry(name) { - if (name === ".DS_Store" || name.endsWith("/.DS_Store")) return true; - return name.split("/").some((part) => part.startsWith("._") || part === "__MACOSX"); -} +(function() { + //#region \0rolldown/runtime.js + var __defProp = Object.defineProperty; + var __esmMin = (fn, res, err) => () => { + if (err) throw err[0]; + try { + return fn && (res = fn(fn = 0)), res; + } catch (e) { + throw err = [e], e; + } + }; + var __exportAll = (all, no_symbols) => { + let target = {}; + for (var name in all) __defProp(target, name, { + get: all[name], + enumerable: true + }); + if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" }); + return target; + }; + //#endregion + //#region node_modules/@lit/reactive-element/css-tag.js + /** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + var t$4 = globalThis; + var e$7 = t$4.ShadowRoot && (void 0 === t$4.ShadyCSS || t$4.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype; + var s$4 = Symbol(); + var o$8 = /* @__PURE__ */ new WeakMap(); + var n$7 = class { + constructor(t, e, o) { + if (this._$cssResult$ = !0, o !== s$4) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); + this.cssText = t, this.t = e; + } + get styleSheet() { + let t = this.o; + const s = this.t; + if (e$7 && void 0 === t) { + const e = void 0 !== s && 1 === s.length; + e && (t = o$8.get(s)), void 0 === t && ((this.o = t = new CSSStyleSheet()).replaceSync(this.cssText), e && o$8.set(s, t)); + } + return t; + } + toString() { + return this.cssText; + } + }; + var r$6 = (t) => new n$7("string" == typeof t ? t : t + "", void 0, s$4); + var S$1 = (s, o) => { + if (e$7) s.adoptedStyleSheets = o.map((t) => t instanceof CSSStyleSheet ? t : t.styleSheet); + else for (const e of o) { + const o = document.createElement("style"), n = t$4.litNonce; + void 0 !== n && o.setAttribute("nonce", n), o.textContent = e.cssText, s.appendChild(o); + } + }; + var c$5 = e$7 ? (t) => t : (t) => t instanceof CSSStyleSheet ? ((t) => { + let e = ""; + for (const s of t.cssRules) e += s.cssText; + return r$6(e); + })(t) : t; + //#endregion + //#region node_modules/@lit/reactive-element/reactive-element.js + /** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ var { is: i$5, defineProperty: e$6, getOwnPropertyDescriptor: h$4, getOwnPropertyNames: r$5, getOwnPropertySymbols: o$7, getPrototypeOf: n$6 } = Object, a$1 = globalThis, c$4 = a$1.trustedTypes, l$2 = c$4 ? c$4.emptyScript : "", p$2 = a$1.reactiveElementPolyfillSupport, d$2 = (t, s) => t, u$3 = { + toAttribute(t, s) { + switch (s) { + case Boolean: + t = t ? l$2 : null; + break; + case Object: + case Array: t = null == t ? t : JSON.stringify(t); + } + return t; + }, + fromAttribute(t, s) { + let i = t; + switch (s) { + case Boolean: + i = null !== t; + break; + case Number: + i = null === t ? null : Number(t); + break; + case Object: + case Array: try { + i = JSON.parse(t); + } catch (t) { + i = null; + } + } + return i; + } + }, f$3 = (t, s) => !i$5(t, s), b$1 = { + attribute: !0, + type: String, + converter: u$3, + reflect: !1, + useDefault: !1, + hasChanged: f$3 + }; + Symbol.metadata ??= Symbol("metadata"), a$1.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap(); + var y$1 = class extends HTMLElement { + static addInitializer(t) { + this._$Ei(), (this.l ??= []).push(t); + } + static get observedAttributes() { + return this.finalize(), this._$Eh && [...this._$Eh.keys()]; + } + static createProperty(t, s = b$1) { + if (s.state && (s.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(t) && ((s = Object.create(s)).wrapped = !0), this.elementProperties.set(t, s), !s.noAccessor) { + const i = Symbol(), h = this.getPropertyDescriptor(t, i, s); + void 0 !== h && e$6(this.prototype, t, h); + } + } + static getPropertyDescriptor(t, s, i) { + const { get: e, set: r } = h$4(this.prototype, t) ?? { + get() { + return this[s]; + }, + set(t) { + this[s] = t; + } + }; + return { + get: e, + set(s) { + const h = e?.call(this); + r?.call(this, s), this.requestUpdate(t, h, i); + }, + configurable: !0, + enumerable: !0 + }; + } + static getPropertyOptions(t) { + return this.elementProperties.get(t) ?? b$1; + } + static _$Ei() { + if (this.hasOwnProperty(d$2("elementProperties"))) return; + const t = n$6(this); + t.finalize(), void 0 !== t.l && (this.l = [...t.l]), this.elementProperties = new Map(t.elementProperties); + } + static finalize() { + if (this.hasOwnProperty(d$2("finalized"))) return; + if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(d$2("properties"))) { + const t = this.properties, s = [...r$5(t), ...o$7(t)]; + for (const i of s) this.createProperty(i, t[i]); + } + const t = this[Symbol.metadata]; + if (null !== t) { + const s = litPropertyMetadata.get(t); + if (void 0 !== s) for (const [t, i] of s) this.elementProperties.set(t, i); + } + this._$Eh = /* @__PURE__ */ new Map(); + for (const [t, s] of this.elementProperties) { + const i = this._$Eu(t, s); + void 0 !== i && this._$Eh.set(i, t); + } + this.elementStyles = this.finalizeStyles(this.styles); + } + static finalizeStyles(s) { + const i = []; + if (Array.isArray(s)) { + const e = new Set(s.flat(1 / 0).reverse()); + for (const s of e) i.unshift(c$5(s)); + } else void 0 !== s && i.push(c$5(s)); + return i; + } + static _$Eu(t, s) { + const i = s.attribute; + return !1 === i ? void 0 : "string" == typeof i ? i : "string" == typeof t ? t.toLowerCase() : void 0; + } + constructor() { + super(), this._$Ep = void 0, this.isUpdatePending = !1, this.hasUpdated = !1, this._$Em = null, this._$Ev(); + } + _$Ev() { + this._$ES = new Promise((t) => this.enableUpdating = t), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t) => t(this)); + } + addController(t) { + (this._$EO ??= /* @__PURE__ */ new Set()).add(t), void 0 !== this.renderRoot && this.isConnected && t.hostConnected?.(); + } + removeController(t) { + this._$EO?.delete(t); + } + _$E_() { + const t = /* @__PURE__ */ new Map(), s = this.constructor.elementProperties; + for (const i of s.keys()) this.hasOwnProperty(i) && (t.set(i, this[i]), delete this[i]); + t.size > 0 && (this._$Ep = t); + } + createRenderRoot() { + const t = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); + return S$1(t, this.constructor.elementStyles), t; + } + connectedCallback() { + this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(!0), this._$EO?.forEach((t) => t.hostConnected?.()); + } + enableUpdating(t) {} + disconnectedCallback() { + this._$EO?.forEach((t) => t.hostDisconnected?.()); + } + attributeChangedCallback(t, s, i) { + this._$AK(t, i); + } + _$ET(t, s) { + const i = this.constructor.elementProperties.get(t), e = this.constructor._$Eu(t, i); + if (void 0 !== e && !0 === i.reflect) { + const h = (void 0 !== i.converter?.toAttribute ? i.converter : u$3).toAttribute(s, i.type); + this._$Em = t, null == h ? this.removeAttribute(e) : this.setAttribute(e, h), this._$Em = null; + } + } + _$AK(t, s) { + const i = this.constructor, e = i._$Eh.get(t); + if (void 0 !== e && this._$Em !== e) { + const t = i.getPropertyOptions(e), h = "function" == typeof t.converter ? { fromAttribute: t.converter } : void 0 !== t.converter?.fromAttribute ? t.converter : u$3; + this._$Em = e; + const r = h.fromAttribute(s, t.type); + this[e] = r ?? this._$Ej?.get(e) ?? r, this._$Em = null; + } + } + requestUpdate(t, s, i, e = !1, h) { + if (void 0 !== t) { + const r = this.constructor; + if (!1 === e && (h = this[t]), i ??= r.getPropertyOptions(t), !((i.hasChanged ?? f$3)(h, s) || i.useDefault && i.reflect && h === this._$Ej?.get(t) && !this.hasAttribute(r._$Eu(t, i)))) return; + this.C(t, s, i); + } + !1 === this.isUpdatePending && (this._$ES = this._$EP()); + } + C(t, s, { useDefault: i, reflect: e, wrapped: h }, r) { + i && !(this._$Ej ??= /* @__PURE__ */ new Map()).has(t) && (this._$Ej.set(t, r ?? s ?? this[t]), !0 !== h || void 0 !== r) || (this._$AL.has(t) || (this.hasUpdated || i || (s = void 0), this._$AL.set(t, s)), !0 === e && this._$Em !== t && (this._$Eq ??= /* @__PURE__ */ new Set()).add(t)); + } + async _$EP() { + this.isUpdatePending = !0; + try { + await this._$ES; + } catch (t) { + Promise.reject(t); + } + const t = this.scheduleUpdate(); + return null != t && await t, !this.isUpdatePending; + } + scheduleUpdate() { + return this.performUpdate(); + } + performUpdate() { + if (!this.isUpdatePending) return; + if (!this.hasUpdated) { + if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) { + for (const [t, s] of this._$Ep) this[t] = s; + this._$Ep = void 0; + } + const t = this.constructor.elementProperties; + if (t.size > 0) for (const [s, i] of t) { + const { wrapped: t } = i, e = this[s]; + !0 !== t || this._$AL.has(s) || void 0 === e || this.C(s, void 0, i, e); + } + } + let t = !1; + const s = this._$AL; + try { + t = this.shouldUpdate(s), t ? (this.willUpdate(s), this._$EO?.forEach((t) => t.hostUpdate?.()), this.update(s)) : this._$EM(); + } catch (s) { + throw t = !1, this._$EM(), s; + } + t && this._$AE(s); + } + willUpdate(t) {} + _$AE(t) { + this._$EO?.forEach((t) => t.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = !0, this.firstUpdated(t)), this.updated(t); + } + _$EM() { + this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = !1; + } + get updateComplete() { + return this.getUpdateComplete(); + } + getUpdateComplete() { + return this._$ES; + } + shouldUpdate(t) { + return !0; + } + update(t) { + this._$Eq &&= this._$Eq.forEach((t) => this._$ET(t, this[t])), this._$EM(); + } + updated(t) {} + firstUpdated(t) {} + }; + y$1.elementStyles = [], y$1.shadowRootOptions = { mode: "open" }, y$1[d$2("elementProperties")] = /* @__PURE__ */ new Map(), y$1[d$2("finalized")] = /* @__PURE__ */ new Map(), p$2?.({ ReactiveElement: y$1 }), (a$1.reactiveElementVersions ??= []).push("2.1.2"); + //#endregion + //#region node_modules/lit-html/lit-html.js + /** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + var t$3 = globalThis; + var i$4 = (t) => t; + var s$3 = t$3.trustedTypes; + var e$5 = s$3 ? s$3.createPolicy("lit-html", { createHTML: (t) => t }) : void 0; + var h$3 = "$lit$"; + var o$6 = `lit$${Math.random().toFixed(9).slice(2)}$`; + var n$5 = "?" + o$6; + var r$4 = `<${n$5}>`; + var l$1 = document; + var c$3 = () => l$1.createComment(""); + var a = (t) => null === t || "object" != typeof t && "function" != typeof t; + var u$2 = Array.isArray; + var d$1 = (t) => u$2(t) || "function" == typeof t?.[Symbol.iterator]; + var f$2 = "[ \n\f\r]"; + var v$1 = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g; + var _ = /-->/g; + var m$1 = />/g; + var p$1 = RegExp(`>|${f$2}(?:([^\\s"'>=/]+)(${f$2}*=${f$2}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`, "g"); + var g = /'/g; + var $ = /"/g; + var y = /^(?:script|style|textarea|title)$/i; + var x = (t) => (i, ...s) => ({ + _$litType$: t, + strings: i, + values: s + }); + var b = x(1); + var E = Symbol.for("lit-noChange"); + var A = Symbol.for("lit-nothing"); + var C = /* @__PURE__ */ new WeakMap(); + var P = l$1.createTreeWalker(l$1, 129); + function V(t, i) { + if (!u$2(t) || !t.hasOwnProperty("raw")) throw Error("invalid template strings array"); + return void 0 !== e$5 ? e$5.createHTML(i) : i; + } + var N = (t, i) => { + const s = t.length - 1, e = []; + let n, l = 2 === i ? "" : 3 === i ? "" : "", c = v$1; + for (let i = 0; i < s; i++) { + const s = t[i]; + let a, u, d = -1, f = 0; + for (; f < s.length && (c.lastIndex = f, u = c.exec(s), null !== u);) f = c.lastIndex, c === v$1 ? "!--" === u[1] ? c = _ : void 0 !== u[1] ? c = m$1 : void 0 !== u[2] ? (y.test(u[2]) && (n = RegExp("" === u[0] ? (c = n ?? v$1, d = -1) : void 0 === u[1] ? d = -2 : (d = c.lastIndex - u[2].length, a = u[1], c = void 0 === u[3] ? p$1 : "\"" === u[3] ? $ : g) : c === $ || c === g ? c = p$1 : c === _ || c === m$1 ? c = v$1 : (c = p$1, n = void 0); + const x = c === p$1 && t[i + 1].startsWith("/>") ? " " : ""; + l += c === v$1 ? s + r$4 : d >= 0 ? (e.push(a), s.slice(0, d) + h$3 + s.slice(d) + o$6 + x) : s + o$6 + (-2 === d ? i : x); + } + return [V(t, l + (t[s] || "") + (2 === i ? "" : 3 === i ? "" : "")), e]; + }; + var S = class S { + constructor({ strings: t, _$litType$: i }, e) { + let r; + this.parts = []; + let l = 0, a = 0; + const u = t.length - 1, d = this.parts, [f, v] = N(t, i); + if (this.el = S.createElement(f, e), P.currentNode = this.el.content, 2 === i || 3 === i) { + const t = this.el.content.firstChild; + t.replaceWith(...t.childNodes); + } + for (; null !== (r = P.nextNode()) && d.length < u;) { + if (1 === r.nodeType) { + if (r.hasAttributes()) for (const t of r.getAttributeNames()) if (t.endsWith(h$3)) { + const i = v[a++], s = r.getAttribute(t).split(o$6), e = /([.?@])?(.*)/.exec(i); + d.push({ + type: 1, + index: l, + name: e[2], + strings: s, + ctor: "." === e[1] ? I : "?" === e[1] ? L : "@" === e[1] ? z : H + }), r.removeAttribute(t); + } else t.startsWith(o$6) && (d.push({ + type: 6, + index: l + }), r.removeAttribute(t)); + if (y.test(r.tagName)) { + const t = r.textContent.split(o$6), i = t.length - 1; + if (i > 0) { + r.textContent = s$3 ? s$3.emptyScript : ""; + for (let s = 0; s < i; s++) r.append(t[s], c$3()), P.nextNode(), d.push({ + type: 2, + index: ++l + }); + r.append(t[i], c$3()); + } + } + } else if (8 === r.nodeType) if (r.data === n$5) d.push({ + type: 2, + index: l + }); + else { + let t = -1; + for (; -1 !== (t = r.data.indexOf(o$6, t + 1));) d.push({ + type: 7, + index: l + }), t += o$6.length - 1; + } + l++; + } + } + static createElement(t, i) { + const s = l$1.createElement("template"); + return s.innerHTML = t, s; + } + }; + function M$1(t, i, s = t, e) { + if (i === E) return i; + let h = void 0 !== e ? s._$Co?.[e] : s._$Cl; + const o = a(i) ? void 0 : i._$litDirective$; + return h?.constructor !== o && (h?._$AO?.(!1), void 0 === o ? h = void 0 : (h = new o(t), h._$AT(t, s, e)), void 0 !== e ? (s._$Co ??= [])[e] = h : s._$Cl = h), void 0 !== h && (i = M$1(t, h._$AS(t, i.values), h, e)), i; + } + var R = class { + constructor(t, i) { + this._$AV = [], this._$AN = void 0, this._$AD = t, this._$AM = i; + } + get parentNode() { + return this._$AM.parentNode; + } + get _$AU() { + return this._$AM._$AU; + } + u(t) { + const { el: { content: i }, parts: s } = this._$AD, e = (t?.creationScope ?? l$1).importNode(i, !0); + P.currentNode = e; + let h = P.nextNode(), o = 0, n = 0, r = s[0]; + for (; void 0 !== r;) { + if (o === r.index) { + let i; + 2 === r.type ? i = new k(h, h.nextSibling, this, t) : 1 === r.type ? i = new r.ctor(h, r.name, r.strings, this, t) : 6 === r.type && (i = new Z(h, this, t)), this._$AV.push(i), r = s[++n]; + } + o !== r?.index && (h = P.nextNode(), o++); + } + return P.currentNode = l$1, e; + } + p(t) { + let i = 0; + for (const s of this._$AV) void 0 !== s && (void 0 !== s.strings ? (s._$AI(t, s, i), i += s.strings.length - 2) : s._$AI(t[i])), i++; + } + }; + var k = class k { + get _$AU() { + return this._$AM?._$AU ?? this._$Cv; + } + constructor(t, i, s, e) { + this.type = 2, this._$AH = A, this._$AN = void 0, this._$AA = t, this._$AB = i, this._$AM = s, this.options = e, this._$Cv = e?.isConnected ?? !0; + } + get parentNode() { + let t = this._$AA.parentNode; + const i = this._$AM; + return void 0 !== i && 11 === t?.nodeType && (t = i.parentNode), t; + } + get startNode() { + return this._$AA; + } + get endNode() { + return this._$AB; + } + _$AI(t, i = this) { + t = M$1(this, t, i), a(t) ? t === A || null == t || "" === t ? (this._$AH !== A && this._$AR(), this._$AH = A) : t !== this._$AH && t !== E && this._(t) : void 0 !== t._$litType$ ? this.$(t) : void 0 !== t.nodeType ? this.T(t) : d$1(t) ? this.k(t) : this._(t); + } + O(t) { + return this._$AA.parentNode.insertBefore(t, this._$AB); + } + T(t) { + this._$AH !== t && (this._$AR(), this._$AH = this.O(t)); + } + _(t) { + this._$AH !== A && a(this._$AH) ? this._$AA.nextSibling.data = t : this.T(l$1.createTextNode(t)), this._$AH = t; + } + $(t) { + const { values: i, _$litType$: s } = t, e = "number" == typeof s ? this._$AC(t) : (void 0 === s.el && (s.el = S.createElement(V(s.h, s.h[0]), this.options)), s); + if (this._$AH?._$AD === e) this._$AH.p(i); + else { + const t = new R(e, this), s = t.u(this.options); + t.p(i), this.T(s), this._$AH = t; + } + } + _$AC(t) { + let i = C.get(t.strings); + return void 0 === i && C.set(t.strings, i = new S(t)), i; + } + k(t) { + u$2(this._$AH) || (this._$AH = [], this._$AR()); + const i = this._$AH; + let s, e = 0; + for (const h of t) e === i.length ? i.push(s = new k(this.O(c$3()), this.O(c$3()), this, this.options)) : s = i[e], s._$AI(h), e++; + e < i.length && (this._$AR(s && s._$AB.nextSibling, e), i.length = e); + } + _$AR(t = this._$AA.nextSibling, s) { + for (this._$AP?.(!1, !0, s); t !== this._$AB;) { + const s = i$4(t).nextSibling; + i$4(t).remove(), t = s; + } + } + setConnected(t) { + void 0 === this._$AM && (this._$Cv = t, this._$AP?.(t)); + } + }; + var H = class { + get tagName() { + return this.element.tagName; + } + get _$AU() { + return this._$AM._$AU; + } + constructor(t, i, s, e, h) { + this.type = 1, this._$AH = A, this._$AN = void 0, this.element = t, this.name = i, this._$AM = e, this.options = h, s.length > 2 || "" !== s[0] || "" !== s[1] ? (this._$AH = Array(s.length - 1).fill(/* @__PURE__ */ new String()), this.strings = s) : this._$AH = A; + } + _$AI(t, i = this, s, e) { + const h = this.strings; + let o = !1; + if (void 0 === h) t = M$1(this, t, i, 0), o = !a(t) || t !== this._$AH && t !== E, o && (this._$AH = t); + else { + const e = t; + let n, r; + for (t = h[0], n = 0; n < h.length - 1; n++) r = M$1(this, e[s + n], i, n), r === E && (r = this._$AH[n]), o ||= !a(r) || r !== this._$AH[n], r === A ? t = A : t !== A && (t += (r ?? "") + h[n + 1]), this._$AH[n] = r; + } + o && !e && this.j(t); + } + j(t) { + t === A ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t ?? ""); + } + }; + var I = class extends H { + constructor() { + super(...arguments), this.type = 3; + } + j(t) { + this.element[this.name] = t === A ? void 0 : t; + } + }; + var L = class extends H { + constructor() { + super(...arguments), this.type = 4; + } + j(t) { + this.element.toggleAttribute(this.name, !!t && t !== A); + } + }; + var z = class extends H { + constructor(t, i, s, e, h) { + super(t, i, s, e, h), this.type = 5; + } + _$AI(t, i = this) { + if ((t = M$1(this, t, i, 0) ?? A) === E) return; + const s = this._$AH, e = t === A && s !== A || t.capture !== s.capture || t.once !== s.once || t.passive !== s.passive, h = t !== A && (s === A || e); + e && this.element.removeEventListener(this.name, this, s), h && this.element.addEventListener(this.name, this, t), this._$AH = t; + } + handleEvent(t) { + "function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t) : this._$AH.handleEvent(t); + } + }; + var Z = class { + constructor(t, i, s) { + this.element = t, this.type = 6, this._$AN = void 0, this._$AM = i, this.options = s; + } + get _$AU() { + return this._$AM._$AU; + } + _$AI(t) { + M$1(this, t); + } + }; + var j$1 = { + M: h$3, + P: o$6, + A: n$5, + C: 1, + L: N, + R, + D: d$1, + V: M$1, + I: k, + H, + N: L, + U: z, + B: I, + F: Z + }; + var B = t$3.litHtmlPolyfillSupport; + B?.(S, k), (t$3.litHtmlVersions ??= []).push("3.3.3"); + var D = (t, i, s) => { + const e = s?.renderBefore ?? i; + let h = e._$litPart$; + if (void 0 === h) { + const t = s?.renderBefore ?? null; + e._$litPart$ = h = new k(i.insertBefore(c$3(), t), t, void 0, s ?? {}); + } + return h._$AI(t), h; + }; + //#endregion + //#region node_modules/lit-element/lit-element.js + /** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ var s$2 = globalThis; + var i$3 = class extends y$1 { + constructor() { + super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; + } + createRenderRoot() { + const t = super.createRenderRoot(); + return this.renderOptions.renderBefore ??= t.firstChild, t; + } + update(t) { + const r = this.render(); + this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t), this._$Do = D(r, this.renderRoot, this.renderOptions); + } + connectedCallback() { + super.connectedCallback(), this._$Do?.setConnected(!0); + } + disconnectedCallback() { + super.disconnectedCallback(), this._$Do?.setConnected(!1); + } + render() { + return E; + } + }; + i$3._$litElement$ = !0, i$3["finalized"] = !0, s$2.litElementHydrateSupport?.({ LitElement: i$3 }); + var o$5 = s$2.litElementPolyfillSupport; + o$5?.({ LitElement: i$3 }); + (s$2.litElementVersions ??= []).push("4.2.2"); + //#endregion + //#region node_modules/@lit/reactive-element/decorators/custom-element.js + /** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + var t$2 = (t) => (e, o) => { + void 0 !== o ? o.addInitializer(() => { + customElements.define(t, e); + }) : customElements.define(t, e); + }; + //#endregion + //#region node_modules/@lit/reactive-element/decorators/property.js + /** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ var o$4 = { + attribute: !0, + type: String, + converter: u$3, + reflect: !1, + hasChanged: f$3 + }; + var r$3 = (t = o$4, e, r) => { + const { kind: n, metadata: i } = r; + let s = globalThis.litPropertyMetadata.get(i); + if (void 0 === s && globalThis.litPropertyMetadata.set(i, s = /* @__PURE__ */ new Map()), "setter" === n && ((t = Object.create(t)).wrapped = !0), s.set(r.name, t), "accessor" === n) { + const { name: o } = r; + return { + set(r) { + const n = e.get.call(this); + e.set.call(this, r), this.requestUpdate(o, n, t, !0, r); + }, + init(e) { + return void 0 !== e && this.C(o, void 0, t, e), e; + } + }; + } + if ("setter" === n) { + const { name: o } = r; + return function(r) { + const n = this[o]; + e.call(this, r), this.requestUpdate(o, n, t, !0, r); + }; + } + throw Error("Unsupported decorator location: " + n); + }; + function n$4(t) { + return (e, o) => "object" == typeof o ? r$3(t, e, o) : ((t, e, o) => { + const r = e.hasOwnProperty(o); + return e.constructor.createProperty(o, t), r ? Object.getOwnPropertyDescriptor(e, o) : void 0; + })(t, e, o); + } + //#endregion + //#region node_modules/@lit/reactive-element/decorators/state.js + /** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ function r$2(r) { + return n$4({ + ...r, + state: !0, + attribute: !1 + }); + } + //#endregion + //#region node_modules/lit-html/directive.js + /** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + var t$1 = { + ATTRIBUTE: 1, + CHILD: 2, + PROPERTY: 3, + BOOLEAN_ATTRIBUTE: 4, + EVENT: 5, + ELEMENT: 6 + }; + var e$4 = (t) => (...e) => ({ + _$litDirective$: t, + values: e + }); + var i$2 = class { + constructor(t) {} + get _$AU() { + return this._$AM._$AU; + } + _$AT(t, e, i) { + this._$Ct = t, this._$AM = e, this._$Ci = i; + } + _$AS(t, e) { + return this.update(t, e); + } + update(t, e) { + return this.render(...e); + } + }; + //#endregion + //#region node_modules/lit-html/directives/unsafe-html.js + /** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ var e$3 = class extends i$2 { + constructor(i) { + if (super(i), this.it = A, i.type !== t$1.CHILD) throw Error(this.constructor.directiveName + "() can only be used in child bindings"); + } + render(r) { + if (r === A || null == r) return this._t = void 0, this.it = r; + if (r === E) return r; + if ("string" != typeof r) throw Error(this.constructor.directiveName + "() called with a non-string value"); + if (r === this.it) return this._t; + this.it = r; + const s = [r]; + return s.raw = s, this._t = { + _$litType$: this.constructor.resultType, + strings: s, + values: [] + }; + } + }; + e$3.directiveName = "unsafeHTML", e$3.resultType = 1; + var o$3 = e$4(e$3); + //#endregion + //#region node_modules/lit-html/directives/class-map.js + /** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ var e$2 = e$4(class extends i$2 { + constructor(t) { + if (super(t), t.type !== t$1.ATTRIBUTE || "class" !== t.name || t.strings?.length > 2) throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute."); + } + render(t) { + return " " + Object.keys(t).filter((s) => t[s]).join(" ") + " "; + } + update(s, [i]) { + if (void 0 === this.st) { + this.st = /* @__PURE__ */ new Set(), void 0 !== s.strings && (this.nt = new Set(s.strings.join(" ").split(/\s/).filter((t) => "" !== t))); + for (const t in i) i[t] && !this.nt?.has(t) && this.st.add(t); + return this.render(i); + } + const r = s.element.classList; + for (const t of this.st) t in i || (r.remove(t), this.st.delete(t)); + for (const t in i) { + const s = !!i[t]; + s === this.st.has(t) || this.nt?.has(t) || (s ? (r.add(t), this.st.add(t)) : (r.remove(t), this.st.delete(t))); + } + return E; + } + }); + //#endregion + //#region src/components/cursor-hint/cursor-hint.css?inline + var cursor_hint_default = ":host {\n display: contents;\n}\n.cursor-hint {\n position: fixed;\n z-index: 200;\n width: max-content;\n max-width: 16rem;\n padding: 0.3rem 0.5rem;\n border-radius: 0.25rem;\n background: var(--panel);\n border: 1px solid var(--border);\n color: var(--text);\n font-family: var(--font-ui);\n font-size: 0.75rem;\n line-height: 1.35;\n text-align: center;\n white-space: normal;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);\n pointer-events: none;\n}\n.cursor-hint.cursor-hint-detail {\n max-width: 24rem;\n padding: 0.45rem 0.55rem;\n text-align: left;\n}\n.head-tooltip {\n border-collapse: collapse;\n width: 100%;\n font-size: 0.6875rem;\n line-height: 1.4;\n}\n.head-tooltip th {\n padding: 0.1rem 0.55rem 0.1rem 0;\n color: var(--muted);\n font-weight: 600;\n text-align: left;\n vertical-align: top;\n white-space: nowrap;\n}\n.head-tooltip td {\n padding: 0.1rem 0;\n vertical-align: top;\n word-break: break-word;\n}\n.head-tooltip .head-default {\n color: var(--muted);\n font-style: italic;\n white-space: nowrap;\n}\n"; + //#endregion + //#region \0@oxc-project+runtime@0.144.0/helpers/esm/decorate.js + function __decorate(decorators, target, key, desc) { + var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; + if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); + else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + return c > 3 && r && Object.defineProperty(target, key, r), r; + } + //#endregion + //#region src/components/cursor-hint/cursor-hint.ts + /** — floating tooltip that follows the pointer */ + var OFFSET = 10; + var MARGIN = 8; + var DoclangCursorHint = class DoclangCursorHint extends i$3 { + static styles = r$6(cursor_hint_default); + _content = null; + _isHtml = false; + _detail = false; + _hidden = true; + _left = 0; + _top = 0; + hide() { + this._content = null; + this._isHtml = false; + this._detail = false; + this._hidden = true; + this.requestUpdate(); + } + show(content, clientX, clientY, detail = false) { + this._content = content; + this._isHtml = false; + this._detail = detail; + this._hidden = false; + this._position(clientX, clientY); + this.requestUpdate(); + } + showHtml(html, clientX, clientY) { + this._content = html; + this._isHtml = true; + this._detail = true; + this._hidden = false; + this._position(clientX, clientY); + this.requestUpdate(); + } + render() { + if (this._hidden || this._content === null) return A; + const classes = { + "cursor-hint": true, + "cursor-hint-detail": this._detail + }; + const style = `left:${this._left}px;top:${this._top}px`; + if (this._isHtml && typeof this._content === "string") return b``; + if (typeof this._content === "string") return b``; + return b``; + } + updated() { + if (!this._hidden && this._content instanceof Node) { + const hint = this.shadowRoot?.querySelector(".cursor-hint"); + if (hint) { + hint.replaceChildren(this._content.cloneNode(true)); + const rect = hint.getBoundingClientRect(); + this._repositionFromRect(rect); + } + } + } + _position(clientX, clientY) { + this._left = Math.max(MARGIN, clientX + OFFSET); + this._top = Math.max(MARGIN, clientY + OFFSET); + } + _repositionFromRect(rect) { + const hint = this.shadowRoot?.querySelector(".cursor-hint"); + if (!hint) return; + if (this._left + rect.width > window.innerWidth - MARGIN) this._left = Math.max(MARGIN, this._left - rect.width - 20); + if (this._top + rect.height > window.innerHeight - MARGIN) this._top = Math.max(MARGIN, this._top - rect.height - 20); + hint.style.left = `${this._left}px`; + hint.style.top = `${this._top}px`; + } + }; + DoclangCursorHint = __decorate([t$2("doclang-cursor-hint")], DoclangCursorHint); + //#endregion + //#region node_modules/lit-html/directive-helpers.js + /** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ var { I: t } = j$1, i$1 = (o) => o, r$1 = (o) => void 0 === o.strings, s$1 = () => document.createComment(""), v = (o, n, e) => { + const l = o._$AA.parentNode, d = void 0 === n ? o._$AB : n._$AA; + if (void 0 === e) e = new t(l.insertBefore(s$1(), d), l.insertBefore(s$1(), d), o, o.options); + else { + const t = e._$AB.nextSibling, n = e._$AM, c = n !== o; + if (c) { + let t; + e._$AQ?.(o), e._$AM = o, void 0 !== e._$AP && (t = o._$AU) !== n._$AU && e._$AP(t); + } + if (t !== d || c) { + let o = e._$AA; + for (; o !== t;) { + const t = i$1(o).nextSibling; + i$1(l).insertBefore(o, d), o = t; + } + } + } + return e; + }, u$1 = (o, t, i = o) => (o._$AI(t, i), o), m = {}, p = (o, t = m) => o._$AH = t, M = (o) => o._$AH, h$2 = (o) => { + o._$AR(), o._$AA.remove(); + }; + //#endregion + //#region node_modules/lit-html/async-directive.js + /** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ var s = (i, t) => { + const e = i._$AN; + if (void 0 === e) return !1; + for (const i of e) i._$AO?.(t, !1), s(i, t); + return !0; + }; + var o$2 = (i) => { + let t, e; + do { + if (void 0 === (t = i._$AM)) break; + e = t._$AN, e.delete(i), i = t; + } while (0 === e?.size); + }; + var r = (i) => { + for (let t; t = i._$AM; i = t) { + let e = t._$AN; + if (void 0 === e) t._$AN = e = /* @__PURE__ */ new Set(); + else if (e.has(i)) break; + e.add(i), c$1(t); + } + }; + function h$1(i) { + void 0 !== this._$AN ? (o$2(this), this._$AM = i, r(this)) : this._$AM = i; + } + function n$2(i, t = !1, e = 0) { + const r = this._$AH, h = this._$AN; + if (void 0 !== h && 0 !== h.size) if (t) if (Array.isArray(r)) for (let i = e; i < r.length; i++) s(r[i], !1), o$2(r[i]); + else null != r && (s(r, !1), o$2(r)); + else s(this, i); + } + var c$1 = (i) => { + i.type == t$1.CHILD && (i._$AP ??= n$2, i._$AQ ??= h$1); + }; + var f = class extends i$2 { + constructor() { + super(...arguments), this._$AN = void 0; + } + _$AT(i, t, e) { + super._$AT(i, t, e), r(this), this.isConnected = i._$AU; + } + _$AO(i, t = !0) { + i !== this.isConnected && (this.isConnected = i, i ? this.reconnected?.() : this.disconnected?.()), t && (s(this, i), o$2(this)); + } + setValue(t) { + if (r$1(this._$Ct)) this._$Ct._$AI(t, this); + else { + const i = [...this._$Ct._$AH]; + i[this._$Ci] = t, this._$Ct._$AI(i, this, 0); + } + } + disconnected() {} + reconnected() {} + }; + //#endregion + //#region node_modules/lit-html/directives/ref.js + /** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ var e = () => new h(); + var h = class {}; + var o$1 = /* @__PURE__ */ new WeakMap(); + var n$1 = e$4(class extends f { + render(i) { + return A; + } + update(i, [s]) { + const e = s !== this.G; + return e && this.rt(void 0), (e || this.lt !== this.ct) && (this.G = s, this.ht = i.options?.host, this.rt(this.ct = i.element)), A; + } + rt(t) { + if (void 0 !== this.G) if (this.isConnected || (t = void 0), "function" == typeof this.G) { + const i = this.ht ?? globalThis; + let s = o$1.get(i); + void 0 === s && (s = /* @__PURE__ */ new WeakMap(), o$1.set(i, s)), void 0 !== s.get(this.G) && this.G.call(this.ht, void 0), s.set(this.G, t), void 0 !== t && this.G.call(this.ht, t); + } else this.G.value = t; + } + get lt() { + return "function" == typeof this.G ? o$1.get(this.ht ?? globalThis)?.get(this.G) : this.G?.value; + } + disconnected() { + this.lt === this.ct && this.rt(void 0); + } + reconnected() { + this.rt(this.ct); + } + }); + //#endregion + //#region src/components/page-nav/page-nav.css?inline + var page_nav_default = ":host {\n display: contents;\n}\nnav {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n margin-left: 0.25rem;\n}\nnav::before {\n content: '';\n flex-shrink: 0;\n align-self: center;\n width: 1px;\n height: 1.125rem;\n margin-right: 0.75rem;\n background: var(--border);\n}\nnav[hidden] {\n display: none;\n}\n.page-nav-btns {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n}\n.page-nav-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 1.875rem;\n height: 1.875rem;\n padding: 0;\n flex-shrink: 0;\n appearance: none;\n border: 1px solid var(--border);\n background: var(--panel);\n color: var(--text);\n border-radius: 0.375rem;\n font: inherit;\n font-size: 0.875rem;\n cursor: pointer;\n}\n.page-nav-btn:hover {\n border-color: var(--accent);\n}\n.page-nav-btn:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.page-nav-chevron {\n display: block;\n width: 0.875rem;\n height: 0.875rem;\n}\n.page-indicator {\n display: inline-flex;\n align-items: center;\n font-size: 0.875rem;\n color: var(--muted);\n font-variant-numeric: tabular-nums;\n}\n.page-number-input {\n appearance: none;\n border: 1px solid var(--border);\n border-radius: 0.375rem;\n background: var(--panel);\n color: var(--text);\n font: inherit;\n font-size: 0.875rem;\n font-variant-numeric: tabular-nums;\n text-align: center;\n padding: 0 0.35rem;\n margin: 0;\n height: 1.875rem;\n box-sizing: border-box;\n width: calc(var(--page-num-digits, 1) * 1ch + 0.75rem);\n line-height: 1;\n cursor: text;\n}\n.page-number-input:hover:not(:disabled) {\n border-color: var(--accent);\n}\n.page-number-input:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 22%, transparent);\n}\n.page-number-input:disabled {\n opacity: 0.55;\n cursor: not-allowed;\n}\n"; + //#endregion + //#region src/components/page-nav/page-nav.ts + /** — page navigation (prev/next buttons + page indicator) */ + var DoclangPageNav = class DoclangPageNav extends i$3 { + static styles = r$6(page_nav_default); + _currentPage = 1; + _pageCount = 1; + _visible = false; + _inputRef = e(); + render() { + if (!this._visible) return A; + const digits = Math.max(1, String(this._pageCount).length); + return b` + + `; + } + setVisible(visible) { + this._visible = visible; + this.requestUpdate(); + } + setIndicator(pageNum, pageCount) { + this._currentPage = pageNum; + this._pageCount = pageCount; + this.requestUpdate(); + } + reset() { + this._currentPage = 1; + this._pageCount = 1; + this._visible = false; + this.requestUpdate(); + } + _onInputKeydown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + this._commitInput(); + e.target.blur(); + } else if (e.key === "Escape") { + e.preventDefault(); + this._resetInput(); + e.target.blur(); + } + }; + _onInputBlur = () => this._resetInput(); + _resetInput() { + const input = this._inputRef.value; + if (input) input.value = String(this._currentPage); + } + _commitInput() { + const input = this._inputRef.value; + if (!input) return; + const n = Number.parseInt(input.value.trim(), 10); + if (!Number.isFinite(n)) { + this._resetInput(); + return; + } + const page = Math.min(Math.max(1, n), this._pageCount); + this.dispatchEvent(new CustomEvent("doclang-go-to-page", { + bubbles: true, + composed: true, + detail: { page } + })); + } + }; + DoclangPageNav = __decorate([t$2("doclang-page-nav")], DoclangPageNav); + //#endregion + //#region src/components/toolbar/toolbar.css?inline + var toolbar_default = ":host {\n display: contents;\n}\n.toolbar {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n flex-wrap: wrap;\n grid-column: 3;\n justify-self: end;\n}\n.header-divider {\n flex-shrink: 0;\n align-self: center;\n width: 1px;\n height: 1.125rem;\n background: var(--border);\n}\n.toolbar-options-wrap {\n position: relative;\n}\n/* Menu/control triggers */\n.toolbar-options-btn {\n appearance: none;\n border: 1px solid transparent;\n background: transparent;\n color: var(--muted);\n font-weight: 500;\n cursor: pointer;\n white-space: nowrap;\n flex-shrink: 0;\n display: inline-flex;\n align-items: center;\n transition:\n color 0.12s ease,\n background 0.12s ease,\n border-color 0.12s ease;\n border-radius: 0.375rem;\n padding: 0.35rem 0.45rem 0.35rem 0.55rem;\n font-size: 0.8125rem;\n line-height: 1.2;\n font: inherit;\n}\n.toolbar-options-btn::after {\n content: '';\n flex-shrink: 0;\n width: 0.32rem;\n height: 0.32rem;\n margin-left: 0.3rem;\n margin-top: -0.14em;\n border-right: 1.5px solid currentColor;\n border-bottom: 1.5px solid currentColor;\n transform: rotate(45deg);\n opacity: 0.65;\n}\n.toolbar-options-btn:hover {\n color: var(--text);\n background: color-mix(in srgb, var(--text) 5%, transparent);\n}\n.toolbar-options-btn[aria-expanded='true'] {\n color: var(--accent);\n background: color-mix(in srgb, var(--accent) 10%, var(--panel));\n border-color: color-mix(in srgb, var(--accent) 22%, transparent);\n}\n.toolbar-options-btn:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n}\n.toolbar-options-panel {\n position: absolute;\n top: calc(100% + 0.35rem);\n right: 0;\n z-index: 30;\n min-width: 11rem;\n padding: 0.5rem 0;\n border: 1px solid var(--border);\n border-radius: 0.375rem;\n background: var(--panel);\n box-shadow: 0 0.5rem 1.25rem color-mix(in srgb, var(--text) 12%, transparent);\n}\n.toolbar-options-panel[hidden] {\n display: none;\n}\n.toolbar-options-item {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n padding: 0.35rem 0.75rem;\n font-size: 0.875rem;\n cursor: pointer;\n font-family: var(--font-ui);\n}\n.toolbar-options-item:hover {\n background: color-mix(in srgb, var(--accent) 6%, var(--panel));\n}\n.toolbar-options-item input {\n margin: 0;\n flex-shrink: 0;\n cursor: pointer;\n}\n.toolbar-options-item-disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.toolbar-options-item-disabled input {\n cursor: not-allowed;\n}\n.toolbar-options-divider {\n margin: 0.35rem 0;\n border-top: 1px solid var(--border);\n}\n.toolbar-options-reset {\n display: block;\n width: 100%;\n box-sizing: border-box;\n margin: 0;\n padding: 0.35rem 0.75rem;\n border: 0;\n border-radius: 0;\n background: transparent;\n color: var(--text);\n font: inherit;\n font-size: 0.875rem;\n text-align: left;\n cursor: pointer;\n}\n.toolbar-options-reset:hover:not(:disabled) {\n background: color-mix(in srgb, var(--accent) 6%, var(--panel));\n color: var(--accent);\n}\n.toolbar-options-reset:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\n.toolbar-file-group {\n display: inline-flex;\n align-items: center;\n}\nbutton,\nlabel.file-btn {\n appearance: none;\n border: 1px solid var(--border);\n background: var(--panel);\n color: var(--text);\n border-radius: 0.375rem;\n padding: 0.4rem 0.75rem;\n font: inherit;\n font-size: 0.875rem;\n cursor: pointer;\n}\nbutton:hover,\nlabel.file-btn:hover {\n border-color: var(--accent);\n}\nbutton:disabled {\n opacity: 0.45;\n cursor: not-allowed;\n}\nlabel.file-btn input {\n display: none;\n}\n.header-site-link {\n color: var(--muted);\n font-size: 0.875rem;\n text-decoration: none;\n white-space: nowrap;\n}\n.header-site-link:hover {\n color: var(--accent);\n text-decoration: underline;\n}\n.header-site-link:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n border-radius: 0.125rem;\n}\n"; + //#endregion + //#region src/components/toolbar/toolbar.ts + /** — header toolbar (Views menu, file open, demo, site link) */ + var OPEN_FILE_HINT = `Open a DocLang file (.dclx, .dclg)`; + var DoclangToolbar = class DoclangToolbar extends i$3 { + static styles = r$6(toolbar_default); + _panelOpen = false; + _demoLoading = false; + _panes = { + file: true, + page: true, + markup: true, + reading: true, + fileAvailable: false, + pageAvailable: false, + hasState: false + }; + _inputArchiveRef = e(); + render() { + const { file, page, markup, reading, fileAvailable, pageAvailable, hasState } = this._panes; + return b` +
+
+ + ${this._panelOpen ? b` + + ` : A} +
+ + + + + + + doclang.ai +
+ `; + } + /** Sync the Views-menu checkboxes and disabled/greyed state from app state. */ + syncPaneToggles(opts) { + this._panes = opts; + this.requestUpdate(); + } + setDemoLoading(loading) { + this._demoLoading = loading; + this.requestUpdate(); + } + setOptionsOpen(open) { + this._panelOpen = open; + this.requestUpdate(); + } + closeOptionsIfOpen() { + if (this._panelOpen) this.setOptionsOpen(false); + } + connectedCallback() { + super.connectedCallback(); + document.addEventListener("click", this._onDocClick); + } + disconnectedCallback() { + super.disconnectedCallback(); + document.removeEventListener("click", this._onDocClick); + } + _onDocClick = (e) => { + if (!this._panelOpen) return; + if (e.composedPath().includes(this)) return; + this.setOptionsOpen(false); + }; + _onOptionsClick = (e) => { + e.stopPropagation(); + this.setOptionsOpen(!this._panelOpen); + }; + _emitTogglePane(pane, checked) { + this.dispatchEvent(new CustomEvent("doclang-toggle-pane", { + bubbles: true, + composed: true, + detail: { + pane, + checked + } + })); + } + _onArchiveChange = (e) => { + const input = e.target; + this.dispatchEvent(new CustomEvent("doclang-open-files", { + bubbles: true, + composed: true, + detail: { files: [...input.files ?? []] } + })); + input.value = ""; + }; + _onFileBtnMousemove = (e) => { + this.dispatchEvent(new CustomEvent("doclang-hint", { + bubbles: true, + composed: true, + detail: { + text: OPEN_FILE_HINT, + clientX: e.clientX, + clientY: e.clientY + } + })); + }; + _onFileBtnMouseleave = () => { + this.dispatchEvent(new CustomEvent("doclang-hint-hide", { + bubbles: true, + composed: true + })); + }; + }; + DoclangToolbar = __decorate([t$2("doclang-toolbar")], DoclangToolbar); + //#endregion + //#region node_modules/lit-html/directives/repeat.js + /** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ + var u = (e, s, t) => { + const r = /* @__PURE__ */ new Map(); + for (let l = s; l <= t; l++) r.set(e[l], l); + return r; + }; + var c = e$4(class extends i$2 { + constructor(e) { + if (super(e), e.type !== t$1.CHILD) throw Error("repeat() can only be used in text expressions"); + } + dt(e, s, t) { + let r; + void 0 === t ? t = s : void 0 !== s && (r = s); + const l = [], o = []; + let i = 0; + for (const s of e) l[i] = r ? r(s, i) : i, o[i] = t(s, i), i++; + return { + values: o, + keys: l + }; + } + render(e, s, t) { + return this.dt(e, s, t).values; + } + update(s, [t, r, c]) { + const d = M(s), { values: p$3, keys: a } = this.dt(t, r, c); + if (!Array.isArray(d)) return this.ut = a, p$3; + const h = this.ut ??= [], v$2 = []; + let m, y, x = 0, j = d.length - 1, k = 0, w = p$3.length - 1; + for (; x <= j && k <= w;) if (null === d[x]) x++; + else if (null === d[j]) j--; + else if (h[x] === a[k]) v$2[k] = u$1(d[x], p$3[k]), x++, k++; + else if (h[j] === a[w]) v$2[w] = u$1(d[j], p$3[w]), j--, w--; + else if (h[x] === a[w]) v$2[w] = u$1(d[x], p$3[w]), v(s, v$2[w + 1], d[x]), x++, w--; + else if (h[j] === a[k]) v$2[k] = u$1(d[j], p$3[k]), v(s, d[x], d[j]), j--, k++; + else if (void 0 === m && (m = u(a, k, w), y = u(h, x, j)), m.has(h[x])) if (m.has(h[j])) { + const e = y.get(a[k]), t = void 0 !== e ? d[e] : null; + if (null === t) { + const e = v(s, d[x]); + u$1(e, p$3[k]), v$2[k] = e; + } else v$2[k] = u$1(t, p$3[k]), v(s, d[x], t), d[e] = null; + k++; + } else h$2(d[j]), j--; + else h$2(d[x]), x++; + for (; k <= w;) { + const e = v(s, v$2[w + 1]); + u$1(e, p$3[k]), v$2[k++] = e; + } + for (; x <= j;) { + const e = d[x++]; + null !== e && h$2(e); + } + return this.ut = a, p(s, v$2), E; + } + }); + //#endregion + //#region src/components/file-pane/file-pane.css?inline + var file_pane_default = ":host {\n display: flex;\n flex-direction: column;\n border-right: 1px solid var(--border);\n min-height: 0;\n min-width: 0;\n overflow: hidden;\n}\n:host([hidden]) {\n display: none !important;\n}\n:host(.pane-layout-last) {\n border-right: none;\n}\n.pane-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.35rem;\n box-sizing: border-box;\n height: 2.125rem;\n padding: 0 0.35rem;\n font-size: 0.75rem;\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--muted);\n border-bottom: 1px solid var(--border);\n background: var(--panel);\n}\n.pane-header-title {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n padding-inline: calc(0.75rem - 0.35rem);\n}\n.file-pane-close-all {\n appearance: none;\n border: 1px solid transparent;\n border-radius: 0.25rem;\n background: transparent;\n color: var(--muted);\n font: inherit;\n font-size: 0.625rem;\n font-weight: 600;\n line-height: 1;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n padding: 0 0.15rem;\n height: 1.25rem;\n cursor: pointer;\n white-space: nowrap;\n flex-shrink: 0;\n transition:\n color 0.12s ease,\n background 0.12s ease,\n border-color 0.12s ease;\n}\n.file-pane-close-all:hover {\n color: var(--text);\n background: color-mix(in srgb, var(--text) 5%, transparent);\n}\n.file-pane-close-all:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 2px;\n}\n.pane-body {\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding: 0.375rem var(--file-pane-pad-x, 0.5rem);\n min-width: 0;\n}\n.file-view-list {\n list-style: none;\n margin: 0;\n padding: 0;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n width: 100%;\n}\n.file-view-list > li {\n width: 100%;\n max-width: 100%;\n}\n.file-view-item {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 0.35rem;\n width: 100%;\n max-width: 100%;\n box-sizing: border-box;\n border: none;\n border-radius: 0;\n background: transparent;\n color: var(--text);\n font: inherit;\n font-size: 0.8125rem;\n line-height: 1.35;\n text-align: center;\n padding: 0.5rem var(--file-item-pad-x, 0.35rem);\n cursor: pointer;\n}\n.file-view-thumb-wrap {\n position: relative;\n flex-shrink: 0;\n}\n.file-view-close {\n position: absolute;\n top: -0.25rem;\n right: -0.25rem;\n appearance: none;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 1rem;\n height: 1rem;\n padding: 0;\n border: 1px solid var(--border);\n border-radius: 50%;\n background: var(--bg);\n color: var(--muted);\n font: inherit;\n font-size: 0.75rem;\n line-height: 1;\n cursor: pointer;\n box-shadow: 0 1px 2px rgb(0 0 0 / 8%);\n z-index: 1;\n}\n.file-view-close:hover {\n color: var(--text);\n background: var(--panel);\n}\n.file-view-close:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: 1px;\n}\n.file-view-thumb {\n flex-shrink: 0;\n width: var(--file-thumb-size, 3.75rem);\n height: var(--file-thumb-size, 3.75rem);\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--placeholder-bg);\n border: 1px solid var(--border);\n border-radius: 0.25rem;\n overflow: hidden;\n}\n.file-view-thumb img {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n.file-view-thumb-placeholder {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n color: var(--muted);\n}\n.file-view-thumb-placeholder svg {\n display: block;\n width: 1.875rem;\n height: 1.875rem;\n}\n.file-view-label {\n width: 100%;\n max-width: 100%;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.file-view-item:hover {\n background: var(--markup-hover);\n}\n.file-view-item.is-active {\n background: var(--markup-selected);\n color: var(--accent);\n font-weight: 500;\n}\n.file-view-item:focus-visible {\n outline: 2px solid var(--accent);\n outline-offset: -2px;\n}\n"; + //#endregion + //#region src/components/file-pane/file-pane.ts + /** — file list sidebar */ + var FILE_THUMB_PLACEHOLDER_SVG = ``; + var DoclangFilePane = class DoclangFilePane extends i$3 { + static styles = r$6(file_pane_default); + _entries = []; + connectedCallback() { + super.connectedCallback(); + this.classList.add("pane", "pane-file"); + } + render() { + const hasEntries = this._entries.length > 0; + return b` +
+ Files + ${hasEntries ? b`` : A} +
+
${hasEntries ? b` +
    + ${c(this._entries, (_, i) => i, this._renderEntry)} +
+ ` : A}
+ `; + } + setVisible(visible) { + this.hidden = !visible; + } + setLastPane(isLast) { + this.classList.toggle("pane-layout-last", isLast); + } + renderFiles(entries) { + this._entries = entries; + this.requestUpdate(); + } + _onCloseAll = () => { + this.dispatchEvent(new CustomEvent("doclang-file-pane-close-all", { + bubbles: true, + composed: true + })); + }; + _renderEntry = (entry, index) => { + const onCardClick = (e) => { + if (e.target.closest(".file-view-close")) return; + this._emitFileSelect(index); + }; + const onCardKeydown = (e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + this._emitFileSelect(index); + } + }; + const onCloseClick = (e) => { + e.stopPropagation(); + this._emitFileClose(index); + }; + const thumb = entry.thumbnailUrl ? b`` : b``; + return b` +
  • +
    +
    + + +
    + ${entry.label} +
    +
  • + `; + }; + _emitFileSelect(index) { + this.dispatchEvent(new CustomEvent("doclang-file-select", { + bubbles: true, + composed: true, + detail: { index } + })); + } + _emitFileClose(index) { + this.dispatchEvent(new CustomEvent("doclang-file-close", { + bubbles: true, + composed: true, + detail: { index } + })); + } + }; + DoclangFilePane = __decorate([t$2("doclang-file-pane")], DoclangFilePane); + //#endregion + //#region src/doclang/dom.ts + function isTextLikeNode(node) { + return node.nodeType === Node.TEXT_NODE || node.nodeType === Node.CDATA_SECTION_NODE; + } + function isWhitespaceOnlyText(node) { + return isTextLikeNode(node) && !node.textContent?.trim(); + } + function markupAttributes(el) { + return [...el.attributes].filter((a) => a.name !== "xmlns" || a.value !== "https://www.doclang.ai/ns/v0").map((a) => ({ + name: a.name, + value: a.value + })); + } + function childElements(el) { + return [...el.children]; + } + function localName(el) { + return el.localName || el.tagName.replace(/^.*:/, ""); + } + function headLocations(el) { + return parseElementHeadAt([...el.childNodes], 0)?.locs ?? []; + } + function parseElementHeadAt(nodes, startIdx) { + const locs = []; + let i = startIdx; + while (i < nodes.length) { + const node = nodes[i]; + if (!node) { + i += 1; + continue; + } + if (node.nodeType !== Node.ELEMENT_NODE) { + i += 1; + continue; + } + const tag = localName(node); + if (tag === "location") { + locs.push(node); + i += 1; + if (locs.length === 4) return { + locs, + nextIndex: i + }; + continue; + } + if (locs.length) break; + if (HEAD_TAGS.has(tag)) { + i += 1; + continue; + } + break; + } + return locs.length === 4 ? { + locs, + nextIndex: i + } : null; + } + function walkElements(nodes, fn) { + for (let i = 0; i < nodes.length; i += 1) { + const node = nodes[i]; + if (!node || node.nodeType !== Node.ELEMENT_NODE) continue; + fn(node); + walkElements(childElements(node), fn); + } + } + function isCellToken(tag) { + return CELL_TOKENS.has(tag); + } + function isListOrOtslContainer(el) { + const tag = localName(el); + return tag === "list" || OTSL_CONTAINER_TAGS.has(tag); + } + function skipContainerLevelHead(nodes, startIdx) { + let i = startIdx; + while (i < nodes.length) { + const node = nodes[i]; + if (!node) { + i += 1; + continue; + } + if (node.nodeType !== Node.ELEMENT_NODE) { + i += 1; + continue; + } + const tag = localName(node); + if (tag === "ldiv" || isCellToken(tag)) break; + if (HEAD_TAGS.has(tag) || tag === "location") { + i += 1; + continue; + } + break; + } + return i; + } + function skipUntilListItemBoundary(nodes, startIdx) { + let i = startIdx; + while (i < nodes.length) { + const node = nodes[i]; + if (node && node.nodeType === Node.ELEMENT_NODE && localName(node) === "ldiv") break; + i += 1; + } + return i; + } + function skipUntilCellBoundary(nodes, startIdx) { + let i = startIdx; + while (i < nodes.length) { + const node = nodes[i]; + if (node && node.nodeType === Node.ELEMENT_NODE && isCellToken(localName(node))) break; + i += 1; + } + return i; + } + function locationResolution(el, axisDefault) { + const r = parseInt(el.getAttribute("resolution") ?? String(axisDefault), 10); + return Number.isFinite(r) && r > 0 ? r : axisDefault; + } + function headingLevel(el) { + return Math.min(Math.max(parseInt(el.getAttribute("level") ?? "1", 10) || 1, 1), 6); + } + function firstHeadChild(el, tag) { + return childElements(el).find((child) => localName(child) === tag) ?? null; + } + function xmlContains(target, ancestor) { + let node = target; + while (node) { + if (node === ancestor) return true; + node = node.parentNode; + } + return false; + } + function formatMarkupTextNode(node) { + if (node.nodeType === Node.CDATA_SECTION_NODE) return ``; + return node.textContent?.trim() ?? ""; + } + function serializeMarkupTextNodes(nodes) { + return Array.from(nodes).filter((n) => isTextLikeNode(n) && !isWhitespaceOnlyText(n)).map(formatMarkupTextNode).join(""); + } + function escapeHtml(text) { + return String(text).replace(/&/g, "&").replace(//g, ">"); + } + function normalizeArchivePath(path) { + return path.replace(/\\/g, "/").replace(/^\.\//, ""); + } + function archiveRelativeAssetPath(path) { + const norm = normalizeArchivePath(path); + const idx = norm.indexOf("assets/"); + return idx === -1 ? null : norm.slice(idx); + } + function skipOtslContainerHead(nodes, startIdx) { + let i = startIdx; + while (i < nodes.length) { + const node = nodes[i]; + if (!node) { + i += 1; + continue; + } + if (node.nodeType !== Node.ELEMENT_NODE) { + i += 1; + continue; + } + const tag = localName(node); + if (isCellToken(tag)) break; + if (HEAD_TAGS.has(tag) || tag === "h_thread" || tag === "location") { + i += 1; + continue; + } + break; + } + return i; + } + function skipElementHeadNodes(nodes, startIdx) { + let i = startIdx; + while (i < nodes.length && isWhitespaceOnlyText(nodes[i])) i += 1; + while (i < nodes.length) { + const node = nodes[i]; + if (node && node.nodeType === Node.ELEMENT_NODE && HEAD_TAGS.has(localName(node))) { + i += 1; + while (i < nodes.length && isWhitespaceOnlyText(nodes[i])) i += 1; + continue; + } + break; + } + return i; + } + function isSemanticElement(el) { + return SEMANTIC_TAGS.has(localName(el)); + } + function isVirtualTextHost(el) { + const tag = localName(el); + return tag === "ldiv" || CELL_CONTENT_TAGS.has(tag); + } + function elementThreadId(el) { + return firstHeadChild(el, "thread")?.getAttribute("thread_id") ?? null; + } + function elementLayer(el) { + const layerEl = firstHeadChild(el, "layer"); + if (layerEl) { + const value = layerEl.getAttribute("value") ?? "body"; + return ELEMENT_LAYERS.has(value) ? value : "body"; + } + if (headLocations(el).length === 4) return "body"; + const parent = el.parentElement; + if (!parent) return "body"; + const nodes = [...parent.childNodes]; + const idx = nodes.indexOf(el); + if (idx < 0) return "body"; + return layerFromHeadNodes(nodes, idx + 1); + } + function layerFromHeadNodes(nodes, startIdx) { + let i = startIdx; + while (i < nodes.length) { + const node = nodes[i]; + if (!node) { + i += 1; + continue; + } + if (node.nodeType !== Node.ELEMENT_NODE) { + i += 1; + continue; + } + const tag = localName(node); + if (tag === "layer") { + const value = node.getAttribute("value") ?? "body"; + return ELEMENT_LAYERS.has(value) ? value : "body"; + } + if (tag === "location") break; + if (HEAD_TAGS.has(tag)) { + i += 1; + continue; + } + break; + } + return "body"; + } + function layerClassForValue(layer) { + if (layer === "furniture") return "layer-furniture"; + if (layer === "background") return "layer-background"; + return ""; + } + function elementLabel(el) { + if (isVirtualTextOverlayUnit(el)) return "text"; + const tag = localName(el); + if (tag === "heading" || tag === "field_heading") return `${tag}[${headingLevel(el)}]`; + const level = el.getAttribute("level"); + if (level) return `${tag}[${level}]`; + const cls = el.getAttribute("class"); + if (cls) return `${tag}.${cls}`; + return tag; + } + function isVirtualTextOverlayUnit(el) { + if (headLocations(el).length === 4) return false; + if (!hasVirtualTextLocations(el)) return false; + const tag = localName(el); + if (tag === "ldiv" && el.parentElement && localName(el.parentElement) === "list") return true; + if (isCellToken(tag) && tag !== "nl" && !CELL_SPAN_TAGS.has(tag)) return el.parentElement ? OTSL_CONTAINER_TAGS.has(localName(el.parentElement)) : false; + return false; + } + function hasVirtualTextLocations(el) { + const parent = el.parentElement; + if (!parent) return false; + const nodes = [...parent.childNodes]; + const idx = nodes.indexOf(el); + if (idx < 0) return false; + return parseElementHeadAt(nodes, idx + 1) !== null; + } + function virtualTextHeadLocations(el) { + const parent = el.parentElement; + if (!parent) return []; + const nodes = [...parent.childNodes]; + const idx = nodes.indexOf(el); + if (idx < 0) return []; + return parseElementHeadAt(nodes, idx + 1)?.locs ?? []; + } + function elementHeadLocations(el) { + const own = headLocations(el); + return own.length === 4 ? own : virtualTextHeadLocations(el); + } + function isPictureContentElement(el) { + if (!el) return false; + const tag = localName(el); + if (tag === "picture" || tag === "caption") return false; + let node = el.parentElement; + while (node) { + if (localName(node) === "picture") return true; + node = node.parentElement; + } + return false; + } + function isTableContentElement(el) { + if (!el) return false; + const tag = localName(el); + if (OTSL_CONTAINER_TAGS.has(tag) || tag === "caption") return false; + let node = el.parentElement; + while (node) { + if (OTSL_CONTAINER_TAGS.has(localName(node))) return true; + node = node.parentElement; + } + return false; + } + var HEAD_TAGS, SEMANTIC_TAGS, CELL_TOKENS, CELL_CONTENT_TAGS, CELL_SPAN_TAGS, OTSL_CONTAINER_TAGS, ELEMENT_LAYERS; + var init_dom = __esmMin((() => { + HEAD_TAGS = /* @__PURE__ */ new Set([ + "label", + "thread", + "xref", + "href", + "layer", + "location", + "caption", + "description", + "summary", + "custom" + ]); + SEMANTIC_TAGS = /* @__PURE__ */ new Set([ + "text", + "heading", + "footnote", + "page_header", + "page_footer", + "field_region", + "list", + "table", + "index", + "formula", + "code", + "picture", + "marker", + "group", + "field_heading", + "field_item", + "key", + "value", + "hint", + "caption", + "page_break" + ]); + CELL_TOKENS = /* @__PURE__ */ new Set([ + "fcel", + "ecel", + "ched", + "rhed", + "corn", + "srow", + "lcel", + "ucel", + "xcel", + "nl" + ]); + CELL_CONTENT_TAGS = /* @__PURE__ */ new Set([ + "fcel", + "ecel", + "ched", + "rhed", + "corn", + "srow" + ]); + CELL_SPAN_TAGS = /* @__PURE__ */ new Set([ + "lcel", + "ucel", + "xcel" + ]); + OTSL_CONTAINER_TAGS = /* @__PURE__ */ new Set([ + "table", + "index", + "tabular" + ]); + ELEMENT_LAYERS = /* @__PURE__ */ new Set([ + "body", + "background", + "furniture" + ]); + })); + //#endregion + //#region src/doclang/zip.ts + async function unzip(buffer, { shouldExtract } = {}) { + if (typeof DecompressionStream === "undefined") throw new Error("ZIP decompression requires a browser with DecompressionStream support"); + const bytes = new Uint8Array(buffer); + const view = new DataView(buffer); + const eocdOffset = findEndOfCentralDirectory(bytes); + const entryCount = view.getUint16(eocdOffset + 10, true); + const centralDirOffset = view.getUint32(eocdOffset + 16, true); + if (entryCount > ZIP_MAX_ENTRIES) throw new Error("ZIP archive has too many entries"); + if (centralDirOffset >= bytes.length) throw new Error("Invalid ZIP central directory offset"); + const entries = []; + let offset = centralDirOffset; + let totalUncompressed = 0; + for (let i = 0; i < entryCount; i += 1) { + if (offset + 46 > bytes.length || view.getUint32(offset, true) !== 33639248) throw new Error("Invalid ZIP central directory"); + const compression = view.getUint16(offset + 10, true); + const compressedSize = view.getUint32(offset + 20, true); + const uncompressedSize = view.getUint32(offset + 24, true); + const nameLength = view.getUint16(offset + 28, true); + const extraLength = view.getUint16(offset + 30, true); + const commentLength = view.getUint16(offset + 32, true); + const localHeaderOffset = view.getUint32(offset + 42, true); + const nameEnd = offset + 46 + nameLength; + if (nameEnd > bytes.length) throw new Error("Invalid ZIP entry name"); + const name = normalizeZipEntryName(new TextDecoder().decode(bytes.subarray(offset + 46, nameEnd))); + offset = nameEnd + extraLength + commentLength; + if (offset > bytes.length) throw new Error("Invalid ZIP central directory"); + if (!name || name.endsWith("/") || isIgnoredArchiveEntry(name)) continue; + if (shouldExtract && !shouldExtract(name)) continue; + if (compressedSize > ZIP_MAX_ENTRY_COMPRESSED_BYTES || uncompressedSize > ZIP_MAX_ENTRY_UNCOMPRESSED_BYTES) throw new Error(`ZIP entry exceeds size limit: ${name}`); + if (compressedSize > 0 && uncompressedSize > 0 && uncompressedSize / compressedSize > ZIP_MAX_COMPRESSION_RATIO) throw new Error(`ZIP entry compression ratio exceeds limit: ${name}`); + if (totalUncompressed + uncompressedSize > ZIP_MAX_TOTAL_UNCOMPRESSED_BYTES) throw new Error("ZIP archive exceeds total uncompressed size limit"); + if (localHeaderOffset + 30 > bytes.length || view.getUint32(localHeaderOffset, true) !== 67324752) throw new Error(`Invalid ZIP local header: ${name}`); + const localNameLength = view.getUint16(localHeaderOffset + 26, true); + const localExtraLength = view.getUint16(localHeaderOffset + 28, true); + const dataOffset = localHeaderOffset + 30 + localNameLength + localExtraLength; + if (dataOffset + compressedSize > bytes.length) throw new Error(`ZIP entry data out of bounds: ${name}`); + const data = await decompressZipEntry(bytes.subarray(dataOffset, dataOffset + compressedSize), compression, uncompressedSize); + totalUncompressed += data.length; + if (totalUncompressed > ZIP_MAX_TOTAL_UNCOMPRESSED_BYTES) throw new Error("ZIP archive exceeds total uncompressed size limit"); + entries.push({ + name, + data + }); + } + return entries; + } + function findEndOfCentralDirectory(bytes) { + for (let i = bytes.length - 22; i >= 0; i -= 1) if (bytes[i] === 80 && bytes[i + 1] === 75 && bytes[i + 2] === 5 && bytes[i + 3] === 6) return i; + throw new Error("Invalid ZIP archive"); + } + function concatUint8Arrays(chunks, totalLength) { + const out = new Uint8Array(totalLength); + let offset = 0; + for (const chunk of chunks) { + out.set(chunk, offset); + offset += chunk.byteLength; + } + return out; + } + async function decompressZipEntry(data, method, uncompressedSize) { + if (method === 0) { + if (data.length > ZIP_MAX_ENTRY_UNCOMPRESSED_BYTES) throw new Error("ZIP entry exceeds size limit"); + return data; + } + if (method === 8) { + const reader = new Blob([data]).stream().pipeThrough(new DecompressionStream("deflate-raw")).getReader(); + const chunks = []; + let total = 0; + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + total += value.byteLength; + if (total > ZIP_MAX_ENTRY_UNCOMPRESSED_BYTES) throw new Error("ZIP entry exceeds size limit"); + if (data.length > 0 && total / data.length > ZIP_MAX_COMPRESSION_RATIO) throw new Error("ZIP entry compression ratio exceeds limit"); + chunks.push(value); + } + } catch (err) { + try { + await reader.cancel(); + } catch {} + throw err; + } + const out = concatUint8Arrays(chunks, total); + if (uncompressedSize && out.length !== uncompressedSize) return out.slice(0, Math.min(out.length, uncompressedSize)); + return out; + } + throw new Error(`Unsupported ZIP compression method ${method}`); + } + function normalizeZipEntryName(name) { + return name.replace(/\\/g, "/").replace(/^\.\//, ""); + } + function findArchiveEntry(entries, fileName) { + return entries.find((e) => e.name === fileName); + } + function isIgnoredArchiveEntry(name) { + if (name === ".DS_Store" || name.endsWith("/.DS_Store")) return true; + return name.split("/").some((part) => part.startsWith("._") || part === "__MACOSX"); + } + async function extractArchiveFromZipBuffer(buffer) { + const entries = await unzip(buffer); + const markupEntry = findArchiveEntry(entries, "document.xml"); + if (!markupEntry) throw new Error("Archive must contain document.xml"); + const markupXml = new TextDecoder().decode(markupEntry.data); + const pageImages = /* @__PURE__ */ new Map(); + const assetUrls = /* @__PURE__ */ new Map(); + for (const e of entries) { + const m = e.name.match(/^pages\/(\d+)\.(png|jpe?g|webp)$/i); + if (m) { + const ext = m[2].toLowerCase().replace("jpeg", "jpg"); + const mime = ext === "png" ? "image/png" : ext === "webp" ? "image/webp" : "image/jpeg"; + pageImages.set(Number(m[1]), URL.createObjectURL(new Blob([e.data], { type: mime }))); + continue; + } + if (e.name.startsWith("assets/") && !e.name.endsWith("/")) assetUrls.set(e.name, URL.createObjectURL(new Blob([e.data], { type: mimeFromAssetPath(e.name) }))); + } + return { + markupXml, + pageImages, + assetUrls + }; + } + function mimeFromAssetPath(path) { + switch (path.split(".").pop()?.toLowerCase() ?? "") { + case "png": return "image/png"; + case "jpg": + case "jpeg": return "image/jpeg"; + case "webp": return "image/webp"; + case "gif": return "image/gif"; + case "svg": return "image/svg+xml"; + default: return "application/octet-stream"; + } + } + var ZIP_MAX_ENTRIES, ZIP_MAX_ENTRY_COMPRESSED_BYTES, ZIP_MAX_ENTRY_UNCOMPRESSED_BYTES, ZIP_MAX_TOTAL_UNCOMPRESSED_BYTES, ZIP_MAX_COMPRESSION_RATIO; + var init_zip = __esmMin((() => { + ZIP_MAX_ENTRIES = 5e3; + ZIP_MAX_ENTRY_COMPRESSED_BYTES = 134217728; + ZIP_MAX_ENTRY_UNCOMPRESSED_BYTES = 134217728; + ZIP_MAX_TOTAL_UNCOMPRESSED_BYTES = 536870912; + ZIP_MAX_COMPRESSION_RATIO = 100; + })); + //#endregion + //#region src/doclang/document.ts + var document_exports = /* @__PURE__ */ __exportAll({ + NO_MARKUP: () => NO_MARKUP, + PAGE_IMAGE_RE: () => PAGE_IMAGE_RE, + assignElementIds: () => assignElementIds, + buildDocumentState: () => buildDocumentState, + buildElementPageMap: () => buildElementPageMap, + buildThreadNavByElement: () => buildThreadNavByElement, + buildThreadPagesById: () => buildThreadPagesById, + collectBoundingBoxes: () => collectBoundingBoxes, + collectCaptionLinks: () => collectCaptionLinks, + collectFragmentLinks: () => collectFragmentLinks, + collectFragmentNavItems: () => collectFragmentNavItems, + collectReadingOrderSteps: () => collectReadingOrderSteps, + collectXrefLinks: () => collectXrefLinks, + computeReadingOrder: () => computeReadingOrder, + computeReadingOrderDisplayNumbers: () => computeReadingOrderDisplayNumbers, + extractArchiveFromFiles: () => extractArchiveFromFiles, + extractArchiveFromZipBuffer: () => extractArchiveFromZipBuffer, + invertElementIds: () => invertElementIds, + readDefaultResolution: () => readDefaultResolution, + revokeDocumentState: () => revokeDocumentState, + segmentHasMarkup: () => segmentHasMarkup, + serializeSegment: () => serializeSegment, + splitIntoSegments: () => splitIntoSegments + }); + function splitIntoSegments(root) { + const body = childElements(root).filter((el) => localName(el) !== "head"); + const segments = [[]]; + for (const el of body) if (localName(el) === "page_break") segments.push([]); + else segments[segments.length - 1].push(el); + return segments.length ? segments : [[]]; + } + function segmentHasMarkup(segment) { + return segment.some((el) => el.nodeType === Node.ELEMENT_NODE); + } + function readDefaultResolution(head) { + const fallback = { + width: 512, + height: 512 + }; + if (!head) return fallback; + const dr = childElements(head).find((el) => localName(el) === "default_resolution"); + if (!dr) return fallback; + const w = parseInt(dr.getAttribute("width") ?? "512", 10); + const h = parseInt(dr.getAttribute("height") ?? "512", 10); + return { + width: Number.isFinite(w) && w > 0 ? w : 512, + height: Number.isFinite(h) && h > 0 ? h : 512 + }; + } + function assignElementIds(segment) { + const ids = /* @__PURE__ */ new Map(); + let counter = 0; + walkElements(segment, (el) => { + ids.set(el, `el-${counter++}`); + }); + return ids; + } + function invertElementIds(elementIds) { + const idToElement = /* @__PURE__ */ new Map(); + for (const [el, id] of elementIds) idToElement.set(id, el); + return idToElement; + } + function buildThreadsById(docRoot) { + const map = /* @__PURE__ */ new Map(); + walkElements(childElements(docRoot).filter((el) => localName(el) !== "head"), (el) => { + for (const child of childElements(el)) { + if (localName(child) !== "thread") continue; + const threadId = child.getAttribute("thread_id"); + if (!threadId) continue; + if (!map.has(threadId)) map.set(threadId, []); + map.get(threadId).push(el); + } + }); + return map; + } + function buildElementPageMap(segments) { + const elementPage = /* @__PURE__ */ new Map(); + segments.forEach((segment, idx) => { + const pageNum = idx + 1; + walkElements(segment, (el) => elementPage.set(el, pageNum)); + }); + return elementPage; + } + function buildThreadPagesById(docRoot, elementPageByEl) { + const threadPagesById = /* @__PURE__ */ new Map(); + for (const [threadId, elements] of buildThreadsById(docRoot)) { + const pages = /* @__PURE__ */ new Set(); + for (const el of elements) { + const page = elementPageByEl.get(el); + if (page) pages.add(page); + } + if (pages.size) threadPagesById.set(threadId, pages); + } + return threadPagesById; + } + function buildThreadNavByElement(docRoot) { + const nav = /* @__PURE__ */ new Map(); + for (const [, elements] of buildThreadsById(docRoot)) for (let i = 0; i < elements.length; i += 1) nav.set(elements[i], { + prev: i > 0 ? elements[i - 1] : null, + next: i < elements.length - 1 ? elements[i + 1] : null + }); + return nav; + } + function isReadingOrderUnit(el) { + const tag = localName(el); + if (tag === "caption") return true; + if (HEAD_TAGS.has(tag) || tag === "location" || tag === "h_thread" || tag === "page_break") return false; + if (tag === "nl" || CELL_SPAN_TAGS.has(tag)) return false; + if (RENDER_FORMAT_TAGS$1.has(tag) || tag === "src" || tag === "checkbox") return false; + return true; + } + function computeReadingOrder(docRoot) { + const bodyChildren = childElements(docRoot).filter((el) => localName(el) !== "head"); + const threadsById = buildThreadsById(docRoot); + const consumedViaXref = /* @__PURE__ */ new Set(); + const order = []; + function record(el) { + if (!isReadingOrderUnit(el)) return; + order.push(el); + } + function consumeThread(threadId) { + for (const target of threadsById.get(threadId) ?? []) { + if (consumedViaXref.has(target)) continue; + consumedViaXref.add(target); + visitElement(target); + } + } + function walkChildren(parent) { + for (const child of parent.childNodes) { + if (child.nodeType !== Node.ELEMENT_NODE) continue; + const el = child; + const tag = localName(el); + if (tag === "xref") { + const threadId = el.getAttribute("thread_id"); + if (threadId) consumeThread(threadId); + continue; + } + if (tag === "page_break") continue; + visitElement(el); + } + } + function visitElement(el) { + record(el); + walkChildren(el); + } + for (const el of bodyChildren) { + if (localName(el) === "page_break") continue; + if (consumedViaXref.has(el)) continue; + visitElement(el); + } + return order; + } + function isReadingOrderOverlayUnit(el) { + if (!isReadingOrderUnit(el)) return false; + if (isPictureContentElement(el) || isTableContentElement(el)) return false; + if (headLocations(el).length === 4) return true; + return isVirtualTextOverlayUnit(el); + } + function computeReadingOrderDisplayNumbers(readingOrder) { + const numbers = /* @__PURE__ */ new Map(); + let n = 0; + for (const el of readingOrder) { + if (!isReadingOrderOverlayUnit(el)) continue; + n += 1; + numbers.set(el, n); + } + return numbers; + } + function pushBoundingBox(boxes, locs, defaultResolution, kind, tag, elementId, layer = "body") { + const [x0el, y0el, x1el, y1el] = locs; + const resW = locationResolution(x0el, defaultResolution.width); + const resH = locationResolution(y0el, defaultResolution.height); + boxes.push({ + kind, + tag, + elementId, + layer, + x0: parseInt(x0el.getAttribute("value") ?? "0", 10), + y0: parseInt(y0el.getAttribute("value") ?? "0", 10), + x1: parseInt(x1el.getAttribute("value") ?? "0", 10), + y1: parseInt(y1el.getAttribute("value") ?? "0", 10), + resW, + resH + }); + } + function collectListVirtualTextBoxes(list, defaultResolution, boxes, elementIds) { + const nodes = [...list.childNodes]; + let i = skipContainerLevelHead(nodes, 0); + while (i < nodes.length) { + const node = nodes[i]; + if (node.nodeType !== Node.ELEMENT_NODE || localName(node) !== "ldiv") { + i += 1; + continue; + } + i += 1; + const head = parseElementHeadAt(nodes, i); + if (head) { + const elementId = elementIds.get(node); + if (elementId) pushBoundingBox(boxes, head.locs, defaultResolution, "text", "text", elementId, elementLayer(node)); + i = head.nextIndex; + } + i = skipUntilListItemBoundary(nodes, i); + } + } + function collectTableVirtualTextBoxes(container, defaultResolution, boxes, elementIds) { + const nodes = [...container.childNodes]; + let i = skipContainerLevelHead(nodes, 0); + while (i < nodes.length) { + const node = nodes[i]; + if (node.nodeType !== Node.ELEMENT_NODE) { + i += 1; + continue; + } + const tag = localName(node); + if (!isCellToken(tag)) { + i += 1; + continue; + } + if (tag === "nl") { + i += 1; + continue; + } + i += 1; + const head = parseElementHeadAt(nodes, i); + if (head) { + const elementId = elementIds.get(node); + if (elementId) pushBoundingBox(boxes, head.locs, defaultResolution, "text", "text", elementId, elementLayer(node)); + i = head.nextIndex; + } + i = skipUntilCellBoundary(nodes, i); + } + } + function collectBoundingBoxes(segment, defaultResolution, elementIds) { + const boxes = []; + walkElements(segment, (el) => { + const locs = headLocations(el); + if (locs.length !== 4) return; + const elementId = elementIds.get(el); + if (!elementId) return; + pushBoundingBox(boxes, locs, defaultResolution, localName(el), elementLabel(el), elementId, elementLayer(el)); + }); + walkElements(segment, (el) => { + const tag = localName(el); + if (tag === "list") collectListVirtualTextBoxes(el, defaultResolution, boxes, elementIds); + else if (OTSL_CONTAINER_TAGS.has(tag)) collectTableVirtualTextBoxes(el, defaultResolution, boxes, elementIds); + }); + return boxes; + } + function collectCaptionLinks(segment, elementIds, boxes) { + const boxById = new Map(boxes.map((b) => [b.elementId, b])); + const links = []; + walkElements(segment, (el) => { + if (localName(el) !== "caption") return; + const captionId = elementIds.get(el); + const captionBox = captionId ? boxById.get(captionId) : null; + if (!captionBox) return; + const host = el.parentElement; + if (!host || headLocations(host).length !== 4) return; + const hostId = elementIds.get(host); + const hostBox = hostId ? boxById.get(hostId) : null; + if (!hostBox) return; + links.push({ + captionBox, + hostBox, + captionElementId: captionId, + hostElementId: hostId + }); + }); + return links; + } + function collectXrefLinks(segment, elementIds, boxes) { + const boxById = new Map(boxes.map((b) => [b.elementId, b])); + const threadsById = /* @__PURE__ */ new Map(); + walkElements(segment, (el) => { + const elementId = elementIds.get(el); + const box = elementId ? boxById.get(elementId) : null; + if (!box) return; + for (const thread of childElements(el)) { + if (localName(thread) !== "thread") continue; + const threadId = thread.getAttribute("thread_id"); + if (!threadId) continue; + if (!threadsById.has(threadId)) threadsById.set(threadId, []); + threadsById.get(threadId).push({ + elementId, + box + }); + } + }); + const links = []; + walkElements(segment, (el) => { + const xrefs = childElements(el).filter((c) => localName(c) === "xref"); + if (!xrefs.length) return; + const from = findNearestLocatedBox(el, elementIds, boxById); + if (!from) return; + for (const xref of xrefs) { + const threadId = xref.getAttribute("thread_id"); + if (!threadId) continue; + for (const { elementId: toId, box: toBox } of threadsById.get(threadId) ?? []) { + if (toId === from.elementId) continue; + links.push({ + fromBox: from.box, + toBox, + fromElementId: from.elementId, + toElementId: toId + }); + } + } + }); + return links; + } + function findNearestLocatedBox(el, elementIds, boxById) { + let node = el; + while (node) { + const elementId = elementIds.get(node); + const box = elementId ? boxById.get(elementId) : null; + if (box) return { + elementId, + box + }; + if (localName(node) === "doclang") break; + node = node.parentElement; + } + return null; + } + function collectFragmentLinks(segment, elementIds, boxes, pageNum, threadPagesById) { + const boxById = new Map(boxes.map((b) => [b.elementId, b])); + const onPage = /* @__PURE__ */ new Map(); + walkElements(segment, (el) => { + const threadId = firstHeadChild(el, "thread")?.getAttribute("thread_id"); + if (!threadId) return; + const elementId = elementIds.get(el); + const box = elementId ? boxById.get(elementId) : null; + if (!box) return; + if (!onPage.has(threadId)) onPage.set(threadId, []); + onPage.get(threadId).push({ + elementId, + box + }); + }); + const links = []; + for (const [threadId, members] of onPage) { + if (members.length >= 2) { + for (let i = 0; i < members.length - 1; i += 1) links.push({ + fromBox: members[i].box, + toBox: members[i + 1].box, + fromElementId: members[i].elementId, + toElementId: members[i + 1].elementId, + threadId + }); + continue; + } + if (members.length !== 1) continue; + const threadPages = threadPagesById.get(threadId); + if (!threadPages) continue; + const hasPrevious = [...threadPages].some((p) => p < pageNum); + const hasFollowing = [...threadPages].some((p) => p > pageNum); + if (!hasPrevious && !hasFollowing) continue; + const base = { + fromBox: members[0].box, + toBox: null, + fromElementId: members[0].elementId, + toElementId: null, + threadId + }; + if (hasPrevious) links.push({ + ...base, + targetCorner: "tl" + }); + if (hasFollowing) links.push({ + ...base, + targetCorner: "br" + }); + } + return links; + } + function collectFragmentNavItems(segment, elementIds, boxes, threadNavByElement) { + const boxById = new Map(boxes.map((b) => [b.elementId, b])); + const items = []; + walkElements(segment, (el) => { + const nav = threadNavByElement.get(el); + if (!nav || !nav.prev && !nav.next) return; + const elementId = elementIds.get(el); + const box = elementId ? boxById.get(elementId) : null; + if (!box) return; + items.push({ + elementId, + box, + hasPrev: nav.prev !== null, + hasNext: nav.next !== null + }); + }); + return items; + } + function collectReadingOrderSteps(segment, elementIds, boxes, readingOrder, globalNumbering = true, displayNumbers = null) { + const boxById = new Map(boxes.map((b) => [b.elementId, b])); + const steps = []; + let pageOrder = 0; + readingOrder.forEach((el) => { + if (isPictureContentElement(el) || isTableContentElement(el)) return; + const elementId = elementIds.get(el); + if (!elementId) return; + const box = boxById.get(elementId); + if (!box) return; + pageOrder += 1; + steps.push({ + order: globalNumbering ? displayNumbers?.get(el) ?? pageOrder : pageOrder, + box, + elementId + }); + }); + return steps; + } + function buildDocumentState(markupXml, pageImages, label, assetUrls, { markupOnly }) { + const doc = new DOMParser().parseFromString(markupXml, "application/xml"); + if (doc.querySelector("parsererror")) { + alert(`Invalid XML in ${label}`); + return null; + } + const root = doc.documentElement; + if (localName(root) !== "doclang") { + alert(`${label}: root element must be `); + return null; + } + const defaultResolution = readDefaultResolution(childElements(root).find((el) => localName(el) === "head") ?? null); + const segments = markupOnly ? [childElements(root).filter((el) => localName(el) !== "head")] : splitIntoSegments(root); + const hasPageView = !markupOnly && pageImages.size > 0; + const maxImagePage = hasPageView ? Math.max(...pageImages.keys()) : 0; + const pageCount = markupOnly ? 1 : Math.max(segments.length, maxImagePage, 1); + const readingOrder = computeReadingOrder(root); + const elementPageByEl = buildElementPageMap(segments); + return { + pageImages, + assetUrls, + currentPage: 1, + pageCount, + segments, + defaultResolution, + elementIds: /* @__PURE__ */ new Map(), + idToElement: /* @__PURE__ */ new Map(), + hasPageView, + markupOnly, + docRoot: root, + threadPagesById: buildThreadPagesById(root, elementPageByEl), + elementPageByEl, + threadNavByElement: buildThreadNavByElement(root), + pendingSelectElement: null, + readingOrder, + readingOrderDisplayNumbers: computeReadingOrderDisplayNumbers(readingOrder), + pageViewOverlay: null + }; + } + async function extractArchiveFromFiles(files) { + const markupFile = files.find((f) => f.name === "document.xml"); + if (!markupFile) throw new Error("Archive must contain document.xml at its root."); + const markupXml = await markupFile.text(); + const pageImages = /* @__PURE__ */ new Map(); + const assetUrls = /* @__PURE__ */ new Map(); + for (const f of files) { + const relPath = f.webkitRelativePath || f.name; + const parts = relPath.split("/"); + if (parts.length >= 2 && parts[parts.length - 2] === "pages") { + const m = PAGE_IMAGE_RE.exec(f.name); + if (m) pageImages.set(Number(m[1]), URL.createObjectURL(f)); + } + const assetPath = archiveRelativeAssetPath(relPath); + if (assetPath) assetUrls.set(assetPath, URL.createObjectURL(f)); + } + return { + markupXml, + pageImages, + assetUrls + }; + } + function revokeDocumentState(docState) { + if (!docState) return; + for (const url of docState.pageImages.values()) if (url.startsWith("blob:")) URL.revokeObjectURL(url); + for (const url of docState.assetUrls.values()) if (url.startsWith("blob:")) URL.revokeObjectURL(url); + } + function serializeSegment(segment) { + return segment.map((el) => serializeElement(el, 0)).join("\n"); + } + function serializeElement(el, depth) { + const pad = " ".repeat(depth); + const tag = localName(el); + const attrs = [...el.attributes].filter((a) => a.name !== "xmlns" || a.value !== "https://www.doclang.ai/ns/v0").map((a) => `${a.name}="${a.value}"`).join(" "); + const attrStr = attrs ? ` ${attrs}` : ""; + if (!el.childNodes.length) return `${pad}<${tag}${attrStr}/>`; + const meaningfulText = [...el.childNodes].filter((n) => isTextLikeNode(n) && !isWhitespaceOnlyText(n)); + if (meaningfulText.length > 0 && meaningfulText.every(isTextLikeNode) && !childElements(el).length) { + const text = serializeMarkupTextNodes(el.childNodes); + if (text) return `${pad}<${tag}${attrStr}>${text}`; + } + const parts = [`${pad}<${tag}${attrStr}>`]; + for (const child of el.childNodes) if (isTextLikeNode(child)) { + const text = formatMarkupTextNode(child); + if (text) parts.push(" ".repeat(depth + 1) + text); + } else if (child.nodeType === Node.ELEMENT_NODE) parts.push(serializeElement(child, depth + 1)); + parts.push(`${pad}`); + return parts.join("\n"); + } + var PAGE_IMAGE_RE, NO_MARKUP, RENDER_FORMAT_TAGS$1; + var init_document = __esmMin((() => { + init_dom(); + init_zip(); + PAGE_IMAGE_RE = /^(\d+)\.(png|jpe?g|webp)$/i; + NO_MARKUP = "(No markup to be shown.)"; + RENDER_FORMAT_TAGS$1 = /* @__PURE__ */ new Set([ + "bold", + "italic", + "underline", + "strikethrough", + "superscript", + "subscript", + "handwriting", + "rtl", + "content" + ]); + })); + //#endregion + //#region src/components/base/page-element.ts + /** + * Base class for doclang web components that render document content. + * + * Supports three ways to provide the DocLang document: + * + * 1. `src` attribute — a URL (http:, data:, blob:) pointing to either a + * raw XML markup string or a .dclx/.zip archive. The component fetches + * and parses it automatically. + * + * 2. Inline child `