From 512754b04f545382773935780daafcbaa8c9a5fc Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 27 Feb 2026 20:02:30 -0500 Subject: [PATCH 1/4] Add IDF Monaco editor, schema-driven field metadata pills, and TOC/math fixes Integrate a browser-side Monaco editor for IDF code blocks with hover documentation powered by idfkit epJSON schemas. Each IDF field heading gets inline HTML pills showing type, units, default, range, and constraints. The editor bundle is built from TypeScript/Vite source in idf-editor/ and bundled into scripts/assets/. Also fixes LaTeX equation rendering (enable pymdownx.arithmatex), adds a collapsible right-side TOC with mobile hamburger menu support, a custom Pygments IDF lexer, and optimizes bracket macro expansion in the LaTeX preprocessor. Co-Authored-By: Claude Opus 4.6 --- .claude/launch.json | 35 + .github/workflows/convert-docs.yml | 28 +- .gitignore | 4 + CLAUDE.md | 21 + Makefile | 9 +- idf-editor/package-lock.json | 1160 +++++++++++++++++++++++++++ idf-editor/package.json | 17 + idf-editor/src/editor-manager.ts | 294 +++++++ idf-editor/src/idd-schema-loader.ts | 85 ++ idf-editor/src/idf-editor.css | 80 ++ idf-editor/src/idf-hover-service.ts | 255 ++++++ idf-editor/src/idf-language.ts | 226 ++++++ idf-editor/src/idf-themes.ts | 93 +++ idf-editor/src/main.ts | 126 +++ idf-editor/src/types.ts | 77 ++ idf-editor/tsconfig.json | 24 + idf-editor/vite.config.ts | 22 + pyproject.toml | 11 + scripts/assets/idf-editor.css | 1 + scripts/assets/idf-editor.js | 4 + scripts/assets/idf-fields.css | 128 +++ scripts/assets/theme-overrides.css | 53 ++ scripts/config.py | 2 +- scripts/convert.py | 47 +- scripts/latex_preprocessor.py | 47 +- scripts/markdown_postprocessor.py | 125 +++ scripts/pygments_idf_lexer.py | 53 ++ scripts/schema_utils.py | 355 ++++++++ uv.lock | 13 +- zensical.toml | 7 + 30 files changed, 3366 insertions(+), 36 deletions(-) create mode 100644 .claude/launch.json create mode 100644 idf-editor/package-lock.json create mode 100644 idf-editor/package.json create mode 100644 idf-editor/src/editor-manager.ts create mode 100644 idf-editor/src/idd-schema-loader.ts create mode 100644 idf-editor/src/idf-editor.css create mode 100644 idf-editor/src/idf-hover-service.ts create mode 100644 idf-editor/src/idf-language.ts create mode 100644 idf-editor/src/idf-themes.ts create mode 100644 idf-editor/src/main.ts create mode 100644 idf-editor/src/types.ts create mode 100644 idf-editor/tsconfig.json create mode 100644 idf-editor/vite.config.ts create mode 100644 scripts/assets/idf-editor.css create mode 100644 scripts/assets/idf-editor.js create mode 100644 scripts/assets/idf-fields.css create mode 100644 scripts/assets/theme-overrides.css create mode 100644 scripts/pygments_idf_lexer.py create mode 100644 scripts/schema_utils.py diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 000000000..8ee3a9211 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "zensical-dev", + "runtimeExecutable": "bash", + "runtimeArgs": [ + "-c", + "cd build/v25.2 && uv run zensical serve -a localhost:$PORT" + ], + "port": 8000, + "autoPort": true + }, + { + "name": "dist-server", + "runtimeExecutable": "bash", + "runtimeArgs": [ + "-c", + "uv run python -m http.server $PORT --directory dist" + ], + "port": 8003, + "autoPort": true + }, + { + "name": "idf-editor-dev", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", + "dev" + ], + "cwd": "idf-editor", + "port": 5173 + } + ] +} diff --git a/.github/workflows/convert-docs.yml b/.github/workflows/convert-docs.yml index f0883b509..31d285ee9 100644 --- a/.github/workflows/convert-docs.yml +++ b/.github/workflows/convert-docs.yml @@ -75,10 +75,28 @@ jobs: sudo apt-get update sudo apt-get install -y pandoc + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: idf-editor/package-lock.json + + - name: Build IDF editor bundle + run: | + cd idf-editor && npm ci --silent && npm run build + cp dist/idf-editor.js dist/idf-editor.css ../scripts/assets/ + + - name: Compute short version + id: short + run: | + SHORT=$(uv run python -c "from scripts.config import version_to_short; print(version_to_short('${{ matrix.version }}'))") + echo "version_short=${SHORT}" >> "$GITHUB_OUTPUT" + - name: Compute cache key id: cache-key run: | - SCRIPTS_HASH=$(find scripts/ -type f -name '*.py' -o -name '*.lua' | sort | xargs sha256sum | sha256sum | cut -d' ' -f1) + SCRIPTS_HASH=$(find scripts/ idf-editor/src/ -type f \( -name '*.py' -o -name '*.lua' -o -name '*.ts' -o -name '*.css' \) | sort | xargs sha256sum | sha256sum | cut -d' ' -f1) echo "key=docs-${{ matrix.version }}-${SCRIPTS_HASH}" >> "$GITHUB_OUTPUT" - name: Cache built version @@ -86,15 +104,9 @@ jobs: if: ${{ !inputs.force_rebuild }} uses: actions/cache@v4 with: - path: build/${{ matrix.version_short }} + path: build/${{ steps.short.outputs.version_short }} key: ${{ steps.cache-key.outputs.key }} - - name: Compute short version - id: short - run: | - SHORT=$(uv run python -c "from scripts.config import version_to_short; print(version_to_short('${{ matrix.version }}'))") - echo "version_short=${SHORT}" >> "$GITHUB_OUTPUT" - - name: Clone EnergyPlus source (sparse, doc/ only) if: steps.cache.outputs.cache-hit != 'true' run: | diff --git a/.gitignore b/.gitignore index 09109dfa2..7993cf83e 100644 --- a/.gitignore +++ b/.gitignore @@ -28,5 +28,9 @@ __pycache__/ .DS_Store Thumbs.db +# IDF editor build artifacts +idf-editor/dist/ +idf-editor/node_modules/ + # Pre-commit cache .cache diff --git a/CLAUDE.md b/CLAUDE.md index e998b989f..82db1c4dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -135,6 +135,27 @@ All workflows are in `.github/workflows/`: A reusable action at `.github/actions/setup-python-env/action.yml` handles Python + uv setup. +## IDF Editor (Monaco) Development + +The `idf-editor/` directory contains a TypeScript/Vite project that builds a browser-side Monaco editor bundle for IDF code blocks. When making CSS or JS changes to the editor: + +1. **Edit source** in `idf-editor/src/` (e.g., `editor-manager.ts`, `idf-editor.css`) +2. **Build the bundle:** `cd idf-editor && npm run build` +3. **Copy to assets:** `cp idf-editor/dist/idf-editor.{js,css} scripts/assets/` +4. **Rebuild the docs:** `make convert VERSION=v25.2.0` (this copies `scripts/assets/` into the build output) + +For iterating quickly without a full `make convert`, you can copy assets directly into the build output and rebuild Zensical: + +```bash +cp scripts/assets/idf-editor.{js,css} build/v25.2/docs/assets/ +cd build/v25.2 && uv run zensical build --clean +``` + +**Important:** Always use `--clean` (or delete `build/vXX.X/.cache/`) when: +- Adding or changing a Pygments lexer (e.g., the IDF lexer that produces `language-idf` CSS classes) +- Changing code block language detection +- Zensical caches rendered HTML and won't pick up new lexer registrations without clearing the cache + ## Conventions - All Python code is formatted and linted by Ruff. Run `make check` to validate. diff --git a/Makefile b/Makefile index 654e23f3e..ea501c72a 100644 --- a/Makefile +++ b/Makefile @@ -41,8 +41,15 @@ serve: ## Serve the full multi-version site from dist/ @echo "Serving full site from dist/ on http://localhost:8003" @uv run python -m http.server 8003 --directory dist +.PHONY: build-editor +build-editor: ## Build the IDF Monaco editor bundle + @echo "Building IDF editor bundle..." + @cd idf-editor && npm ci --silent && npm run build + @cp idf-editor/dist/idf-editor.js idf-editor/dist/idf-editor.css scripts/assets/ + @echo "IDF editor bundle built and copied to scripts/assets/" + .PHONY: convert -convert: ## Convert a single EnergyPlus version (usage: make convert VERSION=v25.2.0) +convert: build-editor ## Convert a single EnergyPlus version (usage: make convert VERSION=v25.2.0) @if [ -z "$(VERSION)" ]; then echo "Usage: make convert VERSION=v25.2.0"; exit 1; fi @echo "Converting EnergyPlus $(VERSION)..." @mkdir -p build/sources diff --git a/idf-editor/package-lock.json b/idf-editor/package-lock.json new file mode 100644 index 000000000..ce36182ca --- /dev/null +++ b/idf-editor/package-lock.json @@ -0,0 +1,1160 @@ +{ + "name": "idf-editor-bundle", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "idf-editor-bundle", + "version": "1.0.0", + "dependencies": { + "monaco-editor": "^0.55.1" + }, + "devDependencies": { + "typescript": "~5.7.2", + "vite": "^6.0.7" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "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", + "optional": true + }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "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/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/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "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/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.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "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.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/typescript": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + } + } +} diff --git a/idf-editor/package.json b/idf-editor/package.json new file mode 100644 index 000000000..faaac1fa7 --- /dev/null +++ b/idf-editor/package.json @@ -0,0 +1,17 @@ +{ + "name": "idf-editor-bundle", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite build --watch" + }, + "dependencies": { + "monaco-editor": "0.55.1" + }, + "devDependencies": { + "vite": "^6.0.7", + "typescript": "~5.7.2" + } +} diff --git a/idf-editor/src/editor-manager.ts b/idf-editor/src/editor-manager.ts new file mode 100644 index 000000000..903629458 --- /dev/null +++ b/idf-editor/src/editor-manager.ts @@ -0,0 +1,294 @@ +/** + * Editor Manager + * + * Manages the lifecycle of Monaco editor instances embedded in documentation + * pages. Handles: + * - Discovering IDF code blocks on the page + * - Lazy-loading editors via IntersectionObserver + * - Creating read-only Monaco editor instances + * - Theme synchronization with Zensical's dark/light toggle + * - Cleanup on page navigation (instant nav) + * - Copy-to-clipboard button + */ + +import type * as Monaco from 'monaco-editor'; +import { getCurrentTheme, THEME_DARK, THEME_LIGHT } from './idf-themes'; + +/** Minimum editor height in pixels (3 lines) */ +const MIN_HEIGHT = 60; +/** Maximum editor height before scrolling (40 lines) */ +const MAX_HEIGHT = 720; +/** Line height for height calculation (matches fontSize 13) */ +const LINE_HEIGHT = 19; +/** Padding (top + bottom, matches Monaco padding option) */ +const PADDING = 20; +/** IntersectionObserver rootMargin for pre-loading */ +const PRELOAD_MARGIN = '200px'; +/** Minimum viewport width to enable Monaco (skip on mobile/narrow screens) */ +const MIN_VIEWPORT_WIDTH = 768; + +/** Monaco editor version to load from CDN */ +const MONACO_VERSION = '0.55.1'; +const MONACO_CDN_BASE = `https://cdn.jsdelivr.net/npm/monaco-editor@${MONACO_VERSION}/min/vs`; + +/** Copy icon SVG */ +const COPY_ICON = ``; +const CHECK_ICON = ``; + +/** Tracked editor instance */ +interface EditorInstance { + editor: Monaco.editor.IStandaloneCodeEditor; + container: HTMLElement; +} + +export class EditorManager { + private monaco: typeof Monaco; + private editors: EditorInstance[] = []; + private observer: IntersectionObserver | null = null; + private themeObserver: MutationObserver | null = null; + private pendingBlocks: Map = new Map(); + + constructor(monaco: typeof Monaco) { + this.monaco = monaco; + } + + /** + * Initialize the editor manager for the current page. + * Finds all IDF code blocks and sets up lazy loading. + */ + initialize(codeBlocks: NodeListOf): void { + if (codeBlocks.length === 0) return; + + // Set up IntersectionObserver for lazy loading + this.observer = new IntersectionObserver( + (entries) => this.handleIntersection(entries), + { rootMargin: PRELOAD_MARGIN } + ); + + // Process each code block. + // Structure:
......
+ codeBlocks.forEach((codeEl) => { + const pre = codeEl.parentElement; + if (!pre || pre.tagName !== 'PRE') return; + + // The wrapper div has the language-idf class + const wrapper = pre.parentElement; + if (!wrapper) return; + + // Skip if already converted + if (wrapper.dataset.idfEditor === 'true') return; + + const code = codeEl.textContent || ''; + this.pendingBlocks.set(wrapper, { wrapper: wrapper as HTMLElement, code }); + this.observer!.observe(wrapper); + }); + + // Set up theme change listener + this.watchThemeChanges(); + } + + /** Dispose all editors and observers */ + dispose(): void { + // Dispose editors + for (const { editor } of this.editors) { + editor.dispose(); + } + this.editors = []; + + // Disconnect observers + this.observer?.disconnect(); + this.observer = null; + this.themeObserver?.disconnect(); + this.themeObserver = null; + + // Clear pending blocks + this.pendingBlocks.clear(); + } + + /** Handle IntersectionObserver callbacks */ + private handleIntersection(entries: IntersectionObserverEntry[]): void { + for (const entry of entries) { + if (!entry.isIntersecting) continue; + + const block = this.pendingBlocks.get(entry.target); + if (!block) continue; + + // Stop observing this element + this.observer?.unobserve(entry.target); + this.pendingBlocks.delete(entry.target); + + // Create the editor + this.createEditor(block.wrapper, block.code); + } + } + + /** Create a Monaco editor replacing the given wrapper element */ + private createEditor(wrapper: HTMLElement, code: string): void { + // Calculate height based on line count + const lineCount = code.split('\n').length; + const height = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, lineCount * LINE_HEIGHT + PADDING)); + + // Create container + const container = document.createElement('div'); + container.className = 'idf-editor-container'; + container.style.height = `${height}px`; + + // Replace the wrapper
element + wrapper.parentNode?.replaceChild(container, wrapper); + wrapper.dataset.idfEditor = 'true'; + + // Create the editor + const editor = this.monaco.editor.create(container, { + value: code, + language: 'idf', + theme: getCurrentTheme(), + readOnly: true, + domReadOnly: true, + minimap: { enabled: false }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + wordWrap: 'off', + folding: false, + glyphMargin: false, + lineDecorationsWidth: 8, + lineNumbersMinChars: 3, + renderLineHighlight: 'none', + overviewRulerLanes: 0, + hideCursorInOverviewRuler: true, + overviewRulerBorder: false, + scrollbar: { + vertical: 'auto', + horizontal: 'auto', + verticalScrollbarSize: 8, + horizontalScrollbarSize: 8, + }, + fontSize: 13, + fontFamily: "'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, monospace", + padding: { top: 8, bottom: 8 }, + automaticLayout: true, + contextmenu: false, + links: false, + renderValidationDecorations: 'off', + // Render hover/suggestion widgets outside the editor so they aren't clipped + fixedOverflowWidgets: true, + // Accessibility for read-only blocks + accessibilitySupport: 'off', + ariaLabel: 'EnergyPlus IDF code example', + }); + + // Add copy button + this.addCopyButton(container, editor); + + // Track the editor + this.editors.push({ editor, container }); + } + + /** Add a copy-to-clipboard button to the editor container */ + private addCopyButton(container: HTMLElement, editor: Monaco.editor.IStandaloneCodeEditor): void { + const btn = document.createElement('button'); + btn.className = 'idf-editor-copy'; + btn.title = 'Copy to clipboard'; + btn.innerHTML = COPY_ICON; + + btn.addEventListener('click', async () => { + try { + const text = editor.getValue(); + await navigator.clipboard.writeText(text); + btn.innerHTML = CHECK_ICON; + btn.classList.add('copied'); + setTimeout(() => { + btn.innerHTML = COPY_ICON; + btn.classList.remove('copied'); + }, 2000); + } catch { + // Fallback for older browsers + const textarea = document.createElement('textarea'); + textarea.value = editor.getValue(); + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + btn.innerHTML = CHECK_ICON; + btn.classList.add('copied'); + setTimeout(() => { + btn.innerHTML = COPY_ICON; + btn.classList.remove('copied'); + }, 2000); + } + }); + + container.appendChild(btn); + } + + /** Watch for Zensical theme changes and update all editors */ + private watchThemeChanges(): void { + this.themeObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.attributeName === 'data-md-color-scheme') { + const scheme = document.body.getAttribute('data-md-color-scheme'); + const theme = scheme === 'slate' ? THEME_DARK : THEME_LIGHT; + this.monaco.editor.setTheme(theme); + } + } + }); + + this.themeObserver.observe(document.body, { + attributes: true, + attributeFilter: ['data-md-color-scheme'], + }); + } +} + +/** + * Load Monaco editor from CDN using the AMD loader. + * + * Returns the Monaco editor module. The loader and editor core are + * cached by the browser across page navigations. + */ +export function loadMonacoFromCDN(): Promise { + return new Promise((resolve, reject) => { + // Check if Monaco is already loaded + const win = window as Record; + if (win.monaco) { + resolve(win.monaco as typeof Monaco); + return; + } + + // Check if the AMD loader is already present + if (typeof (win.require as Function) === 'function' && (win.require as Record).config) { + configureAndLoadMonaco(resolve, reject); + return; + } + + // Inject the AMD loader script + const script = document.createElement('script'); + script.src = `${MONACO_CDN_BASE}/loader.js`; + script.onload = () => configureAndLoadMonaco(resolve, reject); + script.onerror = () => reject(new Error('Failed to load Monaco AMD loader')); + document.head.appendChild(script); + }); +} + +function configureAndLoadMonaco( + resolve: (monaco: typeof Monaco) => void, + reject: (error: Error) => void +): void { + const win = window as Record; + const require = win.require as { + config: (opts: Record) => void; + (deps: string[], callback: (monaco: typeof Monaco) => void, errorback?: (err: Error) => void): void; + }; + + require.config({ paths: { vs: MONACO_CDN_BASE } }); + require( + ['vs/editor/editor.main'], + (monaco: typeof Monaco) => { + resolve(monaco); + }, + (err: Error) => { + reject(err); + } + ); +} diff --git a/idf-editor/src/idd-schema-loader.ts b/idf-editor/src/idd-schema-loader.ts new file mode 100644 index 000000000..c2a674192 --- /dev/null +++ b/idf-editor/src/idd-schema-loader.ts @@ -0,0 +1,85 @@ +/** + * IDD Schema Loader + * + * Fetches and caches the compact IDD schema JSON for hover documentation. + * The schema is loaded lazily from assets/idd-schema.json relative to the + * current page. + */ + +import type { CompactIDDSchema } from './types'; + +/** Cached schema instance */ +let cachedSchema: CompactIDDSchema | null = null; + +/** Whether a fetch is in progress */ +let fetchPromise: Promise | null = null; + +/** + * Get the IDD schema, loading it if necessary. + * + * Returns null if the schema hasn't been loaded yet or if loading failed. + * The schema is fetched once per session and cached. + */ +export function getSchema(): CompactIDDSchema | null { + return cachedSchema; +} + +/** + * Load the IDD schema from the assets directory. + * + * The schema JSON is expected at assets/idd-schema.json relative to + * the current page's base URL. If loading fails (e.g., the schema + * doesn't exist for older versions), hover docs simply won't be available. + */ +export async function loadSchema(): Promise { + if (cachedSchema) return cachedSchema; + if (fetchPromise) return fetchPromise; + + fetchPromise = (async () => { + try { + // Resolve the schema URL relative to the current page + const schemaUrl = resolveSchemaUrl(); + const response = await fetch(schemaUrl); + if (!response.ok) { + console.debug(`[idf-editor] IDD schema not available (${response.status}), hover docs disabled`); + return null; + } + cachedSchema = (await response.json()) as CompactIDDSchema; + console.debug(`[idf-editor] IDD schema loaded: ${cachedSchema.version}`); + return cachedSchema; + } catch (error) { + console.debug('[idf-editor] Failed to load IDD schema:', error); + return null; + } finally { + fetchPromise = null; + } + })(); + + return fetchPromise; +} + +/** + * Clear the cached schema (used when navigating between versions). + */ +export function clearSchema(): void { + cachedSchema = null; + fetchPromise = null; +} + +/** + * Resolve the URL for the IDD schema JSON. + * + * The schema lives alongside the idf-editor.js script in the assets/ directory. + * We derive the URL from the script's own src attribute so it works regardless + * of the current page path (deep links, multi-version deployment, etc.). + */ +function resolveSchemaUrl(): string { + // Find our own script tag and resolve relative to it + const script = document.querySelector('script[src*="idf-editor"]'); + if (script) { + const scriptSrc = (script as HTMLScriptElement).src; + return new URL('idd-schema.json', scriptSrc).href; + } + // Fallback: absolute path from site root + return new URL('/assets/idd-schema.json', window.location.origin).href; +} diff --git a/idf-editor/src/idf-editor.css b/idf-editor/src/idf-editor.css new file mode 100644 index 000000000..a5a68008a --- /dev/null +++ b/idf-editor/src/idf-editor.css @@ -0,0 +1,80 @@ +/** + * IDF Editor Styles + * + * Styles for Monaco editor containers embedded in documentation pages. + * Colors and spacing are designed to match Zensical's Material Design theme. + */ + +/* Container for Monaco editor replacing
 */
+.idf-editor-container {
+  position: relative;
+  border-radius: 0.2rem;
+  overflow: hidden;
+  margin: 1em 0;
+}
+
+/* Copy button */
+.idf-editor-copy {
+  position: absolute;
+  top: 0.5em;
+  right: 0.5em;
+  z-index: 10;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 2em;
+  height: 2em;
+  padding: 0;
+  border: none;
+  border-radius: 0.2rem;
+  background: transparent;
+  color: var(--md-default-fg-color--lighter, #9e9e9e);
+  cursor: pointer;
+  opacity: 0;
+  transition: opacity 0.2s, color 0.2s, background 0.2s;
+}
+
+/* Show copy button on container hover */
+.idf-editor-container:hover .idf-editor-copy {
+  opacity: 1;
+}
+
+.idf-editor-copy:hover {
+  color: var(--md-accent-fg-color, #ffc107);
+  background: var(--md-default-fg-color--lightest, rgba(0, 0, 0, 0.04));
+}
+
+/* Copied state */
+.idf-editor-copy.copied {
+  opacity: 1;
+  color: #4caf50;
+}
+
+/* Loading placeholder while Monaco initializes */
+.idf-editor-loading {
+  background: var(--md-code-bg-color, #f5f5f5);
+  color: var(--md-code-fg-color, #333);
+  padding: 1em;
+  font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, monospace;
+  font-size: 0.85em;
+  white-space: pre;
+  overflow-x: auto;
+  border-radius: 0.2rem;
+  margin: 1em 0;
+}
+
+/* Ensure Monaco editor fills its container */
+.idf-editor-container .monaco-editor {
+  border-radius: 0.2rem;
+}
+
+/* Constrain hover tooltip width — !important needed to override Monaco's inline style */
+.monaco-hover {
+  max-width: min(360px, 90vw) !important;
+}
+
+/* Hide the default Zensical copy button when editor is present */
+.idf-editor-container + .md-clipboard,
+.idf-editor-container .md-clipboard {
+  display: none;
+}
diff --git a/idf-editor/src/idf-hover-service.ts b/idf-editor/src/idf-hover-service.ts
new file mode 100644
index 000000000..cbc42f158
--- /dev/null
+++ b/idf-editor/src/idf-hover-service.ts
@@ -0,0 +1,255 @@
+/**
+ * IDF Hover Documentation Service
+ *
+ * Provides hover tooltips for IDF code blocks, showing IDD schema documentation
+ * when the user hovers over object class names or field values.
+ *
+ * Extracted from the Envelop project (src/editor/idf-language-service.ts),
+ * keeping only the hover-related functionality.
+ */
+
+import type * as Monaco from 'monaco-editor';
+import { IDF_LANGUAGE_ID } from './idf-language';
+import type { CompactIDDSchema, CompactIDDObjectType, CompactIDDField } from './types';
+
+/**
+ * Register hover provider for IDF files.
+ *
+ * @param monaco - The Monaco editor instance
+ * @param getSchema - Function that returns the current IDD schema (or null if not loaded)
+ * @returns A disposable to unregister the provider
+ */
+export function registerHoverProvider(
+  monaco: typeof Monaco,
+  getSchema: () => CompactIDDSchema | null
+): Monaco.IDisposable {
+  return monaco.languages.registerHoverProvider(IDF_LANGUAGE_ID, {
+    provideHover(model, position): Monaco.languages.Hover | null {
+      const schema = getSchema();
+      if (!schema) {
+        return null;
+      }
+
+      const context = getHoverContext(model, position);
+      if (!context) {
+        return null;
+      }
+
+      const objectType = schema.objectTypes[context.className.toLowerCase()];
+      if (!objectType) {
+        return null;
+      }
+
+      // Hovering over class name
+      if (context.isClassName) {
+        return createObjectHover(objectType, position);
+      }
+
+      // Hovering over a field value
+      if (context.fieldIndex !== undefined && context.fieldIndex < objectType.fields.length) {
+        const field = objectType.fields[context.fieldIndex];
+        if (field) {
+          return createFieldHover(field, objectType);
+        }
+      }
+
+      return null;
+    },
+  });
+}
+
+/** Hover context describing what the cursor is over */
+interface HoverContext {
+  className: string;
+  isClassName: boolean;
+  fieldIndex?: number;
+}
+
+/** Get context for hover documentation at the given position */
+function getHoverContext(
+  model: Monaco.editor.ITextModel,
+  position: Monaco.Position
+): HoverContext | null {
+  const lineContent = model.getLineContent(position.lineNumber);
+
+  // Check if we're hovering over a class name
+  const classMatch = lineContent.match(/^([A-Za-z][A-Za-z0-9:_-]*)\s*,/);
+  if (classMatch && classMatch[1]) {
+    const classNameEnd = classMatch[1].length;
+    if (position.column <= classNameEnd + 1) {
+      return { className: classMatch[1], isClassName: true };
+    }
+  }
+
+  // Find the current object
+  const objectContext = findCurrentObject(model, position);
+  if (objectContext) {
+    const fieldIndex = countFieldsSoFar(model, objectContext.startLine, position);
+    return {
+      className: objectContext.className,
+      isClassName: false,
+      fieldIndex,
+    };
+  }
+
+  return null;
+}
+
+/** Find the current object context (class name and start line) */
+function findCurrentObject(
+  model: Monaco.editor.ITextModel,
+  position: Monaco.Position
+): { className: string; startLine: number } | null {
+  for (let line = position.lineNumber; line >= 1; line--) {
+    const lineContent = model.getLineContent(line);
+
+    // Look for class name pattern: "ClassName,"
+    const match = lineContent.match(/^([A-Za-z][A-Za-z0-9:_-]*)\s*,/);
+    if (match && match[1]) {
+      return { className: match[1], startLine: line };
+    }
+
+    // If we hit a semicolon, we've gone past our object
+    if (lineContent.includes(';') && line < position.lineNumber) {
+      break;
+    }
+  }
+
+  return null;
+}
+
+/** Count the number of fields (commas) from object start to current position */
+function countFieldsSoFar(
+  model: Monaco.editor.ITextModel,
+  startLine: number,
+  position: Monaco.Position
+): number {
+  let fieldCount = 0;
+
+  for (let line = startLine; line <= position.lineNumber; line++) {
+    const lineContent = model.getLineContent(line);
+    const endCol = line === position.lineNumber ? position.column - 1 : lineContent.length;
+    const text = lineContent.substring(0, endCol);
+
+    // Remove comments
+    const withoutComments = text.replace(/!.*$/, '');
+
+    // Count commas
+    const commas = (withoutComments.match(/,/g) || []).length;
+    fieldCount += commas;
+
+    // The first comma after class name is field 0's delimiter
+    if (line === startLine && commas > 0) {
+      fieldCount--;
+    }
+  }
+
+  return fieldCount;
+}
+
+/** Create hover content for an object type */
+function createObjectHover(
+  objectType: CompactIDDObjectType,
+  position: Monaco.Position
+): Monaco.languages.Hover {
+  const contents: Monaco.IMarkdownString[] = [];
+
+  // Title
+  contents.push({ value: `**${objectType.name}**` });
+
+  // Group
+  if (objectType.group) {
+    contents.push({ value: `*Group: ${objectType.group}*` });
+  }
+
+  // Memo
+  if (objectType.memo) {
+    contents.push({ value: objectType.memo });
+  }
+
+  // Properties
+  const props: string[] = [];
+  if (objectType.isUnique) props.push('unique-object');
+  if (objectType.isRequired) props.push('required-object');
+  if (objectType.minFields > 0) props.push(`min-fields: ${String(objectType.minFields)}`);
+  if (objectType.extensible > 0) props.push(`extensible: ${String(objectType.extensible)}`);
+
+  if (props.length > 0) {
+    contents.push({ value: `\`${props.join(' | ')}\`` });
+  }
+
+  return {
+    contents,
+    range: {
+      startLineNumber: position.lineNumber,
+      startColumn: 1,
+      endLineNumber: position.lineNumber,
+      endColumn: objectType.name.length + 1,
+    },
+  };
+}
+
+/** Create hover content for a field */
+function createFieldHover(
+  field: CompactIDDField,
+  objectType: CompactIDDObjectType
+): Monaco.languages.Hover {
+  const contents: Monaco.IMarkdownString[] = [];
+
+  // Title
+  contents.push({ value: `**${field.name || field.id}** (${objectType.name})` });
+
+  // Type and units
+  let typeInfo = `Type: \`${field.type}\``;
+  if (field.units) {
+    typeInfo += ` | Units: \`${field.units}\``;
+  }
+  contents.push({ value: typeInfo });
+
+  // Memo
+  if (field.memo) {
+    contents.push({ value: field.memo });
+  }
+
+  // Range constraints
+  if (field.minimum !== undefined || field.maximum !== undefined) {
+    let range = 'Range: ';
+    if (field.minimum !== undefined) {
+      range += field.exclusiveMinimum ? `> ${String(field.minimum)}` : `>= ${String(field.minimum)}`;
+    }
+    if (field.minimum !== undefined && field.maximum !== undefined) {
+      range += ' and ';
+    }
+    if (field.maximum !== undefined) {
+      range += field.exclusiveMaximum ? `< ${String(field.maximum)}` : `<= ${String(field.maximum)}`;
+    }
+    contents.push({ value: range });
+  }
+
+  // Default value
+  if (field.default) {
+    contents.push({ value: `Default: \`${field.default}\`` });
+  }
+
+  // Choices — use a compact list when there are many options
+  if (field.choices && field.choices.length > 0) {
+    if (field.choices.length <= 5) {
+      contents.push({ value: `Choices: ${field.choices.map((c) => `\`${c}\``).join(', ')}` });
+    } else {
+      const list = field.choices.map((c) => `- \`${c}\``).join('\n');
+      contents.push({ value: `Choices (${String(field.choices.length)}):\n${list}` });
+    }
+  }
+
+  // Properties
+  const props: string[] = [];
+  if (field.required) props.push('required');
+  if (field.autosizable) props.push('autosizable');
+  if (field.autocalculatable) props.push('autocalculatable');
+
+  if (props.length > 0) {
+    contents.push({ value: `\`${props.join(' | ')}\`` });
+  }
+
+  return { contents };
+}
diff --git a/idf-editor/src/idf-language.ts b/idf-editor/src/idf-language.ts
new file mode 100644
index 000000000..3c64c9eab
--- /dev/null
+++ b/idf-editor/src/idf-language.ts
@@ -0,0 +1,226 @@
+/**
+ * IDF Language Definition for Monaco Editor
+ *
+ * Provides syntax highlighting, tokenization, and language configuration
+ * for EnergyPlus IDF (Input Data File) format.
+ *
+ * Adapted from the Envelop project (src/editor/idf-language.ts).
+ */
+
+import type { languages } from 'monaco-editor';
+
+/** Language ID for IDF files */
+export const IDF_LANGUAGE_ID = 'idf';
+
+/** IDF Language configuration */
+export const idfLanguageConfiguration: languages.LanguageConfiguration = {
+  comments: {
+    lineComment: '!',
+  },
+  brackets: [],
+  autoClosingPairs: [],
+  surroundingPairs: [],
+  folding: {
+    markers: {
+      start: /^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i,
+      end: /^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i,
+    },
+  },
+  wordPattern: /[A-Za-z][A-Za-z0-9:_-]*/,
+};
+
+/** Common EnergyPlus object class names for highlighting */
+const COMMON_CLASSES = [
+  'Version',
+  'SimulationControl',
+  'Building',
+  'Timestep',
+  'RunPeriod',
+  'Site:Location',
+  'SizingPeriod:DesignDay',
+  'GlobalGeometryRules',
+  'Zone',
+  'ZoneList',
+  'BuildingSurface:Detailed',
+  'FenestrationSurface:Detailed',
+  'Wall:Exterior',
+  'Wall:Interior',
+  'Roof',
+  'Floor:GroundContact',
+  'Window',
+  'Door',
+  'Material',
+  'Material:NoMass',
+  'Material:AirGap',
+  'WindowMaterial:SimpleGlazingSystem',
+  'WindowMaterial:Glazing',
+  'Construction',
+  'Schedule:Compact',
+  'Schedule:Constant',
+  'Schedule:Day:Interval',
+  'Schedule:Week:Daily',
+  'Schedule:Year',
+  'ScheduleTypeLimits',
+  'People',
+  'Lights',
+  'ElectricEquipment',
+  'ZoneInfiltration:DesignFlowRate',
+  'ZoneVentilation:DesignFlowRate',
+  'Sizing:Zone',
+  'Sizing:System',
+  'Sizing:Plant',
+  'ZoneHVAC:IdealLoadsAirSystem',
+  'ZoneHVAC:EquipmentList',
+  'ZoneHVAC:EquipmentConnections',
+  'ThermostatSetpoint:SingleHeating',
+  'ThermostatSetpoint:SingleCooling',
+  'ThermostatSetpoint:DualSetpoint',
+  'ZoneControl:Thermostat',
+  'AirLoopHVAC',
+  'AirLoopHVAC:ZoneSplitter',
+  'AirLoopHVAC:ZoneMixer',
+  'Fan:ConstantVolume',
+  'Fan:VariableVolume',
+  'Fan:OnOff',
+  'Coil:Heating:Electric',
+  'Coil:Heating:Fuel',
+  'Coil:Heating:Water',
+  'Coil:Cooling:DX:SingleSpeed',
+  'Coil:Cooling:DX:TwoSpeed',
+  'Coil:Cooling:Water',
+  'Controller:OutdoorAir',
+  'AirLoopHVAC:OutdoorAirSystem',
+  'OutdoorAir:Mixer',
+  'SetpointManager:Scheduled',
+  'SetpointManager:SingleZone:Reheat',
+  'PlantLoop',
+  'Pump:ConstantSpeed',
+  'Pump:VariableSpeed',
+  'Boiler:HotWater',
+  'Chiller:Electric:EIR',
+  'CoolingTower:SingleSpeed',
+  'Output:Variable',
+  'Output:Meter',
+  'Output:Table:Monthly',
+  'Output:Table:SummaryReports',
+  'OutputControl:Table:Style',
+];
+
+/** Keywords used in IDF field values */
+const KEYWORDS = [
+  'Yes',
+  'No',
+  'On',
+  'Off',
+  'True',
+  'False',
+  'autocalculate',
+  'autosize',
+  'Continuous',
+  'Discrete',
+  'Any Number',
+  'Hourly',
+  'Timestep',
+  'Daily',
+  'Monthly',
+  'RunPeriod',
+  'Annual',
+  'SummerDesignDay',
+  'WinterDesignDay',
+  'Sunday',
+  'Monday',
+  'Tuesday',
+  'Wednesday',
+  'Thursday',
+  'Friday',
+  'Saturday',
+  'Holiday',
+  'CustomDay1',
+  'CustomDay2',
+  'AllDays',
+  'Weekdays',
+  'Weekends',
+  'AllOtherDays',
+];
+
+/** IDF Monarch tokenizer definition */
+export const idfTokensProvider: languages.IMonarchLanguage = {
+  defaultToken: 'invalid',
+  tokenPostfix: '.idf',
+
+  // Case insensitive
+  ignoreCase: true,
+
+  // Common class names
+  classes: COMMON_CLASSES,
+
+  // Keywords
+  keywords: KEYWORDS,
+
+  // Operators and delimiters
+  operators: [',', ';'],
+
+  // Number patterns
+  digits: /\d+/,
+  floatDigits: /\d*\.\d+([eE][+-]?\d+)?/,
+
+  tokenizer: {
+    root: [
+      // Whitespace
+      { include: '@whitespace' },
+
+      // Comments (must come before other rules)
+      [/!-.*$/, 'comment.doc'],
+      [/!.*$/, 'comment'],
+
+      // Class names (at start of line or after semicolon)
+      [
+        /^([A-Za-z][A-Za-z0-9:_-]*)\s*(,)/,
+        [
+          {
+            cases: {
+              '@classes': 'type.identifier',
+              '@default': 'type',
+            },
+          },
+          'delimiter',
+        ],
+      ],
+
+      // Field values
+      { include: '@fieldValue' },
+
+      // Delimiters
+      [/[,]/, 'delimiter'],
+      [/[;]/, 'delimiter.semicolon'],
+    ],
+
+    whitespace: [[/[ \t\r\n]+/, 'white']],
+
+    fieldValue: [
+      // Numbers (including scientific notation)
+      [/-?\d*\.\d+([eE][+-]?\d+)?/, 'number.float'],
+      [/-?\d+([eE][+-]?\d+)?/, 'number'],
+
+      // Keywords
+      [
+        /[A-Za-z][A-Za-z0-9_-]*/,
+        {
+          cases: {
+            '@keywords': 'keyword',
+            '@default': 'string',
+          },
+        },
+      ],
+
+      // Wildcards and special values
+      [/\*/, 'constant'],
+
+      // Time/date patterns (e.g., "Through: 12/31")
+      [/Through:\s*\d+\/\d+/, 'string.date'],
+      [/For:\s*[A-Za-z,\s]+/, 'string.date'],
+      [/Until:\s*\d+:\d+/, 'string.date'],
+      [/Interpolate:\s*[A-Za-z]+/, 'string.date'],
+    ],
+  },
+};
diff --git a/idf-editor/src/idf-themes.ts b/idf-editor/src/idf-themes.ts
new file mode 100644
index 000000000..022efd35b
--- /dev/null
+++ b/idf-editor/src/idf-themes.ts
@@ -0,0 +1,93 @@
+/**
+ * Monaco Editor Themes for IDF Documentation
+ *
+ * Light and dark themes adapted from the Envelop project, with colors
+ * tuned to match Zensical's Material Design palette (teal/amber with
+ * default and slate color schemes).
+ */
+
+/** IDF theme token rules for dark mode */
+const idfDarkThemeRules: { token: string; foreground?: string; fontStyle?: string }[] = [
+  { token: 'comment', foreground: '6A9955' },
+  { token: 'comment.doc', foreground: '6A9955', fontStyle: 'italic' },
+  { token: 'type', foreground: '4EC9B0' },
+  { token: 'type.identifier', foreground: '4EC9B0', fontStyle: 'bold' },
+  { token: 'keyword', foreground: '569CD6' },
+  { token: 'number', foreground: 'B5CEA8' },
+  { token: 'number.float', foreground: 'B5CEA8' },
+  { token: 'string', foreground: 'CE9178' },
+  { token: 'string.date', foreground: 'DCDCAA' },
+  { token: 'delimiter', foreground: 'D4D4D4' },
+  { token: 'delimiter.semicolon', foreground: 'D4D4D4', fontStyle: 'bold' },
+  { token: 'constant', foreground: '4FC1FF' },
+];
+
+/** IDF theme token rules for light mode */
+const idfLightThemeRules: { token: string; foreground?: string; fontStyle?: string }[] = [
+  { token: 'comment', foreground: '008000' },
+  { token: 'comment.doc', foreground: '008000', fontStyle: 'italic' },
+  { token: 'type', foreground: '267F99' },
+  { token: 'type.identifier', foreground: '267F99', fontStyle: 'bold' },
+  { token: 'keyword', foreground: '0000FF' },
+  { token: 'number', foreground: '098658' },
+  { token: 'number.float', foreground: '098658' },
+  { token: 'string', foreground: 'A31515' },
+  { token: 'string.date', foreground: '795E26' },
+  { token: 'delimiter', foreground: '000000' },
+  { token: 'delimiter.semicolon', foreground: '000000', fontStyle: 'bold' },
+  { token: 'constant', foreground: '0070C1' },
+];
+
+/** Theme name constants */
+export const THEME_LIGHT = 'idf-docs-light';
+export const THEME_DARK = 'idf-docs-dark';
+
+/**
+ * Register both light and dark themes with Monaco.
+ *
+ * Background and widget colors are tuned to match Zensical's Material Design
+ * theme: "default" scheme for light, "slate" scheme for dark.
+ */
+export function registerIDFThemes(monaco: { editor: { defineTheme: Function } }): void {
+  monaco.editor.defineTheme(THEME_DARK, {
+    base: 'vs-dark',
+    inherit: true,
+    rules: idfDarkThemeRules,
+    colors: {
+      'editor.background': '#212121', // Matches Zensical slate code bg
+      'editor.foreground': '#e2e8f0',
+      'editor.lineHighlightBackground': '#2d2d2d',
+      'editor.selectionBackground': '#264f78',
+      'editorLineNumber.foreground': '#6b7280',
+      'editorCursor.foreground': '#e2e8f0',
+      'editorWidget.background': '#2d2d2d',
+      'editorWidget.border': '#404040',
+      'editorHoverWidget.background': '#2d2d2d',
+      'editorHoverWidget.border': '#404040',
+    },
+  });
+
+  monaco.editor.defineTheme(THEME_LIGHT, {
+    base: 'vs',
+    inherit: true,
+    rules: idfLightThemeRules,
+    colors: {
+      'editor.background': '#f5f5f5', // Matches Zensical default code bg
+      'editor.foreground': '#1a202c',
+      'editor.lineHighlightBackground': '#f0f0f0',
+      'editor.selectionBackground': '#c8e1ff',
+      'editorLineNumber.foreground': '#a0aec0',
+      'editorCursor.foreground': '#1a202c',
+      'editorWidget.background': '#ffffff',
+      'editorWidget.border': '#e2e8f0',
+      'editorHoverWidget.background': '#ffffff',
+      'editorHoverWidget.border': '#e2e8f0',
+    },
+  });
+}
+
+/** Get the appropriate theme name based on Zensical's color scheme */
+export function getCurrentTheme(): string {
+  const scheme = document.body.getAttribute('data-md-color-scheme');
+  return scheme === 'slate' ? THEME_DARK : THEME_LIGHT;
+}
diff --git a/idf-editor/src/main.ts b/idf-editor/src/main.ts
new file mode 100644
index 000000000..755d4e753
--- /dev/null
+++ b/idf-editor/src/main.ts
@@ -0,0 +1,126 @@
+/**
+ * IDF Editor — Main Entry Point
+ *
+ * Scans documentation pages for IDF code blocks and progressively replaces
+ * them with rich Monaco editor instances featuring syntax highlighting,
+ * hover documentation, and code folding.
+ *
+ * Monaco is loaded lazily from CDN only when IDF code blocks are present.
+ */
+
+import './idf-editor.css';
+import { EditorManager, loadMonacoFromCDN } from './editor-manager';
+import { idfLanguageConfiguration, idfTokensProvider, IDF_LANGUAGE_ID } from './idf-language';
+import { registerIDFThemes } from './idf-themes';
+import { registerHoverProvider } from './idf-hover-service';
+import { getSchema, loadSchema } from './idd-schema-loader';
+import type * as Monaco from 'monaco-editor';
+
+/** Whether the IDF language has been registered (once globally) */
+let languageRegistered = false;
+
+/** Current editor manager (recreated on each page navigation) */
+let currentManager: EditorManager | null = null;
+
+/**
+ * Register the IDF language, themes, and providers with Monaco.
+ * Only done once globally (Monaco language registration is persistent).
+ */
+function registerLanguage(monaco: typeof Monaco): void {
+  if (languageRegistered) return;
+
+  // Register language
+  monaco.languages.register({
+    id: IDF_LANGUAGE_ID,
+    extensions: ['.idf', '.imf'],
+    aliases: ['IDF', 'EnergyPlus IDF', 'Input Data File'],
+    mimetypes: ['text/x-idf'],
+  });
+  monaco.languages.setLanguageConfiguration(IDF_LANGUAGE_ID, idfLanguageConfiguration);
+  monaco.languages.setMonarchTokensProvider(IDF_LANGUAGE_ID, idfTokensProvider);
+
+  // Register themes
+  registerIDFThemes(monaco);
+
+  // Register hover provider (schema may not be loaded yet; getSchema returns null until it is)
+  registerHoverProvider(monaco, getSchema);
+
+  languageRegistered = true;
+}
+
+/**
+ * Initialize editors for the current page.
+ * Called on initial load and after each instant navigation.
+ */
+async function initPage(): Promise {
+  // Dispose previous editors (from prior page)
+  if (currentManager) {
+    currentManager.dispose();
+    currentManager = null;
+  }
+
+  // Skip Monaco on narrow viewports — touch devices lack hover and the
+  // editors need horizontal space.  Pygments static highlighting remains.
+  if (window.innerWidth < 768) return;
+
+  // Check if there are IDF code blocks on this page.
+  // Zensical/pymdownx puts the language class on a wrapper 
, not on . + // Structure:
...
+ const codeBlocks = document.querySelectorAll('div.language-idf pre > code'); + if (codeBlocks.length === 0) return; + + try { + // Load Monaco from CDN (cached after first load) + const monaco = await loadMonacoFromCDN(); + + // Register language and providers (once) + registerLanguage(monaco); + + // Start loading IDD schema in the background (for hover docs) + loadSchema(); + + // Create editor manager and initialize editors + currentManager = new EditorManager(monaco); + currentManager.initialize(codeBlocks); + } catch (error) { + console.error('[idf-editor] Failed to initialize:', error); + } +} + +/** + * Hook into Zensical's instant navigation system. + * The document$ observable emits on each page navigation. + */ +function hookInstantNav(): boolean { + const win = window as Record; + const document$ = win.document$ as { subscribe: (fn: () => void) => void } | undefined; + + if (!document$) return false; + + document$.subscribe(() => { + // Small delay to ensure the DOM is updated + requestAnimationFrame(() => initPage()); + }); + + return true; +} + +// --- Bootstrap --- + +// Initialize on page load +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => initPage()); +} else { + initPage(); +} + +// Hook into instant navigation (may not be available immediately) +if (!hookInstantNav()) { + let attempts = 0; + const interval = setInterval(() => { + attempts++; + if (hookInstantNav() || attempts > 50) { + clearInterval(interval); + } + }, 100); +} diff --git a/idf-editor/src/types.ts b/idf-editor/src/types.ts new file mode 100644 index 000000000..bd4a1fd10 --- /dev/null +++ b/idf-editor/src/types.ts @@ -0,0 +1,77 @@ +/** + * Compact IDD Schema Types for Browser + * + * These are simplified versions of the Envelop project's IDD types, + * optimized for the hover documentation use case. They use plain + * objects/Records instead of Maps for JSON serialization. + */ + +/** Field type enumeration */ +export type IDDFieldType = + | 'real' + | 'integer' + | 'alpha' + | 'choice' + | 'object-list' + | 'external-list' + | 'node'; + +/** Compact field definition for hover docs */ +export interface CompactIDDField { + /** Field identifier (A1, A2, N1, N2, etc.) */ + id: string; + /** Field name from \field tag */ + name: string; + /** Data type */ + type: IDDFieldType; + /** Whether this field is required */ + required: boolean; + /** Default value */ + default?: string; + /** Unit specification (e.g., "m", "W", "degC") */ + units?: string; + /** Minimum value */ + minimum?: number; + /** Whether minimum is exclusive */ + exclusiveMinimum?: boolean; + /** Maximum value */ + maximum?: number; + /** Whether maximum is exclusive */ + exclusiveMaximum?: boolean; + /** Valid choices for 'choice' type fields */ + choices?: string[]; + /** Documentation text */ + memo: string; + /** Whether this field can be autosized */ + autosizable: boolean; + /** Whether this field can be autocalculated */ + autocalculatable: boolean; +} + +/** Compact object type definition for hover docs */ +export interface CompactIDDObjectType { + /** Object class name (e.g., "Building", "Zone") */ + name: string; + /** Group this object belongs to */ + group: string; + /** Documentation from \memo tags */ + memo: string; + /** Field definitions */ + fields: CompactIDDField[]; + /** Minimum number of fields required */ + minFields: number; + /** Only one instance allowed */ + isUnique: boolean; + /** Must exist in every model */ + isRequired: boolean; + /** Number of fields in extensible group */ + extensible: number; +} + +/** The complete compact IDD schema */ +export interface CompactIDDSchema { + /** EnergyPlus version */ + version: string; + /** Object types keyed by lowercase name */ + objectTypes: Record; +} diff --git a/idf-editor/tsconfig.json b/idf-editor/tsconfig.json new file mode 100644 index 000000000..039320107 --- /dev/null +++ b/idf-editor/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "declaration": false, + "sourceMap": true + }, + "include": [ + "src" + ] +} diff --git a/idf-editor/vite.config.ts b/idf-editor/vite.config.ts new file mode 100644 index 000000000..782156a81 --- /dev/null +++ b/idf-editor/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vite'; +import path from 'path'; + +export default defineConfig({ + build: { + lib: { + entry: path.resolve(__dirname, 'src/main.ts'), + name: 'IDFEditor', + formats: ['iife'], + fileName: () => 'idf-editor.js', + }, + outDir: path.resolve(__dirname, 'dist'), + cssFileName: 'idf-editor', + minify: 'esbuild', + sourcemap: false, + rollupOptions: { + output: { + assetFileNames: 'idf-editor.[ext]', + }, + }, + }, +}); diff --git a/pyproject.toml b/pyproject.toml index 6a6eb064e..f8d61ca10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,6 +8,7 @@ requires-python = ">=3.10,<4.0" dependencies = [ "tomli_w>=1.0", "tomli>=2.0; python_version < '3.11'", + "idfkit>=0.3.0", ] [project.urls] @@ -24,6 +25,16 @@ dev = [ "tomli_w>=1.0", ] +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["scripts"] + +[project.entry-points."pygments.lexers"] +idf = "scripts.pygments_idf_lexer:IDFLexer" + [tool.ruff] target-version = "py310" line-length = 120 diff --git a/scripts/assets/idf-editor.css b/scripts/assets/idf-editor.css new file mode 100644 index 000000000..956fcd1d9 --- /dev/null +++ b/scripts/assets/idf-editor.css @@ -0,0 +1 @@ +.idf-editor-container{position:relative;border-radius:.2rem;overflow:hidden;margin:1em 0}.idf-editor-copy{position:absolute;top:.5em;right:.5em;z-index:10;display:flex;align-items:center;justify-content:center;width:2em;height:2em;padding:0;border:none;border-radius:.2rem;background:transparent;color:var(--md-default-fg-color--lighter, #9e9e9e);cursor:pointer;opacity:0;transition:opacity .2s,color .2s,background .2s}.idf-editor-container:hover .idf-editor-copy{opacity:1}.idf-editor-copy:hover{color:var(--md-accent-fg-color, #ffc107);background:var(--md-default-fg-color--lightest, rgba(0, 0, 0, .04))}.idf-editor-copy.copied{opacity:1;color:#4caf50}.idf-editor-loading{background:var(--md-code-bg-color, #f5f5f5);color:var(--md-code-fg-color, #333);padding:1em;font-family:JetBrains Mono,Fira Code,ui-monospace,SFMono-Regular,monospace;font-size:.85em;white-space:pre;overflow-x:auto;border-radius:.2rem;margin:1em 0}.idf-editor-container .monaco-editor{border-radius:.2rem}.monaco-hover{max-width:min(360px,90vw)!important}.idf-editor-container+.md-clipboard,.idf-editor-container .md-clipboard{display:none} diff --git a/scripts/assets/idf-editor.js b/scripts/assets/idf-editor.js new file mode 100644 index 000000000..197b9fa21 --- /dev/null +++ b/scripts/assets/idf-editor.js @@ -0,0 +1,4 @@ +(function(){"use strict";const w=[{token:"comment",foreground:"6A9955"},{token:"comment.doc",foreground:"6A9955",fontStyle:"italic"},{token:"type",foreground:"4EC9B0"},{token:"type.identifier",foreground:"4EC9B0",fontStyle:"bold"},{token:"keyword",foreground:"569CD6"},{token:"number",foreground:"B5CEA8"},{token:"number.float",foreground:"B5CEA8"},{token:"string",foreground:"CE9178"},{token:"string.date",foreground:"DCDCAA"},{token:"delimiter",foreground:"D4D4D4"},{token:"delimiter.semicolon",foreground:"D4D4D4",fontStyle:"bold"},{token:"constant",foreground:"4FC1FF"}],k=[{token:"comment",foreground:"008000"},{token:"comment.doc",foreground:"008000",fontStyle:"italic"},{token:"type",foreground:"267F99"},{token:"type.identifier",foreground:"267F99",fontStyle:"bold"},{token:"keyword",foreground:"0000FF"},{token:"number",foreground:"098658"},{token:"number.float",foreground:"098658"},{token:"string",foreground:"A31515"},{token:"string.date",foreground:"795E26"},{token:"delimiter",foreground:"000000"},{token:"delimiter.semicolon",foreground:"000000",fontStyle:"bold"},{token:"constant",foreground:"0070C1"}],m="idf-docs-light",g="idf-docs-dark";function A(e){e.editor.defineTheme(g,{base:"vs-dark",inherit:!0,rules:w,colors:{"editor.background":"#212121","editor.foreground":"#e2e8f0","editor.lineHighlightBackground":"#2d2d2d","editor.selectionBackground":"#264f78","editorLineNumber.foreground":"#6b7280","editorCursor.foreground":"#e2e8f0","editorWidget.background":"#2d2d2d","editorWidget.border":"#404040","editorHoverWidget.background":"#2d2d2d","editorHoverWidget.border":"#404040"}}),e.editor.defineTheme(m,{base:"vs",inherit:!0,rules:k,colors:{"editor.background":"#f5f5f5","editor.foreground":"#1a202c","editor.lineHighlightBackground":"#f0f0f0","editor.selectionBackground":"#c8e1ff","editorLineNumber.foreground":"#a0aec0","editorCursor.foreground":"#1a202c","editorWidget.background":"#ffffff","editorWidget.border":"#e2e8f0","editorHoverWidget.background":"#ffffff","editorHoverWidget.border":"#e2e8f0"}})}function D(){return document.body.getAttribute("data-md-color-scheme")==="slate"?g:m}const L=60,E=720,M=19,N=20,O="200px",p="https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs",f='',C='';class H{constructor(n){this.editors=[],this.observer=null,this.themeObserver=null,this.pendingBlocks=new Map,this.monaco=n}initialize(n){n.length!==0&&(this.observer=new IntersectionObserver(o=>this.handleIntersection(o),{rootMargin:O}),n.forEach(o=>{const t=o.parentElement;if(!t||t.tagName!=="PRE")return;const r=t.parentElement;if(!r||r.dataset.idfEditor==="true")return;const i=o.textContent||"";this.pendingBlocks.set(r,{wrapper:r,code:i}),this.observer.observe(r)}),this.watchThemeChanges())}dispose(){var n,o;for(const{editor:t}of this.editors)t.dispose();this.editors=[],(n=this.observer)==null||n.disconnect(),this.observer=null,(o=this.themeObserver)==null||o.disconnect(),this.themeObserver=null,this.pendingBlocks.clear()}handleIntersection(n){var o;for(const t of n){if(!t.isIntersecting)continue;const r=this.pendingBlocks.get(t.target);r&&((o=this.observer)==null||o.unobserve(t.target),this.pendingBlocks.delete(t.target),this.createEditor(r.wrapper,r.code))}}createEditor(n,o){var l;const t=o.split(` +`).length,r=Math.max(L,Math.min(E,t*M+N)),i=document.createElement("div");i.className="idf-editor-container",i.style.height=`${r}px`,(l=n.parentNode)==null||l.replaceChild(i,n),n.dataset.idfEditor="true";const s=this.monaco.editor.create(i,{value:o,language:"idf",theme:D(),readOnly:!0,domReadOnly:!0,minimap:{enabled:!1},lineNumbers:"on",scrollBeyondLastLine:!1,wordWrap:"off",folding:!1,glyphMargin:!1,lineDecorationsWidth:8,lineNumbersMinChars:3,renderLineHighlight:"none",overviewRulerLanes:0,hideCursorInOverviewRuler:!0,overviewRulerBorder:!1,scrollbar:{vertical:"auto",horizontal:"auto",verticalScrollbarSize:8,horizontalScrollbarSize:8},fontSize:13,fontFamily:"'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, monospace",padding:{top:8,bottom:8},automaticLayout:!0,contextmenu:!1,links:!1,renderValidationDecorations:"off",fixedOverflowWidgets:!0,accessibilitySupport:"off",ariaLabel:"EnergyPlus IDF code example"});this.addCopyButton(i,s),this.editors.push({editor:s,container:i})}addCopyButton(n,o){const t=document.createElement("button");t.className="idf-editor-copy",t.title="Copy to clipboard",t.innerHTML=f,t.addEventListener("click",async()=>{try{const r=o.getValue();await navigator.clipboard.writeText(r),t.innerHTML=C,t.classList.add("copied"),setTimeout(()=>{t.innerHTML=f,t.classList.remove("copied")},2e3)}catch{const r=document.createElement("textarea");r.value=o.getValue(),r.style.position="fixed",r.style.opacity="0",document.body.appendChild(r),r.select(),document.execCommand("copy"),document.body.removeChild(r),t.innerHTML=C,t.classList.add("copied"),setTimeout(()=>{t.innerHTML=f,t.classList.remove("copied")},2e3)}}),n.appendChild(t)}watchThemeChanges(){this.themeObserver=new MutationObserver(n=>{for(const o of n)if(o.attributeName==="data-md-color-scheme"){const r=document.body.getAttribute("data-md-color-scheme")==="slate"?g:m;this.monaco.editor.setTheme(r)}}),this.themeObserver.observe(document.body,{attributes:!0,attributeFilter:["data-md-color-scheme"]})}}function x(){return new Promise((e,n)=>{const o=window;if(o.monaco){e(o.monaco);return}if(typeof o.require=="function"&&o.require.config){v(e,n);return}const t=document.createElement("script");t.src=`${p}/loader.js`,t.onload=()=>v(e,n),t.onerror=()=>n(new Error("Failed to load Monaco AMD loader")),document.head.appendChild(t)})}function v(e,n){const t=window.require;t.config({paths:{vs:p}}),t(["vs/editor/editor.main"],r=>{e(r)},r=>{n(r)})}const c="idf",I={comments:{lineComment:"!"},brackets:[],autoClosingPairs:[],surroundingPairs:[],folding:{markers:{start:/^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i,end:/^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i}},wordPattern:/[A-Za-z][A-Za-z0-9:_-]*/},F={defaultToken:"invalid",tokenPostfix:".idf",ignoreCase:!0,classes:["Version","SimulationControl","Building","Timestep","RunPeriod","Site:Location","SizingPeriod:DesignDay","GlobalGeometryRules","Zone","ZoneList","BuildingSurface:Detailed","FenestrationSurface:Detailed","Wall:Exterior","Wall:Interior","Roof","Floor:GroundContact","Window","Door","Material","Material:NoMass","Material:AirGap","WindowMaterial:SimpleGlazingSystem","WindowMaterial:Glazing","Construction","Schedule:Compact","Schedule:Constant","Schedule:Day:Interval","Schedule:Week:Daily","Schedule:Year","ScheduleTypeLimits","People","Lights","ElectricEquipment","ZoneInfiltration:DesignFlowRate","ZoneVentilation:DesignFlowRate","Sizing:Zone","Sizing:System","Sizing:Plant","ZoneHVAC:IdealLoadsAirSystem","ZoneHVAC:EquipmentList","ZoneHVAC:EquipmentConnections","ThermostatSetpoint:SingleHeating","ThermostatSetpoint:SingleCooling","ThermostatSetpoint:DualSetpoint","ZoneControl:Thermostat","AirLoopHVAC","AirLoopHVAC:ZoneSplitter","AirLoopHVAC:ZoneMixer","Fan:ConstantVolume","Fan:VariableVolume","Fan:OnOff","Coil:Heating:Electric","Coil:Heating:Fuel","Coil:Heating:Water","Coil:Cooling:DX:SingleSpeed","Coil:Cooling:DX:TwoSpeed","Coil:Cooling:Water","Controller:OutdoorAir","AirLoopHVAC:OutdoorAirSystem","OutdoorAir:Mixer","SetpointManager:Scheduled","SetpointManager:SingleZone:Reheat","PlantLoop","Pump:ConstantSpeed","Pump:VariableSpeed","Boiler:HotWater","Chiller:Electric:EIR","CoolingTower:SingleSpeed","Output:Variable","Output:Meter","Output:Table:Monthly","Output:Table:SummaryReports","OutputControl:Table:Style"],keywords:["Yes","No","On","Off","True","False","autocalculate","autosize","Continuous","Discrete","Any Number","Hourly","Timestep","Daily","Monthly","RunPeriod","Annual","SummerDesignDay","WinterDesignDay","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Holiday","CustomDay1","CustomDay2","AllDays","Weekdays","Weekends","AllOtherDays"],operators:[",",";"],digits:/\d+/,floatDigits:/\d*\.\d+([eE][+-]?\d+)?/,tokenizer:{root:[{include:"@whitespace"},[/!-.*$/,"comment.doc"],[/!.*$/,"comment"],[/^([A-Za-z][A-Za-z0-9:_-]*)\s*(,)/,[{cases:{"@classes":"type.identifier","@default":"type"}},"delimiter"]],{include:"@fieldValue"},[/[,]/,"delimiter"],[/[;]/,"delimiter.semicolon"]],whitespace:[[/[ \t\r\n]+/,"white"]],fieldValue:[[/-?\d*\.\d+([eE][+-]?\d+)?/,"number.float"],[/-?\d+([eE][+-]?\d+)?/,"number"],[/[A-Za-z][A-Za-z0-9_-]*/,{cases:{"@keywords":"keyword","@default":"string"}}],[/\*/,"constant"],[/Through:\s*\d+\/\d+/,"string.date"],[/For:\s*[A-Za-z,\s]+/,"string.date"],[/Until:\s*\d+:\d+/,"string.date"],[/Interpolate:\s*[A-Za-z]+/,"string.date"]]}};function T(e,n){return e.languages.registerHoverProvider(c,{provideHover(o,t){const r=n();if(!r)return null;const i=z(o,t);if(!i)return null;const s=r.objectTypes[i.className.toLowerCase()];if(!s)return null;if(i.isClassName)return W(s,t);if(i.fieldIndex!==void 0&&i.fieldIndex=1;o--){const t=e.getLineContent(o),r=t.match(/^([A-Za-z][A-Za-z0-9:_-]*)\s*,/);if(r&&r[1])return{className:r[1],startLine:o};if(t.includes(";")&&o0&&t--}return t}function W(e,n){const o=[];o.push({value:`**${e.name}**`}),e.group&&o.push({value:`*Group: ${e.group}*`}),e.memo&&o.push({value:e.memo});const t=[];return e.isUnique&&t.push("unique-object"),e.isRequired&&t.push("required-object"),e.minFields>0&&t.push(`min-fields: ${String(e.minFields)}`),e.extensible>0&&t.push(`extensible: ${String(e.extensible)}`),t.length>0&&o.push({value:`\`${t.join(" | ")}\``}),{contents:o,range:{startLineNumber:n.lineNumber,startColumn:1,endLineNumber:n.lineNumber,endColumn:e.name.length+1}}}function B(e,n){const o=[];o.push({value:`**${e.name||e.id}** (${n.name})`});let t=`Type: \`${e.type}\``;if(e.units&&(t+=` | Units: \`${e.units}\``),o.push({value:t}),e.memo&&o.push({value:e.memo}),e.minimum!==void 0||e.maximum!==void 0){let i="Range: ";e.minimum!==void 0&&(i+=e.exclusiveMinimum?`> ${String(e.minimum)}`:`>= ${String(e.minimum)}`),e.minimum!==void 0&&e.maximum!==void 0&&(i+=" and "),e.maximum!==void 0&&(i+=e.exclusiveMaximum?`< ${String(e.maximum)}`:`<= ${String(e.maximum)}`),o.push({value:i})}if(e.default&&o.push({value:`Default: \`${e.default}\``}),e.choices&&e.choices.length>0)if(e.choices.length<=5)o.push({value:`Choices: ${e.choices.map(i=>`\`${i}\``).join(", ")}`});else{const i=e.choices.map(s=>`- \`${s}\``).join(` +`);o.push({value:`Choices (${String(e.choices.length)}): +${i}`})}const r=[];return e.required&&r.push("required"),e.autosizable&&r.push("autosizable"),e.autocalculatable&&r.push("autocalculatable"),r.length>0&&o.push({value:`\`${r.join(" | ")}\``}),{contents:o}}let a=null,u=null;function P(){return a}async function V(){return a||u||(u=(async()=>{try{const e=Z(),n=await fetch(e);return n.ok?(a=await n.json(),console.debug(`[idf-editor] IDD schema loaded: ${a.version}`),a):(console.debug(`[idf-editor] IDD schema not available (${n.status}), hover docs disabled`),null)}catch(e){return console.debug("[idf-editor] Failed to load IDD schema:",e),null}finally{u=null}})(),u)}function Z(){const e=document.querySelector('script[src*="idf-editor"]');if(e){const n=e.src;return new URL("idd-schema.json",n).href}return new URL("/assets/idd-schema.json",window.location.origin).href}let b=!1,d=null;function _(e){b||(e.languages.register({id:c,extensions:[".idf",".imf"],aliases:["IDF","EnergyPlus IDF","Input Data File"],mimetypes:["text/x-idf"]}),e.languages.setLanguageConfiguration(c,I),e.languages.setMonarchTokensProvider(c,F),A(e),T(e,P),b=!0)}async function h(){if(d&&(d.dispose(),d=null),window.innerWidth<768)return;const e=document.querySelectorAll("div.language-idf pre > code");if(e.length!==0)try{const n=await x();_(n),V(),d=new H(n),d.initialize(e)}catch(n){console.error("[idf-editor] Failed to initialize:",n)}}function S(){const n=window.document$;return n?(n.subscribe(()=>{requestAnimationFrame(()=>h())}),!0):!1}if(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>h()):h(),!S()){let e=0;const n=setInterval(()=>{e++,(S()||e>50)&&clearInterval(n)},100)}})(); diff --git a/scripts/assets/idf-fields.css b/scripts/assets/idf-fields.css new file mode 100644 index 000000000..1dacfd5e9 --- /dev/null +++ b/scripts/assets/idf-fields.css @@ -0,0 +1,128 @@ +/* Inline pill/badge styling for IDF field metadata. + * Appears directly after #### Field: headings in the IO Reference. + * Metadata is extracted from the EnergyPlus IDD file. + */ + +/* Container */ +.field-pills { + margin: 0.2em 0 0.5em 0; + line-height: 2; +} + +/* Base pill style */ +.field-pill { + display: inline-block; + font-size: 0.75em; + padding: 0.15em 0.55em; + border-radius: 999px; + font-family: var(--md-code-font-family, monospace); + vertical-align: baseline; + white-space: nowrap; +} + +.field-pill .pill-label { + opacity: 0.65; + font-weight: 400; +} + +/* Type pill — primary accent */ +.field-pill.pill-type { + background: var(--md-accent-fg-color); + color: var(--md-accent-bg-color, #fff); + font-weight: 600; +} + +/* Units pill */ +.field-pill.pill-units { + background: var(--md-code-bg-color); + color: var(--md-code-fg-color); + border: 1px solid var(--md-default-fg-color--lightest); +} + +/* Default pill */ +.field-pill.pill-default { + background: var(--md-code-bg-color); + color: var(--md-code-fg-color); + border: 1px solid var(--md-default-fg-color--lightest); +} + +/* Range pill */ +.field-pill.pill-range { + background: var(--md-code-bg-color); + color: var(--md-code-fg-color); + border: 1px solid var(--md-default-fg-color--lightest); +} + +/* Flag pills (Required, Autosizable, etc.) */ +.field-pill.pill-flag { + background: var(--md-code-bg-color); + color: var(--md-default-fg-color--light); + border: 1px solid var(--md-default-fg-color--lightest); + font-style: italic; +} + +.field-pill.pill-required { + background: hsla(15, 80%, 55%, 0.12); + color: hsl(15, 70%, 45%); + border-color: hsla(15, 70%, 55%, 0.3); + font-weight: 600; + font-style: normal; +} + +/* Dark mode override for Required */ +[data-md-color-scheme="slate"] .field-pill.pill-required { + background: hsla(15, 80%, 55%, 0.18); + color: hsl(15, 70%, 65%); + border-color: hsla(15, 70%, 55%, 0.35); +} + +/* Choices row */ +.field-choices { + margin-top: 0.15em; + line-height: 1.9; +} + +.field-choices code.pill-choice { + font-size: 0.8em; + padding: 0.1em 0.4em; + border-radius: 3px; + margin-right: 0.2em; +} + +/* ── Object separator ── */ +hr.idf-object-separator { + border: none; + border-top: 2px solid var(--md-default-fg-color--lightest); + margin: 2.5em 0 1em; +} + +/* ── Sticky object headings ── + * Any h2/h3 immediately after the separator sticks to the top of the + * viewport while the user scrolls through that object's fields. + */ +hr.idf-object-separator + h2, +hr.idf-object-separator + h3 { + position: sticky; + top: 0; + z-index: 2; + background: var(--md-default-bg-color); + padding-top: 0.4em; + padding-bottom: 0.3em; + margin-top: 0; + /* subtle bottom edge so the heading doesn't blend into content */ + box-shadow: 0 1px 0 var(--md-default-fg-color--lightest); +} + +/* When header is hidden on scroll (e.g. Material "header.autohide"), + * adjust sticky offset to account for the header height. */ +[data-md-header="shadow"] hr.idf-object-separator + h2, +[data-md-header="shadow"] hr.idf-object-separator + h3 { + top: 0; +} + +/* When the Material header is visible, offset below it. + * Material's header is ~3.6rem (default). */ +.md-header--shadow ~ .md-container hr.idf-object-separator + h2, +.md-header--shadow ~ .md-container hr.idf-object-separator + h3 { + top: 0; +} diff --git a/scripts/assets/theme-overrides.css b/scripts/assets/theme-overrides.css new file mode 100644 index 000000000..83ae1093f --- /dev/null +++ b/scripts/assets/theme-overrides.css @@ -0,0 +1,53 @@ +/** + * Zensical / Material Theme Overrides + * + * Fixes for theme features that don't work correctly out of the box. + */ + +/* --------------------------------------------------------------------------- + * Mobile TOC — show "On this page" in the hamburger drawer + * -------------------------------------------------------------------------*/ + +/* + * Zensical hides the TOC toggle and its nav in the primary (mobile) sidebar: + * .md-nav--primary .md-nav__link[for="__toc"], + * .md-nav--primary .md-nav__link[for="__toc"] ~ .md-nav { display: none } + * + * Override on mobile so users can access the page TOC from the hamburger menu. + * The label (page title repeated as a TOC toggle) stays hidden to avoid + * duplication — only the TOC nav itself is shown. + */ +@media screen and (max-width: 76.1875em) { + .md-nav--primary .md-nav__link[for="__toc"] ~ .md-nav { + display: block !important; + visibility: visible !important; + opacity: 1 !important; + } +} + +/* --------------------------------------------------------------------------- + * Collapsible TOC sub-trees (desktop right-side + mobile drawer) + * -------------------------------------------------------------------------*/ + +/* + * Object names (H2) stay visible; their children (Inputs / Field: …) + * collapse by default and expand when the object is the active anchor. + * This keeps the list scannable while preserving deep-link navigation. + * + * Requires toc.follow in the theme features list so that + * md-nav__link--active is set on the currently-visible heading. + */ + +/* Collapse nested lists under each H2 in the TOC */ +.md-nav--secondary > .md-nav__list > .md-nav__item > .md-nav { + max-height: 0; + overflow: hidden; + opacity: 0; + transition: max-height 0.25s ease, opacity 0.2s ease; +} + +/* Expand when the H2 anchor is active (toc.follow sets md-nav__link--active) */ +.md-nav--secondary > .md-nav__list > .md-nav__item > .md-nav__link--active ~ .md-nav { + max-height: 500px; /* large enough for any field list */ + opacity: 1; +} diff --git a/scripts/config.py b/scripts/config.py index 0ebce9e9d..99e6774f2 100644 --- a/scripts/config.py +++ b/scripts/config.py @@ -5,7 +5,7 @@ # Target EnergyPlus versions to convert (oldest to newest) TARGET_VERSIONS: list[str] = [ "v8.9.0", - "v9.0.0", + "v9.0.1", "v9.1.0", "v9.2.0", "v9.3.0", diff --git a/scripts/convert.py b/scripts/convert.py index 8ca5aa6b0..e0e9a6a46 100644 --- a/scripts/convert.py +++ b/scripts/convert.py @@ -37,6 +37,7 @@ from scripts.markdown_postprocessor import postprocess from scripts.models import ConversionResult, DocSet, DocSetResult, LabelRef, VersionResult from scripts.nav_generator import extract_heading, generate_nav, parse_input_chain +from scripts.schema_utils import DocObjectInfo, build_object_index, serialize_for_monaco logger = logging.getLogger(__name__) @@ -294,6 +295,7 @@ def convert_tex_file( doc_set_title: str = "", current_md_path: str = "", figure_numbers: list[int] | None = None, + object_index: dict[str, DocObjectInfo] | None = None, ) -> ConversionResult: """Convert a single .tex file to Markdown via preprocessing -> Pandoc -> postprocessing.""" warnings: list[str] = [] @@ -362,6 +364,7 @@ def convert_tex_file( rel_depth=rel_depth, current_md_path=current_md_path, figure_numbers=figure_numbers, + object_index=object_index, ) # Write output @@ -462,6 +465,7 @@ def _convert_files( label_index: dict[str, LabelRef], max_workers: int, file_figure_numbers: dict[str, list[int]] | None = None, + object_index: dict[str, DocObjectInfo] | None = None, ) -> list[tuple[str, ConversionResult]]: """Run file conversions, using a thread pool when *max_workers* > 1.""" if file_figure_numbers is None: @@ -480,6 +484,7 @@ def _convert_files( doc_set_title, current_md_path, file_figure_numbers.get(current_md_path), + object_index, ): inp for inp, tex_path, output_path, rel_depth, current_md_path in tasks } @@ -499,6 +504,7 @@ def _convert_files( doc_set_title=doc_set_title, current_md_path=current_md_path, figure_numbers=file_figure_numbers.get(current_md_path), + object_index=object_index, ), )) return converted @@ -538,6 +544,7 @@ def convert_doc_set( *, max_workers: int = 1, file_figure_numbers: dict[str, list[int]] | None = None, + object_index: dict[str, DocObjectInfo] | None = None, ) -> DocSetResult: """Convert all files in a doc set. @@ -553,7 +560,9 @@ def convert_doc_set( tasks = _collect_tasks(inputs, doc_set, output_dir, parent_children, result) # Phase 1: Convert files (parallel when max_workers > 1) - converted = _convert_files(tasks, doc_set.slug, doc_set.title, label_index, max_workers, file_figure_numbers) + converted = _convert_files( + tasks, doc_set.slug, doc_set.title, label_index, max_workers, file_figure_numbers, object_index + ) # Phase 2: Log results and append TOCs (must happen after files are written) for inp, file_result in converted: @@ -644,13 +653,20 @@ def generate_zensical_config( # Tags for cmd-k search filtering extra["tags"] = {ds.title: ds.slug for ds in doc_sets} - # MathJax with equation numbering + equation tooltips + # MathJax with equation numbering + equation tooltips + IDF editor project["extra_javascript"] = [ {"path": "assets/mathjax-config.js"}, {"path": "https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.js", "async": True}, {"path": "assets/eq-tooltips.js"}, + {"path": "assets/idf-editor.js", "defer": True}, + ] + project["extra_css"] = [ + "assets/eq-tooltips.css", + "assets/figures.css", + "assets/idf-fields.css", + "assets/idf-editor.css", + "assets/theme-overrides.css", ] - project["extra_css"] = ["assets/eq-tooltips.css", "assets/figures.css"] config_path = output_dir / "zensical.toml" config_path.write_text(tomli_w.dumps(config)) @@ -720,11 +736,27 @@ def convert_version( # Build label index across all doc sets label_index, file_figure_numbers = build_label_index(source_dir, doc_sets) + # Load epJSON schema from idfkit for structured field metadata and hover docs + object_index: dict[str, DocObjectInfo] | None = None + try: + object_index = build_object_index(version) + except Exception: + logger.warning( + "Failed to load epJSON schema for %s, field metadata will not be available", version, exc_info=True + ) + # Convert each doc set for ds in doc_sets: logger.info("Converting doc set: %s", ds.title) + # Only pass object index for IO Reference doc set (contains IDF object field docs) + ds_object_index = object_index if ds.slug == "io-reference" else None ds_result = convert_doc_set( - ds, output_dir, label_index, max_workers=max_workers, file_figure_numbers=file_figure_numbers + ds, + output_dir, + label_index, + max_workers=max_workers, + file_figure_numbers=file_figure_numbers, + object_index=ds_object_index, ) result.doc_set_results.append(ds_result) logger.info( @@ -740,9 +772,14 @@ def convert_version( # Generate zensical config generate_zensical_config(version, doc_sets, output_dir) - # Copy static assets (MathJax config, equation tooltips) + # Copy static assets (MathJax config, equation tooltips, editor bundle) copy_assets(output_dir) + # Generate compact JSON schema for Monaco hover documentation + if object_index: + schema_output = output_dir / "docs" / "assets" / "idd-schema.json" + serialize_for_monaco(object_index, version, schema_output) + # Build site if not skip_build: logger.info("Building Zensical site for %s...", version) diff --git a/scripts/latex_preprocessor.py b/scripts/latex_preprocessor.py index 49428468a..722cd4874 100644 --- a/scripts/latex_preprocessor.py +++ b/scripts/latex_preprocessor.py @@ -145,6 +145,8 @@ def _find_brace_content(text: str, start: int) -> tuple[str, int] | None: "\\CB": ("\\left\\{", "\\right\\}"), } +_BRACKET_MACRO_RE = re.compile(r"\\(?:PB|RB|CB)\{") + def _expand_all_bracket_macros(text: str) -> str: r"""Expand all ``\PB``, ``\RB``, ``\CB`` macros in *text*, inside-out. @@ -152,28 +154,33 @@ def _expand_all_bracket_macros(text: str) -> str: When an outer macro wraps inner macros (e.g. ``\PB{a \PB{b}}``) the inner content is recursively expanded first, so the final result contains no bracket macros regardless of nesting depth. + + Uses regex to jump to the next macro occurrence instead of scanning + every character, which is critical for large files. """ result: list[str] = [] - i = 0 - while i < len(text): - matched_macro = None - for macro in _BRACKET_MACROS: - if text[i:].startswith(macro + "{"): - matched_macro = macro - break - if matched_macro: - brace_start = i + len(matched_macro) - found = _find_brace_content(text, brace_start) - if found: - content, end = found - # Recursively expand any bracket macros inside the content - content = _expand_all_bracket_macros(content) - left, right = _BRACKET_MACROS[matched_macro] - result.append(f"{left} {content} {right}") - i = end - continue - result.append(text[i]) - i += 1 + pos = 0 + while True: + m = _BRACKET_MACRO_RE.search(text, pos) + if m is None: + result.append(text[pos:]) + break + # Append everything before this macro + result.append(text[pos : m.start()]) + macro = text[m.start() : m.end() - 1] # e.g. "\\PB" + brace_start = m.end() - 1 # position of the '{' + found = _find_brace_content(text, brace_start) + if found: + content, end = found + # Recursively expand any bracket macros inside the content + content = _expand_all_bracket_macros(content) + left, right = _BRACKET_MACROS[macro] + result.append(f"{left} {content} {right}") + pos = end + else: + # Unbalanced brace — emit macro text literally and continue + result.append(text[m.start() : m.end()]) + pos = m.end() return "".join(result) diff --git a/scripts/markdown_postprocessor.py b/scripts/markdown_postprocessor.py index 2e7b0572b..43f4f705f 100644 --- a/scripts/markdown_postprocessor.py +++ b/scripts/markdown_postprocessor.py @@ -19,6 +19,7 @@ import re from scripts.models import LabelRef +from scripts.schema_utils import DocFieldInfo, DocObjectInfo # Map PDF filenames used in \href{...} to their doc-set URL slugs. # These are inter-doc-set cross-references left over from the original @@ -394,6 +395,127 @@ def clean_div_wrappers(text: str) -> str: return "\n".join(result) +_IDD_TYPE_LABELS: dict[str, str] = { + "real": "Real", + "integer": "Integer", + "alpha": "Alpha", + "choice": "Choice", + "node": "Node", + "object-list": "Object-List", + "external-list": "External-List", +} + + +def _pill(css_class: str, label: str, value: str = "") -> str: + """Create a single HTML pill span.""" + if value: + return f'{label} {value}' + return f'{label}' + + +def _format_range(field: DocFieldInfo) -> str: + """Format min/max constraints as a compact range string.""" + parts: list[str] = [] + if field.minimum: + op = ">" if field.minimum_exclusive else "\u2265" + parts.append(f"{op} {field.minimum}") + if field.maximum: + op = "<" if field.maximum_exclusive else "\u2264" + parts.append(f"{op} {field.maximum}") + return ", ".join(parts) + + +def _collect_field_pills(field: DocFieldInfo) -> list[str]: + """Collect the ordered list of HTML pill spans for a field.""" + pills: list[str] = [] + + type_label = _IDD_TYPE_LABELS.get(field.field_type) + if type_label: + pills.append(_pill("pill-type", type_label)) + + if field.units: + units_text = field.units + if field.ip_units: + units_text += f" ({field.ip_units})" + pills.append(_pill("pill-units", "Units:", units_text)) + + if field.default: + pills.append(_pill("pill-default", "Default:", field.default)) + + range_str = _format_range(field) + if range_str: + pills.append(_pill("pill-range", "Range:", range_str)) + + # Flag pills + for flag, css, label in [ + (field.required, "pill-required pill-flag", "Required"), + (field.autosizable, "pill-flag", "Autosizable"), + (field.autocalculatable, "pill-flag", "Autocalculatable"), + ]: + if flag: + pills.append(_pill(css, label)) + + return pills + + +def _format_field_attrs(field: DocFieldInfo) -> str: + """Build the HTML pill badges for a single field.""" + pills = _collect_field_pills(field) + + result_parts: list[str] = [] + if pills: + result_parts.append(f'
{" ".join(pills)}
') + + if field.keys: + choices_html = " ".join(f'{k}' for k in field.keys) + result_parts.append(f'
{choices_html}
') + + return "\n".join(result_parts) + + +def inject_field_metadata(text: str, object_index: dict[str, DocObjectInfo]) -> str: + """Inject structured metadata blocks after ``#### Field:`` headings using schema data. + + Scans through the markdown line by line, tracks the current IDD object based + on h1-h3 headings, and for each ``#### Field: `` heading, looks up the + corresponding field metadata and appends inline HTML pills. + """ + lines = text.split("\n") + result: list[str] = [] + current_object: DocObjectInfo | None = None + + heading_re = re.compile(r"^(#{1,3})\s+(.+)$") + field_re = re.compile(r"^####\s+Field:\s*(.+)$") + + for line in lines: + result.append(line) + + # Track current IDD object from h1-h3 headings + h_match = heading_re.match(line) + if h_match: + heading_text = h_match.group(2).strip() + # Strip Pandoc attributes like {#id .class} + heading_text = re.sub(r"\s*\{[#.][^}]*\}", "", heading_text).strip() + if heading_text in object_index: + current_object = object_index[heading_text] + + # Inject pills after #### Field: headings + if current_object: + f_match = field_re.match(line) + if f_match: + field_name = f_match.group(1).strip() + # Strip Pandoc attributes + field_name = re.sub(r"\s*\{[#.][^}]*\}", "", field_name).strip() + idd_field = current_object.fields_by_display_name.get(field_name.lower()) + if idd_field: + pills_html = _format_field_attrs(idd_field) + if pills_html: + result.append("") + result.append(pills_html) + + return "\n".join(result) + + def postprocess( text: str, title: str | None = None, @@ -403,6 +525,7 @@ def postprocess( rel_depth: int = 0, current_md_path: str = "", figure_numbers: list[int] | None = None, + object_index: dict[str, DocObjectInfo] | None = None, ) -> str: """Apply all postprocessing transformations in the correct order.""" if label_index is None: @@ -424,6 +547,8 @@ def postprocess( text = fix_heading_dashes(text) text = clean_empty_links(text) text = clean_div_wrappers(text) + if object_index: + text = inject_field_metadata(text, object_index) text = add_front_matter(text, title, doc_set_title=doc_set_title) return text diff --git a/scripts/pygments_idf_lexer.py b/scripts/pygments_idf_lexer.py new file mode 100644 index 000000000..8985b9665 --- /dev/null +++ b/scripts/pygments_idf_lexer.py @@ -0,0 +1,53 @@ +"""Minimal Pygments lexer for EnergyPlus IDF files. + +Registers the ``idf`` language name with Pygments so that fenced code blocks +tagged as ``idf`` in Markdown are rendered with the correct ``language-idf`` +CSS class instead of falling back to ``language-text``. + +The actual syntax highlighting in the browser is handled by the Monaco-based +IDF editor bundle (``idf-editor/``), so this lexer only needs to provide basic +token classification — enough for Pygments to produce reasonable colour output +as a static fallback. +""" + +from __future__ import annotations + +from typing import ClassVar + +from pygments.lexer import RegexLexer, bygroups +from pygments.token import Comment, Keyword, Name, Number, Punctuation, String, Text + + +class IDFLexer(RegexLexer): + """Pygments lexer for EnergyPlus Input Data Files (.idf).""" + + name = "IDF" + aliases: ClassVar[list[str]] = ["idf"] + filenames: ClassVar[list[str]] = ["*.idf"] + mimetypes: ClassVar[list[str]] = ["text/x-idf"] + + tokens: ClassVar[dict] = { + "root": [ + # Comments: everything after '!' + (r"!.*$", Comment.Single), + # Object terminator + (r";", Punctuation), + # Field separator + (r",", Punctuation), + # Numeric values (integer and float, with optional sign) + (r"-?\d+\.\d*([eE][+-]?\d+)?", Number.Float), + (r"-?\d+([eE][+-]?\d+)?", Number.Integer), + # Special keywords + ( + r"\b(autosize|autocalculate|yes|no)\b", + Keyword, + ), + # Object name (first non-whitespace token on a line before comma/semicolon) + # This matches EnergyPlus class names like "Zone," or "Building," + (r"^([A-Za-z][A-Za-z0-9:_ -]*)(,)", bygroups(Name.Class, Punctuation)), + # Field values (general text) + (r"[^\s,;!]+", String), + # Whitespace + (r"\s+", Text), + ], + } diff --git a/scripts/schema_utils.py b/scripts/schema_utils.py new file mode 100644 index 000000000..3adffce01 --- /dev/null +++ b/scripts/schema_utils.py @@ -0,0 +1,355 @@ +"""Utilities for loading EnergyPlus schema metadata from idfkit. + +Provides unified data structures and functions that serve both: +- Inline field metadata pills in the IO Reference (build-time HTML injection) +- Monaco editor hover documentation (runtime JSON loaded by the browser) + +Replaces custom IDD parsers by delegating to idfkit's bundled epJSON schemas. +""" + +from __future__ import annotations + +import json +import logging +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +from idfkit import get_schema +from idfkit.schema import EpJSONSchema + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Data classes +# --------------------------------------------------------------------------- + + +@dataclass +class DocFieldInfo: + """Unified field metadata for documentation rendering. + + Populated from idfkit's epJSON schema. Serves both the inline pill + generator (markdown_postprocessor) and the Monaco hover JSON serialiser. + """ + + name: str # Human-readable display name (e.g. "Thermal Absorptance") + snake_name: str # Schema key (e.g. "thermal_absorptance") + field_id: str = "" # Reconstructed IDD id (e.g. "A1", "N2") + field_type: str = "" # IDD-style: real, integer, alpha, choice, object-list, external-list + units: str = "" + ip_units: str = "" + default: str = "" + minimum: str = "" + minimum_exclusive: bool = False + maximum: str = "" + maximum_exclusive: bool = False + required: bool = False + autosizable: bool = False + autocalculatable: bool = False + keys: list[str] = field(default_factory=list) + notes: str = "" + + +@dataclass +class DocObjectInfo: + """Object-level schema metadata with field lookup by display name.""" + + name: str # Original case (e.g. "Pump:ConstantSpeed") + group: str = "" + memo: str = "" + fields: list[DocFieldInfo] = field(default_factory=list) + fields_by_display_name: dict[str, DocFieldInfo] = field(default_factory=dict, repr=False) + min_fields: int = 0 + is_unique: bool = False + is_required: bool = False + extensible_size: int = 0 + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + + +def _version_tag_to_tuple(version_tag: str) -> tuple[int, int, int]: + """Convert ``'v25.2.0'`` to ``(25, 2, 0)``.""" + parts = version_tag.lstrip("v").split(".") + return (int(parts[0]), int(parts[1]), int(parts[2])) + + +def _detect_auto_flags(field_schema: dict[str, Any]) -> tuple[bool, bool]: + """Return ``(autosizable, autocalculatable)`` from *anyOf* patterns.""" + autosizable = False + autocalculatable = False + for sub in field_schema.get("anyOf", []): + if sub.get("type") == "string": + enum_vals = sub.get("enum", []) + if "Autosize" in enum_vals: + autosizable = True + if "Autocalculate" in enum_vals: + autocalculatable = True + return autosizable, autocalculatable + + +def _resolve_anyof_type(any_of: list[dict[str, Any]]) -> str | None: + """Resolve an IDD type from an ``anyOf`` schema (autosizable/autocalculatable).""" + for sub in any_of: + if sub.get("type") in ("number", "integer"): + return "integer" if sub["type"] == "integer" else "real" + if any(sub.get("enum") for sub in any_of if sub.get("type") == "string"): + return "choice" + return None + + +def _resolve_string_subtype(field_schema: dict[str, Any]) -> str: + """Classify a JSON ``"string"`` field into an IDD type.""" + if "object_list" in field_schema: + return "object-list" + if field_schema.get("data_type") == "external_list": + return "external-list" + return "alpha" + + +_JSON_TYPE_MAP: dict[str, str] = {"number": "real", "integer": "integer"} + + +def _resolve_idd_type(field_schema: dict[str, Any]) -> str: + """Map an epJSON field schema to an IDD-style type string.""" + if "enum" in field_schema: + return "choice" + + any_of: list[dict[str, Any]] = field_schema.get("anyOf", []) + if any_of: + return _resolve_anyof_type(any_of) or "alpha" + + json_type = field_schema.get("type", "string") + if json_type in _JSON_TYPE_MAP: + return _JSON_TYPE_MAP[json_type] + if json_type == "string": + return _resolve_string_subtype(field_schema) + return "alpha" + + +def _compute_field_ids( + field_names: list[str], + field_info: dict[str, Any], +) -> dict[str, str]: + """Reconstruct IDD field IDs (A1, N1, ...) from legacy_idd metadata.""" + alpha_count = 0 + numeric_count = 0 + ids: dict[str, str] = {} + for name in field_names: + ft = field_info.get(name, {}).get("field_type", "a") + if ft == "a": + alpha_count += 1 + ids[name] = f"A{alpha_count}" + else: + numeric_count += 1 + ids[name] = f"N{numeric_count}" + return ids + + +_CHOICE_EXCLUDE = {"", "Autosize", "Autocalculate"} + + +def _extract_choices(field_schema: dict[str, Any]) -> list[str]: + """Extract enum/choice values, filtering out blanks and Autosize/Autocalculate markers.""" + raw: list[str] = list(field_schema.get("enum", [])) + if not raw: + for sub in field_schema.get("anyOf", []): + if sub.get("type") == "string" and "enum" in sub: + raw.extend(sub["enum"]) + return [v for v in raw if v not in _CHOICE_EXCLUDE] + + +def _build_doc_field( + snake_name: str, + schema: EpJSONSchema, + obj_type: str, + field_info_map: dict[str, Any], + field_ids: dict[str, str], + required_set: set[str], +) -> DocFieldInfo: + """Build a single ``DocFieldInfo`` from the epJSON schema.""" + field_schema: dict[str, Any] = schema.get_field_schema(obj_type, snake_name) or {} + fi_entry = field_info_map.get(snake_name, {}) + display_name: str = fi_entry.get("field_name", snake_name) + + autosizable, autocalculatable = _detect_auto_flags(field_schema) + idd_type = _resolve_idd_type(field_schema) + + # Bounds + minimum_val = "" + minimum_exclusive = False + maximum_val = "" + maximum_exclusive = False + + if "exclusiveMinimum" in field_schema: + minimum_val = str(field_schema["exclusiveMinimum"]) + minimum_exclusive = True + elif "minimum" in field_schema: + minimum_val = str(field_schema["minimum"]) + + if "exclusiveMaximum" in field_schema: + maximum_val = str(field_schema["exclusiveMaximum"]) + maximum_exclusive = True + elif "maximum" in field_schema: + maximum_val = str(field_schema["maximum"]) + + # Default + default_val = "" + if "default" in field_schema: + default_val = str(field_schema["default"]) + + return DocFieldInfo( + name=display_name, + snake_name=snake_name, + field_id=field_ids.get(snake_name, ""), + field_type=idd_type, + units=field_schema.get("units", ""), + ip_units=field_schema.get("ip-units", ""), + default=default_val, + minimum=minimum_val, + minimum_exclusive=minimum_exclusive, + maximum=maximum_val, + maximum_exclusive=maximum_exclusive, + required=snake_name in required_set, + autosizable=autosizable, + autocalculatable=autocalculatable, + keys=_extract_choices(field_schema), + notes=field_schema.get("note", ""), + ) + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def build_object_index(version_tag: str) -> dict[str, DocObjectInfo]: + """Load the epJSON schema for *version_tag* and build the full object index. + + Args: + version_tag: Version string such as ``"v25.2.0"``. + + Returns: + Mapping of object type names (original case) to :class:`DocObjectInfo`. + """ + version_tuple = _version_tag_to_tuple(version_tag) + schema = get_schema(version_tuple) + logger.info( + "Loaded epJSON schema for %s (%d object types)", + version_tag, + len(schema), + ) + + index: dict[str, DocObjectInfo] = {} + + for obj_type in schema.object_types: + obj_schema = schema.get_object_schema(obj_type) + if not obj_schema: + continue + + legacy: dict[str, Any] = obj_schema.get("legacy_idd", {}) + field_info_map: dict[str, Any] = legacy.get("field_info", {}) + all_field_names: list[str] = legacy.get("fields", []) + + # Required fields + inner = schema.get_inner_schema(obj_type) + required_set: set[str] = set(inner.get("required", [])) if inner else set() + + # Reconstruct IDD field IDs + field_ids = _compute_field_ids(all_field_names, field_info_map) + + # Build per-field metadata + doc_fields: list[DocFieldInfo] = [] + fields_by_display: dict[str, DocFieldInfo] = {} + + for snake_name in all_field_names: + doc_field = _build_doc_field(snake_name, schema, obj_type, field_info_map, field_ids, required_set) + doc_fields.append(doc_field) + fields_by_display[doc_field.name.lower()] = doc_field + + # Object-level properties + index[obj_type] = DocObjectInfo( + name=obj_type, + group=obj_schema.get("group", ""), + memo=obj_schema.get("memo", ""), + fields=doc_fields, + fields_by_display_name=fields_by_display, + min_fields=obj_schema.get("min_fields", 0), + is_unique=obj_schema.get("maxProperties", 0) == 1, + is_required=False, + extensible_size=obj_schema.get("extensible_size", 0), + ) + + return index + + +def serialize_for_monaco( + object_index: dict[str, DocObjectInfo], + version_tag: str, + output_path: Path, +) -> bool: + """Serialize the object index to compact JSON for the Monaco hover provider. + + Produces the ``CompactIDDSchema`` format expected by + ``idf-editor/src/types.ts``. + """ + object_types: dict[str, dict[str, Any]] = {} + + for _key, obj in object_index.items(): + fields_json: list[dict[str, Any]] = [] + for f in obj.fields: + fd: dict[str, Any] = { + "id": f.field_id, + "name": f.name, + "type": f.field_type, + "required": f.required, + "memo": f.notes, + "autosizable": f.autosizable, + "autocalculatable": f.autocalculatable, + } + if f.default: + fd["default"] = f.default + if f.units: + fd["units"] = f.units + if f.minimum: + fd["minimum"] = float(f.minimum) + if f.minimum_exclusive: + fd["exclusiveMinimum"] = True + if f.maximum: + fd["maximum"] = float(f.maximum) + if f.maximum_exclusive: + fd["exclusiveMaximum"] = True + if f.keys: + fd["choices"] = f.keys + fields_json.append(fd) + + object_types[obj.name.lower()] = { + "name": obj.name, + "group": obj.group, + "memo": obj.memo, + "fields": fields_json, + "minFields": obj.min_fields, + "isUnique": obj.is_unique, + "isRequired": obj.is_required, + "extensible": obj.extensible_size, + } + + schema_json: dict[str, Any] = { + "version": version_tag.lstrip("v"), + "objectTypes": object_types, + } + + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(schema_json, separators=(",", ":"))) + + logger.info( + "Wrote Monaco schema JSON (%d object types, %.1f KB) to %s", + len(object_types), + output_path.stat().st_size / 1024, + output_path, + ) + return True diff --git a/uv.lock b/uv.lock index 55f4a6408..747a1da8f 100644 --- a/uv.lock +++ b/uv.lock @@ -68,11 +68,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b8/58/40fbbcefeda82364720eba5cf2270f98496bdfa19ea75b4cccae79c698e6/identify-2.6.16-py2.py3-none-any.whl", hash = "sha256:391ee4d77741d994189522896270b787aed8670389bfd60f326d677d64a6dfb0", size = 99202, upload-time = "2026-01-12T18:58:56.627Z" }, ] +[[package]] +name = "idfkit" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dd/0a/35fc74570f424dee8edd89d608bdf80452130e70b7815b8ef924370e083a/idfkit-0.3.0.tar.gz", hash = "sha256:cdb3ba19b4d5c6ac68d9bd7c8681824f89296cae27a6309b4d24a0ba3849d6f8", size = 12872869, upload-time = "2026-02-26T22:22:56.442Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/1c/629da6c4041095662d001c4e336d2c4070585beeddca1a0a42c126c3c6dd/idfkit-0.3.0-py3-none-any.whl", hash = "sha256:e29c02b263111e37a277e75f636316d041ff4e2015695fa17e0a24f5346e66ab", size = 12275791, upload-time = "2026-02-26T22:22:53.994Z" }, +] + [[package]] name = "idfkit-docs" version = "0.0.1" -source = { virtual = "." } +source = { editable = "." } dependencies = [ + { name = "idfkit" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tomli-w" }, ] @@ -88,6 +98,7 @@ dev = [ [package.metadata] requires-dist = [ + { name = "idfkit", specifier = ">=0.3.0" }, { name = "tomli", marker = "python_full_version < '3.11'", specifier = ">=2.0" }, { name = "tomli-w", specifier = ">=1.0" }, ] diff --git a/zensical.toml b/zensical.toml index c14ff9f7b..01535c340 100644 --- a/zensical.toml +++ b/zensical.toml @@ -19,6 +19,7 @@ features = [ "navigation.footer", "navigation.tracking", "content.code.copy", + "toc.follow", ] [project.theme.icon] @@ -41,3 +42,9 @@ accent = "amber" [project.theme.palette.toggle] icon = "material/brightness-4" name = "Switch to light mode" + +[project.markdown_extensions.toc] +toc_depth = 4 + +[project.markdown_extensions."pymdownx.arithmatex"] +generic = true From a5eadc93d19af43d45790e7df360c85b52dfdff4 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 27 Feb 2026 20:45:15 -0500 Subject: [PATCH 2/4] Fix IDF code block rendering and add editor build to PR preview Move markdown extensions from zensical.toml to convert.py so that Zensical's defaults (superfences, highlight, etc.) are not overridden. Without superfences, fenced code blocks rendered as inline tags. Also add Node.js setup and IDF editor build steps to the PR preview workflow so the Monaco editor bundle is available in previews. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/pr-docs-preview.yml | 12 +++++++++ scripts/convert.py | 37 +++++++++++++++++++++++++++ zensical.toml | 6 ----- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-docs-preview.yml b/.github/workflows/pr-docs-preview.yml index 0fb4c02ef..76984bdf8 100644 --- a/.github/workflows/pr-docs-preview.yml +++ b/.github/workflows/pr-docs-preview.yml @@ -34,6 +34,18 @@ jobs: sudo apt-get update sudo apt-get install -y pandoc + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: idf-editor/package-lock.json + + - name: Build IDF editor bundle + run: | + cd idf-editor && npm ci --silent && npm run build + cp dist/idf-editor.js dist/idf-editor.css ../scripts/assets/ + - name: Determine latest version id: version run: | diff --git a/scripts/convert.py b/scripts/convert.py index e0e9a6a46..90fddd027 100644 --- a/scripts/convert.py +++ b/scripts/convert.py @@ -653,6 +653,43 @@ def generate_zensical_config( # Tags for cmd-k search filtering extra["tags"] = {ds.title: ds.slug for ds in doc_sets} + # Markdown extensions — specify explicitly so we don't lose Zensical's + # defaults (superfences, highlight, etc.) when adding our overrides. + # Omitting this from zensical.toml would work for `make docs-test`, but + # since we need toc_depth=4 and arithmatex, we must list everything. + project["markdown_extensions"] = { + "abbr": {}, + "admonition": {}, + "attr_list": {}, + "def_list": {}, + "footnotes": {}, + "md_in_html": {}, + "toc": {"permalink": True, "toc_depth": 4}, + "pymdownx.arithmatex": {"generic": True}, + "pymdownx.betterem": {}, + "pymdownx.caret": {}, + "pymdownx.details": {}, + "pymdownx.highlight": { + "anchor_linenums": True, + "line_spans": "__span", + "pygments_lang_class": True, + }, + "pymdownx.inlinehilite": {}, + "pymdownx.keys": {}, + "pymdownx.magiclink": {}, + "pymdownx.mark": {}, + "pymdownx.smartsymbols": {}, + "pymdownx.superfences": { + "custom_fences": [{"name": "mermaid", "class": "mermaid"}], + }, + "pymdownx.tabbed": { + "alternate_style": True, + "combine_header_slug": True, + }, + "pymdownx.tasklist": {"custom_checkbox": True}, + "pymdownx.tilde": {}, + } + # MathJax with equation numbering + equation tooltips + IDF editor project["extra_javascript"] = [ {"path": "assets/mathjax-config.js"}, diff --git a/zensical.toml b/zensical.toml index 01535c340..fe87fe827 100644 --- a/zensical.toml +++ b/zensical.toml @@ -42,9 +42,3 @@ accent = "amber" [project.theme.palette.toggle] icon = "material/brightness-4" name = "Switch to light mode" - -[project.markdown_extensions.toc] -toc_depth = 4 - -[project.markdown_extensions."pymdownx.arithmatex"] -generic = true From bbea3b39b9c1703e1ce9f80578026214742b80f2 Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 27 Feb 2026 21:33:18 -0500 Subject: [PATCH 3/4] Fix Monaco editor breaking on TOC clicks and duplicate loader errors document$ is a ReplaySubject that replays its last value on subscribe, causing initPage() to run twice concurrently on initial load. The race condition injected loader.js twice, triggering a _commonjsGlobal duplicate variable error. Additionally, Zensical replaces the DOM even for same-page anchor clicks, so editors need to re-initialize after each document$ emission. Fixes: - Skip the initial ReplaySubject emission (matching mathjax-config.js) - Add initInProgress guard to prevent concurrent initPage() calls - Cache the loadMonacoFromCDN() promise to prevent duplicate injection Co-Authored-By: Claude Opus 4.6 --- idf-editor/src/editor-manager.ts | 12 ++++++-- idf-editor/src/main.ts | 47 ++++++++++++++++++++++---------- scripts/assets/idf-editor.js | 8 +++--- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/idf-editor/src/editor-manager.ts b/idf-editor/src/editor-manager.ts index 903629458..9124856d7 100644 --- a/idf-editor/src/editor-manager.ts +++ b/idf-editor/src/editor-manager.ts @@ -241,14 +241,20 @@ export class EditorManager { } } +/** Cached promise for Monaco loading (prevents duplicate loader injection) */ +let monacoLoadPromise: Promise | null = null; + /** * Load Monaco editor from CDN using the AMD loader. * * Returns the Monaco editor module. The loader and editor core are - * cached by the browser across page navigations. + * cached by the browser across page navigations. The promise is + * cached so concurrent calls share the same load operation. */ export function loadMonacoFromCDN(): Promise { - return new Promise((resolve, reject) => { + if (monacoLoadPromise) return monacoLoadPromise; + + monacoLoadPromise = new Promise((resolve, reject) => { // Check if Monaco is already loaded const win = window as Record; if (win.monaco) { @@ -269,6 +275,8 @@ export function loadMonacoFromCDN(): Promise { script.onerror = () => reject(new Error('Failed to load Monaco AMD loader')); document.head.appendChild(script); }); + + return monacoLoadPromise; } function configureAndLoadMonaco( diff --git a/idf-editor/src/main.ts b/idf-editor/src/main.ts index 755d4e753..e540915d7 100644 --- a/idf-editor/src/main.ts +++ b/idf-editor/src/main.ts @@ -22,6 +22,9 @@ let languageRegistered = false; /** Current editor manager (recreated on each page navigation) */ let currentManager: EditorManager | null = null; +/** Whether initPage() is currently running (prevents concurrent execution) */ +let initInProgress = false; + /** * Register the IDF language, themes, and providers with Monaco. * Only done once globally (Monaco language registration is persistent). @@ -53,23 +56,28 @@ function registerLanguage(monaco: typeof Monaco): void { * Called on initial load and after each instant navigation. */ async function initPage(): Promise { - // Dispose previous editors (from prior page) - if (currentManager) { - currentManager.dispose(); - currentManager = null; - } + // Prevent concurrent execution (e.g. document$ ReplaySubject firing + // while the initial initPage() is still loading Monaco from CDN). + if (initInProgress) return; + initInProgress = true; - // Skip Monaco on narrow viewports — touch devices lack hover and the - // editors need horizontal space. Pygments static highlighting remains. - if (window.innerWidth < 768) return; + try { + // Dispose previous editors (from prior page) + if (currentManager) { + currentManager.dispose(); + currentManager = null; + } - // Check if there are IDF code blocks on this page. - // Zensical/pymdownx puts the language class on a wrapper
, not on . - // Structure:
...
- const codeBlocks = document.querySelectorAll('div.language-idf pre > code'); - if (codeBlocks.length === 0) return; + // Skip Monaco on narrow viewports — touch devices lack hover and the + // editors need horizontal space. Pygments static highlighting remains. + if (window.innerWidth < 768) return; + + // Check if there are IDF code blocks on this page. + // Zensical/pymdownx puts the language class on a wrapper
, not on . + // Structure:
...
+ const codeBlocks = document.querySelectorAll('div.language-idf pre > code'); + if (codeBlocks.length === 0) return; - try { // Load Monaco from CDN (cached after first load) const monaco = await loadMonacoFromCDN(); @@ -84,6 +92,8 @@ async function initPage(): Promise { currentManager.initialize(codeBlocks); } catch (error) { console.error('[idf-editor] Failed to initialize:', error); + } finally { + initInProgress = false; } } @@ -97,7 +107,16 @@ function hookInstantNav(): boolean { if (!document$) return false; + // document$ is a ReplaySubject — it replays the last value on subscribe. + // Skip that initial emission since initPage() already handles the first load. + let firstEmission = true; + document$.subscribe(() => { + if (firstEmission) { + firstEmission = false; + return; + } + // Small delay to ensure the DOM is updated requestAnimationFrame(() => initPage()); }); diff --git a/scripts/assets/idf-editor.js b/scripts/assets/idf-editor.js index 197b9fa21..6b885a81f 100644 --- a/scripts/assets/idf-editor.js +++ b/scripts/assets/idf-editor.js @@ -1,4 +1,4 @@ -(function(){"use strict";const w=[{token:"comment",foreground:"6A9955"},{token:"comment.doc",foreground:"6A9955",fontStyle:"italic"},{token:"type",foreground:"4EC9B0"},{token:"type.identifier",foreground:"4EC9B0",fontStyle:"bold"},{token:"keyword",foreground:"569CD6"},{token:"number",foreground:"B5CEA8"},{token:"number.float",foreground:"B5CEA8"},{token:"string",foreground:"CE9178"},{token:"string.date",foreground:"DCDCAA"},{token:"delimiter",foreground:"D4D4D4"},{token:"delimiter.semicolon",foreground:"D4D4D4",fontStyle:"bold"},{token:"constant",foreground:"4FC1FF"}],k=[{token:"comment",foreground:"008000"},{token:"comment.doc",foreground:"008000",fontStyle:"italic"},{token:"type",foreground:"267F99"},{token:"type.identifier",foreground:"267F99",fontStyle:"bold"},{token:"keyword",foreground:"0000FF"},{token:"number",foreground:"098658"},{token:"number.float",foreground:"098658"},{token:"string",foreground:"A31515"},{token:"string.date",foreground:"795E26"},{token:"delimiter",foreground:"000000"},{token:"delimiter.semicolon",foreground:"000000",fontStyle:"bold"},{token:"constant",foreground:"0070C1"}],m="idf-docs-light",g="idf-docs-dark";function A(e){e.editor.defineTheme(g,{base:"vs-dark",inherit:!0,rules:w,colors:{"editor.background":"#212121","editor.foreground":"#e2e8f0","editor.lineHighlightBackground":"#2d2d2d","editor.selectionBackground":"#264f78","editorLineNumber.foreground":"#6b7280","editorCursor.foreground":"#e2e8f0","editorWidget.background":"#2d2d2d","editorWidget.border":"#404040","editorHoverWidget.background":"#2d2d2d","editorHoverWidget.border":"#404040"}}),e.editor.defineTheme(m,{base:"vs",inherit:!0,rules:k,colors:{"editor.background":"#f5f5f5","editor.foreground":"#1a202c","editor.lineHighlightBackground":"#f0f0f0","editor.selectionBackground":"#c8e1ff","editorLineNumber.foreground":"#a0aec0","editorCursor.foreground":"#1a202c","editorWidget.background":"#ffffff","editorWidget.border":"#e2e8f0","editorHoverWidget.background":"#ffffff","editorHoverWidget.border":"#e2e8f0"}})}function D(){return document.body.getAttribute("data-md-color-scheme")==="slate"?g:m}const L=60,E=720,M=19,N=20,O="200px",p="https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs",f='',C='';class H{constructor(n){this.editors=[],this.observer=null,this.themeObserver=null,this.pendingBlocks=new Map,this.monaco=n}initialize(n){n.length!==0&&(this.observer=new IntersectionObserver(o=>this.handleIntersection(o),{rootMargin:O}),n.forEach(o=>{const t=o.parentElement;if(!t||t.tagName!=="PRE")return;const r=t.parentElement;if(!r||r.dataset.idfEditor==="true")return;const i=o.textContent||"";this.pendingBlocks.set(r,{wrapper:r,code:i}),this.observer.observe(r)}),this.watchThemeChanges())}dispose(){var n,o;for(const{editor:t}of this.editors)t.dispose();this.editors=[],(n=this.observer)==null||n.disconnect(),this.observer=null,(o=this.themeObserver)==null||o.disconnect(),this.themeObserver=null,this.pendingBlocks.clear()}handleIntersection(n){var o;for(const t of n){if(!t.isIntersecting)continue;const r=this.pendingBlocks.get(t.target);r&&((o=this.observer)==null||o.unobserve(t.target),this.pendingBlocks.delete(t.target),this.createEditor(r.wrapper,r.code))}}createEditor(n,o){var l;const t=o.split(` -`).length,r=Math.max(L,Math.min(E,t*M+N)),i=document.createElement("div");i.className="idf-editor-container",i.style.height=`${r}px`,(l=n.parentNode)==null||l.replaceChild(i,n),n.dataset.idfEditor="true";const s=this.monaco.editor.create(i,{value:o,language:"idf",theme:D(),readOnly:!0,domReadOnly:!0,minimap:{enabled:!1},lineNumbers:"on",scrollBeyondLastLine:!1,wordWrap:"off",folding:!1,glyphMargin:!1,lineDecorationsWidth:8,lineNumbersMinChars:3,renderLineHighlight:"none",overviewRulerLanes:0,hideCursorInOverviewRuler:!0,overviewRulerBorder:!1,scrollbar:{vertical:"auto",horizontal:"auto",verticalScrollbarSize:8,horizontalScrollbarSize:8},fontSize:13,fontFamily:"'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, monospace",padding:{top:8,bottom:8},automaticLayout:!0,contextmenu:!1,links:!1,renderValidationDecorations:"off",fixedOverflowWidgets:!0,accessibilitySupport:"off",ariaLabel:"EnergyPlus IDF code example"});this.addCopyButton(i,s),this.editors.push({editor:s,container:i})}addCopyButton(n,o){const t=document.createElement("button");t.className="idf-editor-copy",t.title="Copy to clipboard",t.innerHTML=f,t.addEventListener("click",async()=>{try{const r=o.getValue();await navigator.clipboard.writeText(r),t.innerHTML=C,t.classList.add("copied"),setTimeout(()=>{t.innerHTML=f,t.classList.remove("copied")},2e3)}catch{const r=document.createElement("textarea");r.value=o.getValue(),r.style.position="fixed",r.style.opacity="0",document.body.appendChild(r),r.select(),document.execCommand("copy"),document.body.removeChild(r),t.innerHTML=C,t.classList.add("copied"),setTimeout(()=>{t.innerHTML=f,t.classList.remove("copied")},2e3)}}),n.appendChild(t)}watchThemeChanges(){this.themeObserver=new MutationObserver(n=>{for(const o of n)if(o.attributeName==="data-md-color-scheme"){const r=document.body.getAttribute("data-md-color-scheme")==="slate"?g:m;this.monaco.editor.setTheme(r)}}),this.themeObserver.observe(document.body,{attributes:!0,attributeFilter:["data-md-color-scheme"]})}}function x(){return new Promise((e,n)=>{const o=window;if(o.monaco){e(o.monaco);return}if(typeof o.require=="function"&&o.require.config){v(e,n);return}const t=document.createElement("script");t.src=`${p}/loader.js`,t.onload=()=>v(e,n),t.onerror=()=>n(new Error("Failed to load Monaco AMD loader")),document.head.appendChild(t)})}function v(e,n){const t=window.require;t.config({paths:{vs:p}}),t(["vs/editor/editor.main"],r=>{e(r)},r=>{n(r)})}const c="idf",I={comments:{lineComment:"!"},brackets:[],autoClosingPairs:[],surroundingPairs:[],folding:{markers:{start:/^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i,end:/^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i}},wordPattern:/[A-Za-z][A-Za-z0-9:_-]*/},F={defaultToken:"invalid",tokenPostfix:".idf",ignoreCase:!0,classes:["Version","SimulationControl","Building","Timestep","RunPeriod","Site:Location","SizingPeriod:DesignDay","GlobalGeometryRules","Zone","ZoneList","BuildingSurface:Detailed","FenestrationSurface:Detailed","Wall:Exterior","Wall:Interior","Roof","Floor:GroundContact","Window","Door","Material","Material:NoMass","Material:AirGap","WindowMaterial:SimpleGlazingSystem","WindowMaterial:Glazing","Construction","Schedule:Compact","Schedule:Constant","Schedule:Day:Interval","Schedule:Week:Daily","Schedule:Year","ScheduleTypeLimits","People","Lights","ElectricEquipment","ZoneInfiltration:DesignFlowRate","ZoneVentilation:DesignFlowRate","Sizing:Zone","Sizing:System","Sizing:Plant","ZoneHVAC:IdealLoadsAirSystem","ZoneHVAC:EquipmentList","ZoneHVAC:EquipmentConnections","ThermostatSetpoint:SingleHeating","ThermostatSetpoint:SingleCooling","ThermostatSetpoint:DualSetpoint","ZoneControl:Thermostat","AirLoopHVAC","AirLoopHVAC:ZoneSplitter","AirLoopHVAC:ZoneMixer","Fan:ConstantVolume","Fan:VariableVolume","Fan:OnOff","Coil:Heating:Electric","Coil:Heating:Fuel","Coil:Heating:Water","Coil:Cooling:DX:SingleSpeed","Coil:Cooling:DX:TwoSpeed","Coil:Cooling:Water","Controller:OutdoorAir","AirLoopHVAC:OutdoorAirSystem","OutdoorAir:Mixer","SetpointManager:Scheduled","SetpointManager:SingleZone:Reheat","PlantLoop","Pump:ConstantSpeed","Pump:VariableSpeed","Boiler:HotWater","Chiller:Electric:EIR","CoolingTower:SingleSpeed","Output:Variable","Output:Meter","Output:Table:Monthly","Output:Table:SummaryReports","OutputControl:Table:Style"],keywords:["Yes","No","On","Off","True","False","autocalculate","autosize","Continuous","Discrete","Any Number","Hourly","Timestep","Daily","Monthly","RunPeriod","Annual","SummerDesignDay","WinterDesignDay","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Holiday","CustomDay1","CustomDay2","AllDays","Weekdays","Weekends","AllOtherDays"],operators:[",",";"],digits:/\d+/,floatDigits:/\d*\.\d+([eE][+-]?\d+)?/,tokenizer:{root:[{include:"@whitespace"},[/!-.*$/,"comment.doc"],[/!.*$/,"comment"],[/^([A-Za-z][A-Za-z0-9:_-]*)\s*(,)/,[{cases:{"@classes":"type.identifier","@default":"type"}},"delimiter"]],{include:"@fieldValue"},[/[,]/,"delimiter"],[/[;]/,"delimiter.semicolon"]],whitespace:[[/[ \t\r\n]+/,"white"]],fieldValue:[[/-?\d*\.\d+([eE][+-]?\d+)?/,"number.float"],[/-?\d+([eE][+-]?\d+)?/,"number"],[/[A-Za-z][A-Za-z0-9_-]*/,{cases:{"@keywords":"keyword","@default":"string"}}],[/\*/,"constant"],[/Through:\s*\d+\/\d+/,"string.date"],[/For:\s*[A-Za-z,\s]+/,"string.date"],[/Until:\s*\d+:\d+/,"string.date"],[/Interpolate:\s*[A-Za-z]+/,"string.date"]]}};function T(e,n){return e.languages.registerHoverProvider(c,{provideHover(o,t){const r=n();if(!r)return null;const i=z(o,t);if(!i)return null;const s=r.objectTypes[i.className.toLowerCase()];if(!s)return null;if(i.isClassName)return W(s,t);if(i.fieldIndex!==void 0&&i.fieldIndex=1;o--){const t=e.getLineContent(o),r=t.match(/^([A-Za-z][A-Za-z0-9:_-]*)\s*,/);if(r&&r[1])return{className:r[1],startLine:o};if(t.includes(";")&&o0&&t--}return t}function W(e,n){const o=[];o.push({value:`**${e.name}**`}),e.group&&o.push({value:`*Group: ${e.group}*`}),e.memo&&o.push({value:e.memo});const t=[];return e.isUnique&&t.push("unique-object"),e.isRequired&&t.push("required-object"),e.minFields>0&&t.push(`min-fields: ${String(e.minFields)}`),e.extensible>0&&t.push(`extensible: ${String(e.extensible)}`),t.length>0&&o.push({value:`\`${t.join(" | ")}\``}),{contents:o,range:{startLineNumber:n.lineNumber,startColumn:1,endLineNumber:n.lineNumber,endColumn:e.name.length+1}}}function B(e,n){const o=[];o.push({value:`**${e.name||e.id}** (${n.name})`});let t=`Type: \`${e.type}\``;if(e.units&&(t+=` | Units: \`${e.units}\``),o.push({value:t}),e.memo&&o.push({value:e.memo}),e.minimum!==void 0||e.maximum!==void 0){let i="Range: ";e.minimum!==void 0&&(i+=e.exclusiveMinimum?`> ${String(e.minimum)}`:`>= ${String(e.minimum)}`),e.minimum!==void 0&&e.maximum!==void 0&&(i+=" and "),e.maximum!==void 0&&(i+=e.exclusiveMaximum?`< ${String(e.maximum)}`:`<= ${String(e.maximum)}`),o.push({value:i})}if(e.default&&o.push({value:`Default: \`${e.default}\``}),e.choices&&e.choices.length>0)if(e.choices.length<=5)o.push({value:`Choices: ${e.choices.map(i=>`\`${i}\``).join(", ")}`});else{const i=e.choices.map(s=>`- \`${s}\``).join(` -`);o.push({value:`Choices (${String(e.choices.length)}): -${i}`})}const r=[];return e.required&&r.push("required"),e.autosizable&&r.push("autosizable"),e.autocalculatable&&r.push("autocalculatable"),r.length>0&&o.push({value:`\`${r.join(" | ")}\``}),{contents:o}}let a=null,u=null;function P(){return a}async function V(){return a||u||(u=(async()=>{try{const e=Z(),n=await fetch(e);return n.ok?(a=await n.json(),console.debug(`[idf-editor] IDD schema loaded: ${a.version}`),a):(console.debug(`[idf-editor] IDD schema not available (${n.status}), hover docs disabled`),null)}catch(e){return console.debug("[idf-editor] Failed to load IDD schema:",e),null}finally{u=null}})(),u)}function Z(){const e=document.querySelector('script[src*="idf-editor"]');if(e){const n=e.src;return new URL("idd-schema.json",n).href}return new URL("/assets/idd-schema.json",window.location.origin).href}let b=!1,d=null;function _(e){b||(e.languages.register({id:c,extensions:[".idf",".imf"],aliases:["IDF","EnergyPlus IDF","Input Data File"],mimetypes:["text/x-idf"]}),e.languages.setLanguageConfiguration(c,I),e.languages.setMonarchTokensProvider(c,F),A(e),T(e,P),b=!0)}async function h(){if(d&&(d.dispose(),d=null),window.innerWidth<768)return;const e=document.querySelectorAll("div.language-idf pre > code");if(e.length!==0)try{const n=await x();_(n),V(),d=new H(n),d.initialize(e)}catch(n){console.error("[idf-editor] Failed to initialize:",n)}}function S(){const n=window.document$;return n?(n.subscribe(()=>{requestAnimationFrame(()=>h())}),!0):!1}if(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>h()):h(),!S()){let e=0;const n=setInterval(()=>{e++,(S()||e>50)&&clearInterval(n)},100)}})(); +(function(){"use strict";const A=[{token:"comment",foreground:"6A9955"},{token:"comment.doc",foreground:"6A9955",fontStyle:"italic"},{token:"type",foreground:"4EC9B0"},{token:"type.identifier",foreground:"4EC9B0",fontStyle:"bold"},{token:"keyword",foreground:"569CD6"},{token:"number",foreground:"B5CEA8"},{token:"number.float",foreground:"B5CEA8"},{token:"string",foreground:"CE9178"},{token:"string.date",foreground:"DCDCAA"},{token:"delimiter",foreground:"D4D4D4"},{token:"delimiter.semicolon",foreground:"D4D4D4",fontStyle:"bold"},{token:"constant",foreground:"4FC1FF"}],L=[{token:"comment",foreground:"008000"},{token:"comment.doc",foreground:"008000",fontStyle:"italic"},{token:"type",foreground:"267F99"},{token:"type.identifier",foreground:"267F99",fontStyle:"bold"},{token:"keyword",foreground:"0000FF"},{token:"number",foreground:"098658"},{token:"number.float",foreground:"098658"},{token:"string",foreground:"A31515"},{token:"string.date",foreground:"795E26"},{token:"delimiter",foreground:"000000"},{token:"delimiter.semicolon",foreground:"000000",fontStyle:"bold"},{token:"constant",foreground:"0070C1"}],f="idf-docs-light",g="idf-docs-dark";function D(e){e.editor.defineTheme(g,{base:"vs-dark",inherit:!0,rules:A,colors:{"editor.background":"#212121","editor.foreground":"#e2e8f0","editor.lineHighlightBackground":"#2d2d2d","editor.selectionBackground":"#264f78","editorLineNumber.foreground":"#6b7280","editorCursor.foreground":"#e2e8f0","editorWidget.background":"#2d2d2d","editorWidget.border":"#404040","editorHoverWidget.background":"#2d2d2d","editorHoverWidget.border":"#404040"}}),e.editor.defineTheme(f,{base:"vs",inherit:!0,rules:L,colors:{"editor.background":"#f5f5f5","editor.foreground":"#1a202c","editor.lineHighlightBackground":"#f0f0f0","editor.selectionBackground":"#c8e1ff","editorLineNumber.foreground":"#a0aec0","editorCursor.foreground":"#1a202c","editorWidget.background":"#ffffff","editorWidget.border":"#e2e8f0","editorHoverWidget.background":"#ffffff","editorHoverWidget.border":"#e2e8f0"}})}function E(){return document.body.getAttribute("data-md-color-scheme")==="slate"?g:f}const M=60,N=720,O=19,H=20,x="200px",v="https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs",h='',b='';class I{constructor(o){this.editors=[],this.observer=null,this.themeObserver=null,this.pendingBlocks=new Map,this.monaco=o}initialize(o){o.length!==0&&(this.observer=new IntersectionObserver(n=>this.handleIntersection(n),{rootMargin:x}),o.forEach(n=>{const t=n.parentElement;if(!t||t.tagName!=="PRE")return;const r=t.parentElement;if(!r||r.dataset.idfEditor==="true")return;const i=n.textContent||"";this.pendingBlocks.set(r,{wrapper:r,code:i}),this.observer.observe(r)}),this.watchThemeChanges())}dispose(){var o,n;for(const{editor:t}of this.editors)t.dispose();this.editors=[],(o=this.observer)==null||o.disconnect(),this.observer=null,(n=this.themeObserver)==null||n.disconnect(),this.themeObserver=null,this.pendingBlocks.clear()}handleIntersection(o){var n;for(const t of o){if(!t.isIntersecting)continue;const r=this.pendingBlocks.get(t.target);r&&((n=this.observer)==null||n.unobserve(t.target),this.pendingBlocks.delete(t.target),this.createEditor(r.wrapper,r.code))}}createEditor(o,n){var l;const t=n.split(` +`).length,r=Math.max(M,Math.min(N,t*O+H)),i=document.createElement("div");i.className="idf-editor-container",i.style.height=`${r}px`,(l=o.parentNode)==null||l.replaceChild(i,o),o.dataset.idfEditor="true";const s=this.monaco.editor.create(i,{value:n,language:"idf",theme:E(),readOnly:!0,domReadOnly:!0,minimap:{enabled:!1},lineNumbers:"on",scrollBeyondLastLine:!1,wordWrap:"off",folding:!1,glyphMargin:!1,lineDecorationsWidth:8,lineNumbersMinChars:3,renderLineHighlight:"none",overviewRulerLanes:0,hideCursorInOverviewRuler:!0,overviewRulerBorder:!1,scrollbar:{vertical:"auto",horizontal:"auto",verticalScrollbarSize:8,horizontalScrollbarSize:8},fontSize:13,fontFamily:"'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, monospace",padding:{top:8,bottom:8},automaticLayout:!0,contextmenu:!1,links:!1,renderValidationDecorations:"off",fixedOverflowWidgets:!0,accessibilitySupport:"off",ariaLabel:"EnergyPlus IDF code example"});this.addCopyButton(i,s),this.editors.push({editor:s,container:i})}addCopyButton(o,n){const t=document.createElement("button");t.className="idf-editor-copy",t.title="Copy to clipboard",t.innerHTML=h,t.addEventListener("click",async()=>{try{const r=n.getValue();await navigator.clipboard.writeText(r),t.innerHTML=b,t.classList.add("copied"),setTimeout(()=>{t.innerHTML=h,t.classList.remove("copied")},2e3)}catch{const r=document.createElement("textarea");r.value=n.getValue(),r.style.position="fixed",r.style.opacity="0",document.body.appendChild(r),r.select(),document.execCommand("copy"),document.body.removeChild(r),t.innerHTML=b,t.classList.add("copied"),setTimeout(()=>{t.innerHTML=h,t.classList.remove("copied")},2e3)}}),o.appendChild(t)}watchThemeChanges(){this.themeObserver=new MutationObserver(o=>{for(const n of o)if(n.attributeName==="data-md-color-scheme"){const r=document.body.getAttribute("data-md-color-scheme")==="slate"?g:f;this.monaco.editor.setTheme(r)}}),this.themeObserver.observe(document.body,{attributes:!0,attributeFilter:["data-md-color-scheme"]})}}let c=null;function F(){return c||(c=new Promise((e,o)=>{const n=window;if(n.monaco){e(n.monaco);return}if(typeof n.require=="function"&&n.require.config){S(e,o);return}const t=document.createElement("script");t.src=`${v}/loader.js`,t.onload=()=>S(e,o),t.onerror=()=>o(new Error("Failed to load Monaco AMD loader")),document.head.appendChild(t)}),c)}function S(e,o){const t=window.require;t.config({paths:{vs:v}}),t(["vs/editor/editor.main"],r=>{e(r)},r=>{o(r)})}const m="idf",T={comments:{lineComment:"!"},brackets:[],autoClosingPairs:[],surroundingPairs:[],folding:{markers:{start:/^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i,end:/^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i}},wordPattern:/[A-Za-z][A-Za-z0-9:_-]*/},z={defaultToken:"invalid",tokenPostfix:".idf",ignoreCase:!0,classes:["Version","SimulationControl","Building","Timestep","RunPeriod","Site:Location","SizingPeriod:DesignDay","GlobalGeometryRules","Zone","ZoneList","BuildingSurface:Detailed","FenestrationSurface:Detailed","Wall:Exterior","Wall:Interior","Roof","Floor:GroundContact","Window","Door","Material","Material:NoMass","Material:AirGap","WindowMaterial:SimpleGlazingSystem","WindowMaterial:Glazing","Construction","Schedule:Compact","Schedule:Constant","Schedule:Day:Interval","Schedule:Week:Daily","Schedule:Year","ScheduleTypeLimits","People","Lights","ElectricEquipment","ZoneInfiltration:DesignFlowRate","ZoneVentilation:DesignFlowRate","Sizing:Zone","Sizing:System","Sizing:Plant","ZoneHVAC:IdealLoadsAirSystem","ZoneHVAC:EquipmentList","ZoneHVAC:EquipmentConnections","ThermostatSetpoint:SingleHeating","ThermostatSetpoint:SingleCooling","ThermostatSetpoint:DualSetpoint","ZoneControl:Thermostat","AirLoopHVAC","AirLoopHVAC:ZoneSplitter","AirLoopHVAC:ZoneMixer","Fan:ConstantVolume","Fan:VariableVolume","Fan:OnOff","Coil:Heating:Electric","Coil:Heating:Fuel","Coil:Heating:Water","Coil:Cooling:DX:SingleSpeed","Coil:Cooling:DX:TwoSpeed","Coil:Cooling:Water","Controller:OutdoorAir","AirLoopHVAC:OutdoorAirSystem","OutdoorAir:Mixer","SetpointManager:Scheduled","SetpointManager:SingleZone:Reheat","PlantLoop","Pump:ConstantSpeed","Pump:VariableSpeed","Boiler:HotWater","Chiller:Electric:EIR","CoolingTower:SingleSpeed","Output:Variable","Output:Meter","Output:Table:Monthly","Output:Table:SummaryReports","OutputControl:Table:Style"],keywords:["Yes","No","On","Off","True","False","autocalculate","autosize","Continuous","Discrete","Any Number","Hourly","Timestep","Daily","Monthly","RunPeriod","Annual","SummerDesignDay","WinterDesignDay","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Holiday","CustomDay1","CustomDay2","AllDays","Weekdays","Weekends","AllOtherDays"],operators:[",",";"],digits:/\d+/,floatDigits:/\d*\.\d+([eE][+-]?\d+)?/,tokenizer:{root:[{include:"@whitespace"},[/!-.*$/,"comment.doc"],[/!.*$/,"comment"],[/^([A-Za-z][A-Za-z0-9:_-]*)\s*(,)/,[{cases:{"@classes":"type.identifier","@default":"type"}},"delimiter"]],{include:"@fieldValue"},[/[,]/,"delimiter"],[/[;]/,"delimiter.semicolon"]],whitespace:[[/[ \t\r\n]+/,"white"]],fieldValue:[[/-?\d*\.\d+([eE][+-]?\d+)?/,"number.float"],[/-?\d+([eE][+-]?\d+)?/,"number"],[/[A-Za-z][A-Za-z0-9_-]*/,{cases:{"@keywords":"keyword","@default":"string"}}],[/\*/,"constant"],[/Through:\s*\d+\/\d+/,"string.date"],[/For:\s*[A-Za-z,\s]+/,"string.date"],[/Until:\s*\d+:\d+/,"string.date"],[/Interpolate:\s*[A-Za-z]+/,"string.date"]]}};function $(e,o){return e.languages.registerHoverProvider(m,{provideHover(n,t){const r=o();if(!r)return null;const i=R(n,t);if(!i)return null;const s=r.objectTypes[i.className.toLowerCase()];if(!s)return null;if(i.isClassName)return B(s,t);if(i.fieldIndex!==void 0&&i.fieldIndex=1;n--){const t=e.getLineContent(n),r=t.match(/^([A-Za-z][A-Za-z0-9:_-]*)\s*,/);if(r&&r[1])return{className:r[1],startLine:n};if(t.includes(";")&&n0&&t--}return t}function B(e,o){const n=[];n.push({value:`**${e.name}**`}),e.group&&n.push({value:`*Group: ${e.group}*`}),e.memo&&n.push({value:e.memo});const t=[];return e.isUnique&&t.push("unique-object"),e.isRequired&&t.push("required-object"),e.minFields>0&&t.push(`min-fields: ${String(e.minFields)}`),e.extensible>0&&t.push(`extensible: ${String(e.extensible)}`),t.length>0&&n.push({value:`\`${t.join(" | ")}\``}),{contents:n,range:{startLineNumber:o.lineNumber,startColumn:1,endLineNumber:o.lineNumber,endColumn:e.name.length+1}}}function V(e,o){const n=[];n.push({value:`**${e.name||e.id}** (${o.name})`});let t=`Type: \`${e.type}\``;if(e.units&&(t+=` | Units: \`${e.units}\``),n.push({value:t}),e.memo&&n.push({value:e.memo}),e.minimum!==void 0||e.maximum!==void 0){let i="Range: ";e.minimum!==void 0&&(i+=e.exclusiveMinimum?`> ${String(e.minimum)}`:`>= ${String(e.minimum)}`),e.minimum!==void 0&&e.maximum!==void 0&&(i+=" and "),e.maximum!==void 0&&(i+=e.exclusiveMaximum?`< ${String(e.maximum)}`:`<= ${String(e.maximum)}`),n.push({value:i})}if(e.default&&n.push({value:`Default: \`${e.default}\``}),e.choices&&e.choices.length>0)if(e.choices.length<=5)n.push({value:`Choices: ${e.choices.map(i=>`\`${i}\``).join(", ")}`});else{const i=e.choices.map(s=>`- \`${s}\``).join(` +`);n.push({value:`Choices (${String(e.choices.length)}): +${i}`})}const r=[];return e.required&&r.push("required"),e.autosizable&&r.push("autosizable"),e.autocalculatable&&r.push("autocalculatable"),r.length>0&&n.push({value:`\`${r.join(" | ")}\``}),{contents:n}}let a=null,u=null;function Z(){return a}async function _(){return a||u||(u=(async()=>{try{const e=q(),o=await fetch(e);return o.ok?(a=await o.json(),console.debug(`[idf-editor] IDD schema loaded: ${a.version}`),a):(console.debug(`[idf-editor] IDD schema not available (${o.status}), hover docs disabled`),null)}catch(e){return console.debug("[idf-editor] Failed to load IDD schema:",e),null}finally{u=null}})(),u)}function q(){const e=document.querySelector('script[src*="idf-editor"]');if(e){const o=e.src;return new URL("idd-schema.json",o).href}return new URL("/assets/idd-schema.json",window.location.origin).href}let y=!1,d=null,p=!1;function G(e){y||(e.languages.register({id:m,extensions:[".idf",".imf"],aliases:["IDF","EnergyPlus IDF","Input Data File"],mimetypes:["text/x-idf"]}),e.languages.setLanguageConfiguration(m,T),e.languages.setMonarchTokensProvider(m,z),D(e),$(e,Z),y=!0)}async function C(){if(!p){p=!0;try{if(d&&(d.dispose(),d=null),window.innerWidth<768)return;const e=document.querySelectorAll("div.language-idf pre > code");if(e.length===0)return;const o=await F();G(o),_(),d=new I(o),d.initialize(e)}catch(e){console.error("[idf-editor] Failed to initialize:",e)}finally{p=!1}}}function w(){const o=window.document$;if(!o)return!1;let n=!0;return o.subscribe(()=>{if(n){n=!1;return}requestAnimationFrame(()=>C())}),!0}if(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>C()):C(),!w()){let e=0;const o=setInterval(()=>{e++,(w()||e>50)&&clearInterval(o)},100)}})(); From 6f976061930eff33fd43370d14cd09dd65bff1af Mon Sep 17 00:00:00 2001 From: Samuel Letellier-Duchesne Date: Fri, 27 Feb 2026 21:50:18 -0500 Subject: [PATCH 4/4] Skip editor re-init when DOM was not replaced (same-page TOC clicks) Zensical may or may not replace the DOM when clicking a same-page anchor (TOC link). When it does not replace the DOM, calling initPage() would dispose working editors and fail to recreate them since the original code blocks no longer exist (replaced by editor containers). Add isStillInDOM() check: if any tracked element is still in the live document, skip re-initialization to preserve working editors. Co-Authored-By: Claude Opus 4.6 --- idf-editor/src/editor-manager.ts | 15 +++++++++++++++ idf-editor/src/main.ts | 9 ++++++++- scripts/assets/idf-editor.js | 8 ++++---- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/idf-editor/src/editor-manager.ts b/idf-editor/src/editor-manager.ts index 9124856d7..7df40a287 100644 --- a/idf-editor/src/editor-manager.ts +++ b/idf-editor/src/editor-manager.ts @@ -87,6 +87,21 @@ export class EditorManager { this.watchThemeChanges(); } + /** + * Check whether any tracked element (editor container or pending block) + * is still attached to the live document. Returns false when Zensical's + * instant navigation has replaced the page content. + */ + isStillInDOM(): boolean { + for (const { container } of this.editors) { + if (document.contains(container)) return true; + } + for (const [element] of this.pendingBlocks) { + if (document.contains(element)) return true; + } + return false; + } + /** Dispose all editors and observers */ dispose(): void { // Dispose editors diff --git a/idf-editor/src/main.ts b/idf-editor/src/main.ts index e540915d7..c210da8b0 100644 --- a/idf-editor/src/main.ts +++ b/idf-editor/src/main.ts @@ -118,7 +118,14 @@ function hookInstantNav(): boolean { } // Small delay to ensure the DOM is updated - requestAnimationFrame(() => initPage()); + requestAnimationFrame(() => { + // If our editors/pending blocks are still in the live DOM, Zensical + // did NOT replace the content (e.g. same-page anchor scroll). + // Re-initializing would destroy the working editors for nothing. + if (currentManager && currentManager.isStillInDOM()) return; + + initPage(); + }); }); return true; diff --git a/scripts/assets/idf-editor.js b/scripts/assets/idf-editor.js index 6b885a81f..cb5e78654 100644 --- a/scripts/assets/idf-editor.js +++ b/scripts/assets/idf-editor.js @@ -1,4 +1,4 @@ -(function(){"use strict";const A=[{token:"comment",foreground:"6A9955"},{token:"comment.doc",foreground:"6A9955",fontStyle:"italic"},{token:"type",foreground:"4EC9B0"},{token:"type.identifier",foreground:"4EC9B0",fontStyle:"bold"},{token:"keyword",foreground:"569CD6"},{token:"number",foreground:"B5CEA8"},{token:"number.float",foreground:"B5CEA8"},{token:"string",foreground:"CE9178"},{token:"string.date",foreground:"DCDCAA"},{token:"delimiter",foreground:"D4D4D4"},{token:"delimiter.semicolon",foreground:"D4D4D4",fontStyle:"bold"},{token:"constant",foreground:"4FC1FF"}],L=[{token:"comment",foreground:"008000"},{token:"comment.doc",foreground:"008000",fontStyle:"italic"},{token:"type",foreground:"267F99"},{token:"type.identifier",foreground:"267F99",fontStyle:"bold"},{token:"keyword",foreground:"0000FF"},{token:"number",foreground:"098658"},{token:"number.float",foreground:"098658"},{token:"string",foreground:"A31515"},{token:"string.date",foreground:"795E26"},{token:"delimiter",foreground:"000000"},{token:"delimiter.semicolon",foreground:"000000",fontStyle:"bold"},{token:"constant",foreground:"0070C1"}],f="idf-docs-light",g="idf-docs-dark";function D(e){e.editor.defineTheme(g,{base:"vs-dark",inherit:!0,rules:A,colors:{"editor.background":"#212121","editor.foreground":"#e2e8f0","editor.lineHighlightBackground":"#2d2d2d","editor.selectionBackground":"#264f78","editorLineNumber.foreground":"#6b7280","editorCursor.foreground":"#e2e8f0","editorWidget.background":"#2d2d2d","editorWidget.border":"#404040","editorHoverWidget.background":"#2d2d2d","editorHoverWidget.border":"#404040"}}),e.editor.defineTheme(f,{base:"vs",inherit:!0,rules:L,colors:{"editor.background":"#f5f5f5","editor.foreground":"#1a202c","editor.lineHighlightBackground":"#f0f0f0","editor.selectionBackground":"#c8e1ff","editorLineNumber.foreground":"#a0aec0","editorCursor.foreground":"#1a202c","editorWidget.background":"#ffffff","editorWidget.border":"#e2e8f0","editorHoverWidget.background":"#ffffff","editorHoverWidget.border":"#e2e8f0"}})}function E(){return document.body.getAttribute("data-md-color-scheme")==="slate"?g:f}const M=60,N=720,O=19,H=20,x="200px",v="https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs",h='',b='';class I{constructor(o){this.editors=[],this.observer=null,this.themeObserver=null,this.pendingBlocks=new Map,this.monaco=o}initialize(o){o.length!==0&&(this.observer=new IntersectionObserver(n=>this.handleIntersection(n),{rootMargin:x}),o.forEach(n=>{const t=n.parentElement;if(!t||t.tagName!=="PRE")return;const r=t.parentElement;if(!r||r.dataset.idfEditor==="true")return;const i=n.textContent||"";this.pendingBlocks.set(r,{wrapper:r,code:i}),this.observer.observe(r)}),this.watchThemeChanges())}dispose(){var o,n;for(const{editor:t}of this.editors)t.dispose();this.editors=[],(o=this.observer)==null||o.disconnect(),this.observer=null,(n=this.themeObserver)==null||n.disconnect(),this.themeObserver=null,this.pendingBlocks.clear()}handleIntersection(o){var n;for(const t of o){if(!t.isIntersecting)continue;const r=this.pendingBlocks.get(t.target);r&&((n=this.observer)==null||n.unobserve(t.target),this.pendingBlocks.delete(t.target),this.createEditor(r.wrapper,r.code))}}createEditor(o,n){var l;const t=n.split(` -`).length,r=Math.max(M,Math.min(N,t*O+H)),i=document.createElement("div");i.className="idf-editor-container",i.style.height=`${r}px`,(l=o.parentNode)==null||l.replaceChild(i,o),o.dataset.idfEditor="true";const s=this.monaco.editor.create(i,{value:n,language:"idf",theme:E(),readOnly:!0,domReadOnly:!0,minimap:{enabled:!1},lineNumbers:"on",scrollBeyondLastLine:!1,wordWrap:"off",folding:!1,glyphMargin:!1,lineDecorationsWidth:8,lineNumbersMinChars:3,renderLineHighlight:"none",overviewRulerLanes:0,hideCursorInOverviewRuler:!0,overviewRulerBorder:!1,scrollbar:{vertical:"auto",horizontal:"auto",verticalScrollbarSize:8,horizontalScrollbarSize:8},fontSize:13,fontFamily:"'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, monospace",padding:{top:8,bottom:8},automaticLayout:!0,contextmenu:!1,links:!1,renderValidationDecorations:"off",fixedOverflowWidgets:!0,accessibilitySupport:"off",ariaLabel:"EnergyPlus IDF code example"});this.addCopyButton(i,s),this.editors.push({editor:s,container:i})}addCopyButton(o,n){const t=document.createElement("button");t.className="idf-editor-copy",t.title="Copy to clipboard",t.innerHTML=h,t.addEventListener("click",async()=>{try{const r=n.getValue();await navigator.clipboard.writeText(r),t.innerHTML=b,t.classList.add("copied"),setTimeout(()=>{t.innerHTML=h,t.classList.remove("copied")},2e3)}catch{const r=document.createElement("textarea");r.value=n.getValue(),r.style.position="fixed",r.style.opacity="0",document.body.appendChild(r),r.select(),document.execCommand("copy"),document.body.removeChild(r),t.innerHTML=b,t.classList.add("copied"),setTimeout(()=>{t.innerHTML=h,t.classList.remove("copied")},2e3)}}),o.appendChild(t)}watchThemeChanges(){this.themeObserver=new MutationObserver(o=>{for(const n of o)if(n.attributeName==="data-md-color-scheme"){const r=document.body.getAttribute("data-md-color-scheme")==="slate"?g:f;this.monaco.editor.setTheme(r)}}),this.themeObserver.observe(document.body,{attributes:!0,attributeFilter:["data-md-color-scheme"]})}}let c=null;function F(){return c||(c=new Promise((e,o)=>{const n=window;if(n.monaco){e(n.monaco);return}if(typeof n.require=="function"&&n.require.config){S(e,o);return}const t=document.createElement("script");t.src=`${v}/loader.js`,t.onload=()=>S(e,o),t.onerror=()=>o(new Error("Failed to load Monaco AMD loader")),document.head.appendChild(t)}),c)}function S(e,o){const t=window.require;t.config({paths:{vs:v}}),t(["vs/editor/editor.main"],r=>{e(r)},r=>{o(r)})}const m="idf",T={comments:{lineComment:"!"},brackets:[],autoClosingPairs:[],surroundingPairs:[],folding:{markers:{start:/^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i,end:/^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i}},wordPattern:/[A-Za-z][A-Za-z0-9:_-]*/},z={defaultToken:"invalid",tokenPostfix:".idf",ignoreCase:!0,classes:["Version","SimulationControl","Building","Timestep","RunPeriod","Site:Location","SizingPeriod:DesignDay","GlobalGeometryRules","Zone","ZoneList","BuildingSurface:Detailed","FenestrationSurface:Detailed","Wall:Exterior","Wall:Interior","Roof","Floor:GroundContact","Window","Door","Material","Material:NoMass","Material:AirGap","WindowMaterial:SimpleGlazingSystem","WindowMaterial:Glazing","Construction","Schedule:Compact","Schedule:Constant","Schedule:Day:Interval","Schedule:Week:Daily","Schedule:Year","ScheduleTypeLimits","People","Lights","ElectricEquipment","ZoneInfiltration:DesignFlowRate","ZoneVentilation:DesignFlowRate","Sizing:Zone","Sizing:System","Sizing:Plant","ZoneHVAC:IdealLoadsAirSystem","ZoneHVAC:EquipmentList","ZoneHVAC:EquipmentConnections","ThermostatSetpoint:SingleHeating","ThermostatSetpoint:SingleCooling","ThermostatSetpoint:DualSetpoint","ZoneControl:Thermostat","AirLoopHVAC","AirLoopHVAC:ZoneSplitter","AirLoopHVAC:ZoneMixer","Fan:ConstantVolume","Fan:VariableVolume","Fan:OnOff","Coil:Heating:Electric","Coil:Heating:Fuel","Coil:Heating:Water","Coil:Cooling:DX:SingleSpeed","Coil:Cooling:DX:TwoSpeed","Coil:Cooling:Water","Controller:OutdoorAir","AirLoopHVAC:OutdoorAirSystem","OutdoorAir:Mixer","SetpointManager:Scheduled","SetpointManager:SingleZone:Reheat","PlantLoop","Pump:ConstantSpeed","Pump:VariableSpeed","Boiler:HotWater","Chiller:Electric:EIR","CoolingTower:SingleSpeed","Output:Variable","Output:Meter","Output:Table:Monthly","Output:Table:SummaryReports","OutputControl:Table:Style"],keywords:["Yes","No","On","Off","True","False","autocalculate","autosize","Continuous","Discrete","Any Number","Hourly","Timestep","Daily","Monthly","RunPeriod","Annual","SummerDesignDay","WinterDesignDay","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Holiday","CustomDay1","CustomDay2","AllDays","Weekdays","Weekends","AllOtherDays"],operators:[",",";"],digits:/\d+/,floatDigits:/\d*\.\d+([eE][+-]?\d+)?/,tokenizer:{root:[{include:"@whitespace"},[/!-.*$/,"comment.doc"],[/!.*$/,"comment"],[/^([A-Za-z][A-Za-z0-9:_-]*)\s*(,)/,[{cases:{"@classes":"type.identifier","@default":"type"}},"delimiter"]],{include:"@fieldValue"},[/[,]/,"delimiter"],[/[;]/,"delimiter.semicolon"]],whitespace:[[/[ \t\r\n]+/,"white"]],fieldValue:[[/-?\d*\.\d+([eE][+-]?\d+)?/,"number.float"],[/-?\d+([eE][+-]?\d+)?/,"number"],[/[A-Za-z][A-Za-z0-9_-]*/,{cases:{"@keywords":"keyword","@default":"string"}}],[/\*/,"constant"],[/Through:\s*\d+\/\d+/,"string.date"],[/For:\s*[A-Za-z,\s]+/,"string.date"],[/Until:\s*\d+:\d+/,"string.date"],[/Interpolate:\s*[A-Za-z]+/,"string.date"]]}};function $(e,o){return e.languages.registerHoverProvider(m,{provideHover(n,t){const r=o();if(!r)return null;const i=R(n,t);if(!i)return null;const s=r.objectTypes[i.className.toLowerCase()];if(!s)return null;if(i.isClassName)return B(s,t);if(i.fieldIndex!==void 0&&i.fieldIndex=1;n--){const t=e.getLineContent(n),r=t.match(/^([A-Za-z][A-Za-z0-9:_-]*)\s*,/);if(r&&r[1])return{className:r[1],startLine:n};if(t.includes(";")&&n0&&t--}return t}function B(e,o){const n=[];n.push({value:`**${e.name}**`}),e.group&&n.push({value:`*Group: ${e.group}*`}),e.memo&&n.push({value:e.memo});const t=[];return e.isUnique&&t.push("unique-object"),e.isRequired&&t.push("required-object"),e.minFields>0&&t.push(`min-fields: ${String(e.minFields)}`),e.extensible>0&&t.push(`extensible: ${String(e.extensible)}`),t.length>0&&n.push({value:`\`${t.join(" | ")}\``}),{contents:n,range:{startLineNumber:o.lineNumber,startColumn:1,endLineNumber:o.lineNumber,endColumn:e.name.length+1}}}function V(e,o){const n=[];n.push({value:`**${e.name||e.id}** (${o.name})`});let t=`Type: \`${e.type}\``;if(e.units&&(t+=` | Units: \`${e.units}\``),n.push({value:t}),e.memo&&n.push({value:e.memo}),e.minimum!==void 0||e.maximum!==void 0){let i="Range: ";e.minimum!==void 0&&(i+=e.exclusiveMinimum?`> ${String(e.minimum)}`:`>= ${String(e.minimum)}`),e.minimum!==void 0&&e.maximum!==void 0&&(i+=" and "),e.maximum!==void 0&&(i+=e.exclusiveMaximum?`< ${String(e.maximum)}`:`<= ${String(e.maximum)}`),n.push({value:i})}if(e.default&&n.push({value:`Default: \`${e.default}\``}),e.choices&&e.choices.length>0)if(e.choices.length<=5)n.push({value:`Choices: ${e.choices.map(i=>`\`${i}\``).join(", ")}`});else{const i=e.choices.map(s=>`- \`${s}\``).join(` -`);n.push({value:`Choices (${String(e.choices.length)}): -${i}`})}const r=[];return e.required&&r.push("required"),e.autosizable&&r.push("autosizable"),e.autocalculatable&&r.push("autocalculatable"),r.length>0&&n.push({value:`\`${r.join(" | ")}\``}),{contents:n}}let a=null,u=null;function Z(){return a}async function _(){return a||u||(u=(async()=>{try{const e=q(),o=await fetch(e);return o.ok?(a=await o.json(),console.debug(`[idf-editor] IDD schema loaded: ${a.version}`),a):(console.debug(`[idf-editor] IDD schema not available (${o.status}), hover docs disabled`),null)}catch(e){return console.debug("[idf-editor] Failed to load IDD schema:",e),null}finally{u=null}})(),u)}function q(){const e=document.querySelector('script[src*="idf-editor"]');if(e){const o=e.src;return new URL("idd-schema.json",o).href}return new URL("/assets/idd-schema.json",window.location.origin).href}let y=!1,d=null,p=!1;function G(e){y||(e.languages.register({id:m,extensions:[".idf",".imf"],aliases:["IDF","EnergyPlus IDF","Input Data File"],mimetypes:["text/x-idf"]}),e.languages.setLanguageConfiguration(m,T),e.languages.setMonarchTokensProvider(m,z),D(e),$(e,Z),y=!0)}async function C(){if(!p){p=!0;try{if(d&&(d.dispose(),d=null),window.innerWidth<768)return;const e=document.querySelectorAll("div.language-idf pre > code");if(e.length===0)return;const o=await F();G(o),_(),d=new I(o),d.initialize(e)}catch(e){console.error("[idf-editor] Failed to initialize:",e)}finally{p=!1}}}function w(){const o=window.document$;if(!o)return!1;let n=!0;return o.subscribe(()=>{if(n){n=!1;return}requestAnimationFrame(()=>C())}),!0}if(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>C()):C(),!w()){let e=0;const o=setInterval(()=>{e++,(w()||e>50)&&clearInterval(o)},100)}})(); +(function(){"use strict";const A=[{token:"comment",foreground:"6A9955"},{token:"comment.doc",foreground:"6A9955",fontStyle:"italic"},{token:"type",foreground:"4EC9B0"},{token:"type.identifier",foreground:"4EC9B0",fontStyle:"bold"},{token:"keyword",foreground:"569CD6"},{token:"number",foreground:"B5CEA8"},{token:"number.float",foreground:"B5CEA8"},{token:"string",foreground:"CE9178"},{token:"string.date",foreground:"DCDCAA"},{token:"delimiter",foreground:"D4D4D4"},{token:"delimiter.semicolon",foreground:"D4D4D4",fontStyle:"bold"},{token:"constant",foreground:"4FC1FF"}],D=[{token:"comment",foreground:"008000"},{token:"comment.doc",foreground:"008000",fontStyle:"italic"},{token:"type",foreground:"267F99"},{token:"type.identifier",foreground:"267F99",fontStyle:"bold"},{token:"keyword",foreground:"0000FF"},{token:"number",foreground:"098658"},{token:"number.float",foreground:"098658"},{token:"string",foreground:"A31515"},{token:"string.date",foreground:"795E26"},{token:"delimiter",foreground:"000000"},{token:"delimiter.semicolon",foreground:"000000",fontStyle:"bold"},{token:"constant",foreground:"0070C1"}],f="idf-docs-light",g="idf-docs-dark";function L(e){e.editor.defineTheme(g,{base:"vs-dark",inherit:!0,rules:A,colors:{"editor.background":"#212121","editor.foreground":"#e2e8f0","editor.lineHighlightBackground":"#2d2d2d","editor.selectionBackground":"#264f78","editorLineNumber.foreground":"#6b7280","editorCursor.foreground":"#e2e8f0","editorWidget.background":"#2d2d2d","editorWidget.border":"#404040","editorHoverWidget.background":"#2d2d2d","editorHoverWidget.border":"#404040"}}),e.editor.defineTheme(f,{base:"vs",inherit:!0,rules:D,colors:{"editor.background":"#f5f5f5","editor.foreground":"#1a202c","editor.lineHighlightBackground":"#f0f0f0","editor.selectionBackground":"#c8e1ff","editorLineNumber.foreground":"#a0aec0","editorCursor.foreground":"#1a202c","editorWidget.background":"#ffffff","editorWidget.border":"#e2e8f0","editorHoverWidget.background":"#ffffff","editorHoverWidget.border":"#e2e8f0"}})}function E(){return document.body.getAttribute("data-md-color-scheme")==="slate"?g:f}const M=60,N=720,O=19,I=20,H="200px",v="https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs",h='',b='';class x{constructor(n){this.editors=[],this.observer=null,this.themeObserver=null,this.pendingBlocks=new Map,this.monaco=n}initialize(n){n.length!==0&&(this.observer=new IntersectionObserver(o=>this.handleIntersection(o),{rootMargin:H}),n.forEach(o=>{const t=o.parentElement;if(!t||t.tagName!=="PRE")return;const r=t.parentElement;if(!r||r.dataset.idfEditor==="true")return;const i=o.textContent||"";this.pendingBlocks.set(r,{wrapper:r,code:i}),this.observer.observe(r)}),this.watchThemeChanges())}isStillInDOM(){for(const{container:n}of this.editors)if(document.contains(n))return!0;for(const[n]of this.pendingBlocks)if(document.contains(n))return!0;return!1}dispose(){var n,o;for(const{editor:t}of this.editors)t.dispose();this.editors=[],(n=this.observer)==null||n.disconnect(),this.observer=null,(o=this.themeObserver)==null||o.disconnect(),this.themeObserver=null,this.pendingBlocks.clear()}handleIntersection(n){var o;for(const t of n){if(!t.isIntersecting)continue;const r=this.pendingBlocks.get(t.target);r&&((o=this.observer)==null||o.unobserve(t.target),this.pendingBlocks.delete(t.target),this.createEditor(r.wrapper,r.code))}}createEditor(n,o){var u;const t=o.split(` +`).length,r=Math.max(M,Math.min(N,t*O+I)),i=document.createElement("div");i.className="idf-editor-container",i.style.height=`${r}px`,(u=n.parentNode)==null||u.replaceChild(i,n),n.dataset.idfEditor="true";const s=this.monaco.editor.create(i,{value:o,language:"idf",theme:E(),readOnly:!0,domReadOnly:!0,minimap:{enabled:!1},lineNumbers:"on",scrollBeyondLastLine:!1,wordWrap:"off",folding:!1,glyphMargin:!1,lineDecorationsWidth:8,lineNumbersMinChars:3,renderLineHighlight:"none",overviewRulerLanes:0,hideCursorInOverviewRuler:!0,overviewRulerBorder:!1,scrollbar:{vertical:"auto",horizontal:"auto",verticalScrollbarSize:8,horizontalScrollbarSize:8},fontSize:13,fontFamily:"'JetBrains Mono', 'Fira Code', ui-monospace, SFMono-Regular, monospace",padding:{top:8,bottom:8},automaticLayout:!0,contextmenu:!1,links:!1,renderValidationDecorations:"off",fixedOverflowWidgets:!0,accessibilitySupport:"off",ariaLabel:"EnergyPlus IDF code example"});this.addCopyButton(i,s),this.editors.push({editor:s,container:i})}addCopyButton(n,o){const t=document.createElement("button");t.className="idf-editor-copy",t.title="Copy to clipboard",t.innerHTML=h,t.addEventListener("click",async()=>{try{const r=o.getValue();await navigator.clipboard.writeText(r),t.innerHTML=b,t.classList.add("copied"),setTimeout(()=>{t.innerHTML=h,t.classList.remove("copied")},2e3)}catch{const r=document.createElement("textarea");r.value=o.getValue(),r.style.position="fixed",r.style.opacity="0",document.body.appendChild(r),r.select(),document.execCommand("copy"),document.body.removeChild(r),t.innerHTML=b,t.classList.add("copied"),setTimeout(()=>{t.innerHTML=h,t.classList.remove("copied")},2e3)}}),n.appendChild(t)}watchThemeChanges(){this.themeObserver=new MutationObserver(n=>{for(const o of n)if(o.attributeName==="data-md-color-scheme"){const r=document.body.getAttribute("data-md-color-scheme")==="slate"?g:f;this.monaco.editor.setTheme(r)}}),this.themeObserver.observe(document.body,{attributes:!0,attributeFilter:["data-md-color-scheme"]})}}let c=null;function F(){return c||(c=new Promise((e,n)=>{const o=window;if(o.monaco){e(o.monaco);return}if(typeof o.require=="function"&&o.require.config){S(e,n);return}const t=document.createElement("script");t.src=`${v}/loader.js`,t.onload=()=>S(e,n),t.onerror=()=>n(new Error("Failed to load Monaco AMD loader")),document.head.appendChild(t)}),c)}function S(e,n){const t=window.require;t.config({paths:{vs:v}}),t(["vs/editor/editor.main"],r=>{e(r)},r=>{n(r)})}const m="idf",T={comments:{lineComment:"!"},brackets:[],autoClosingPairs:[],surroundingPairs:[],folding:{markers:{start:/^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i,end:/^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i}},wordPattern:/[A-Za-z][A-Za-z0-9:_-]*/},z={defaultToken:"invalid",tokenPostfix:".idf",ignoreCase:!0,classes:["Version","SimulationControl","Building","Timestep","RunPeriod","Site:Location","SizingPeriod:DesignDay","GlobalGeometryRules","Zone","ZoneList","BuildingSurface:Detailed","FenestrationSurface:Detailed","Wall:Exterior","Wall:Interior","Roof","Floor:GroundContact","Window","Door","Material","Material:NoMass","Material:AirGap","WindowMaterial:SimpleGlazingSystem","WindowMaterial:Glazing","Construction","Schedule:Compact","Schedule:Constant","Schedule:Day:Interval","Schedule:Week:Daily","Schedule:Year","ScheduleTypeLimits","People","Lights","ElectricEquipment","ZoneInfiltration:DesignFlowRate","ZoneVentilation:DesignFlowRate","Sizing:Zone","Sizing:System","Sizing:Plant","ZoneHVAC:IdealLoadsAirSystem","ZoneHVAC:EquipmentList","ZoneHVAC:EquipmentConnections","ThermostatSetpoint:SingleHeating","ThermostatSetpoint:SingleCooling","ThermostatSetpoint:DualSetpoint","ZoneControl:Thermostat","AirLoopHVAC","AirLoopHVAC:ZoneSplitter","AirLoopHVAC:ZoneMixer","Fan:ConstantVolume","Fan:VariableVolume","Fan:OnOff","Coil:Heating:Electric","Coil:Heating:Fuel","Coil:Heating:Water","Coil:Cooling:DX:SingleSpeed","Coil:Cooling:DX:TwoSpeed","Coil:Cooling:Water","Controller:OutdoorAir","AirLoopHVAC:OutdoorAirSystem","OutdoorAir:Mixer","SetpointManager:Scheduled","SetpointManager:SingleZone:Reheat","PlantLoop","Pump:ConstantSpeed","Pump:VariableSpeed","Boiler:HotWater","Chiller:Electric:EIR","CoolingTower:SingleSpeed","Output:Variable","Output:Meter","Output:Table:Monthly","Output:Table:SummaryReports","OutputControl:Table:Style"],keywords:["Yes","No","On","Off","True","False","autocalculate","autosize","Continuous","Discrete","Any Number","Hourly","Timestep","Daily","Monthly","RunPeriod","Annual","SummerDesignDay","WinterDesignDay","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Holiday","CustomDay1","CustomDay2","AllDays","Weekdays","Weekends","AllOtherDays"],operators:[",",";"],digits:/\d+/,floatDigits:/\d*\.\d+([eE][+-]?\d+)?/,tokenizer:{root:[{include:"@whitespace"},[/!-.*$/,"comment.doc"],[/!.*$/,"comment"],[/^([A-Za-z][A-Za-z0-9:_-]*)\s*(,)/,[{cases:{"@classes":"type.identifier","@default":"type"}},"delimiter"]],{include:"@fieldValue"},[/[,]/,"delimiter"],[/[;]/,"delimiter.semicolon"]],whitespace:[[/[ \t\r\n]+/,"white"]],fieldValue:[[/-?\d*\.\d+([eE][+-]?\d+)?/,"number.float"],[/-?\d+([eE][+-]?\d+)?/,"number"],[/[A-Za-z][A-Za-z0-9_-]*/,{cases:{"@keywords":"keyword","@default":"string"}}],[/\*/,"constant"],[/Through:\s*\d+\/\d+/,"string.date"],[/For:\s*[A-Za-z,\s]+/,"string.date"],[/Until:\s*\d+:\d+/,"string.date"],[/Interpolate:\s*[A-Za-z]+/,"string.date"]]}};function $(e,n){return e.languages.registerHoverProvider(m,{provideHover(o,t){const r=n();if(!r)return null;const i=R(o,t);if(!i)return null;const s=r.objectTypes[i.className.toLowerCase()];if(!s)return null;if(i.isClassName)return W(s,t);if(i.fieldIndex!==void 0&&i.fieldIndex=1;o--){const t=e.getLineContent(o),r=t.match(/^([A-Za-z][A-Za-z0-9:_-]*)\s*,/);if(r&&r[1])return{className:r[1],startLine:o};if(t.includes(";")&&o0&&t--}return t}function W(e,n){const o=[];o.push({value:`**${e.name}**`}),e.group&&o.push({value:`*Group: ${e.group}*`}),e.memo&&o.push({value:e.memo});const t=[];return e.isUnique&&t.push("unique-object"),e.isRequired&&t.push("required-object"),e.minFields>0&&t.push(`min-fields: ${String(e.minFields)}`),e.extensible>0&&t.push(`extensible: ${String(e.extensible)}`),t.length>0&&o.push({value:`\`${t.join(" | ")}\``}),{contents:o,range:{startLineNumber:n.lineNumber,startColumn:1,endLineNumber:n.lineNumber,endColumn:e.name.length+1}}}function V(e,n){const o=[];o.push({value:`**${e.name||e.id}** (${n.name})`});let t=`Type: \`${e.type}\``;if(e.units&&(t+=` | Units: \`${e.units}\``),o.push({value:t}),e.memo&&o.push({value:e.memo}),e.minimum!==void 0||e.maximum!==void 0){let i="Range: ";e.minimum!==void 0&&(i+=e.exclusiveMinimum?`> ${String(e.minimum)}`:`>= ${String(e.minimum)}`),e.minimum!==void 0&&e.maximum!==void 0&&(i+=" and "),e.maximum!==void 0&&(i+=e.exclusiveMaximum?`< ${String(e.maximum)}`:`<= ${String(e.maximum)}`),o.push({value:i})}if(e.default&&o.push({value:`Default: \`${e.default}\``}),e.choices&&e.choices.length>0)if(e.choices.length<=5)o.push({value:`Choices: ${e.choices.map(i=>`\`${i}\``).join(", ")}`});else{const i=e.choices.map(s=>`- \`${s}\``).join(` +`);o.push({value:`Choices (${String(e.choices.length)}): +${i}`})}const r=[];return e.required&&r.push("required"),e.autosizable&&r.push("autosizable"),e.autocalculatable&&r.push("autocalculatable"),r.length>0&&o.push({value:`\`${r.join(" | ")}\``}),{contents:o}}let l=null,d=null;function Z(){return l}async function _(){return l||d||(d=(async()=>{try{const e=q(),n=await fetch(e);return n.ok?(l=await n.json(),console.debug(`[idf-editor] IDD schema loaded: ${l.version}`),l):(console.debug(`[idf-editor] IDD schema not available (${n.status}), hover docs disabled`),null)}catch(e){return console.debug("[idf-editor] Failed to load IDD schema:",e),null}finally{d=null}})(),d)}function q(){const e=document.querySelector('script[src*="idf-editor"]');if(e){const n=e.src;return new URL("idd-schema.json",n).href}return new URL("/assets/idd-schema.json",window.location.origin).href}let y=!1,a=null,p=!1;function G(e){y||(e.languages.register({id:m,extensions:[".idf",".imf"],aliases:["IDF","EnergyPlus IDF","Input Data File"],mimetypes:["text/x-idf"]}),e.languages.setLanguageConfiguration(m,T),e.languages.setMonarchTokensProvider(m,z),L(e),$(e,Z),y=!0)}async function C(){if(!p){p=!0;try{if(a&&(a.dispose(),a=null),window.innerWidth<768)return;const e=document.querySelectorAll("div.language-idf pre > code");if(e.length===0)return;const n=await F();G(n),_(),a=new x(n),a.initialize(e)}catch(e){console.error("[idf-editor] Failed to initialize:",e)}finally{p=!1}}}function w(){const n=window.document$;if(!n)return!1;let o=!0;return n.subscribe(()=>{if(o){o=!1;return}requestAnimationFrame(()=>{a&&a.isStillInDOM()||C()})}),!0}if(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>C()):C(),!w()){let e=0;const n=setInterval(()=>{e++,(w()||e>50)&&clearInterval(n)},100)}})();