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/.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/.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..7df40a287 --- /dev/null +++ b/idf-editor/src/editor-manager.ts @@ -0,0 +1,317 @@ +/** + * 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(); + } + + /** + * 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 + 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'], + }); + } +} + +/** 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. The promise is + * cached so concurrent calls share the same load operation. + */ +export function loadMonacoFromCDN(): Promise { + if (monacoLoadPromise) return monacoLoadPromise; + + monacoLoadPromise = 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); + }); + + return monacoLoadPromise; +} + +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..c210da8b0
--- /dev/null
+++ b/idf-editor/src/main.ts
@@ -0,0 +1,152 @@
+/**
+ * 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;
+
+/** 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).
+ */
+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 {
+  // Prevent concurrent execution (e.g. document$ ReplaySubject firing
+  // while the initial initPage() is still loading Monaco from CDN).
+  if (initInProgress) return;
+  initInProgress = true;
+
+  try {
+    // 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; + + // 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); + } finally { + initInProgress = false; + } +} + +/** + * 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$ 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(() => { + // 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; +} + +// --- 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..cb5e78654 --- /dev/null +++ b/scripts/assets/idf-editor.js @@ -0,0 +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"}],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)}})(); 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..90fddd027 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,57 @@ 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 + # 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"}, {"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 +773,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 +809,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..fe87fe827 100644 --- a/zensical.toml +++ b/zensical.toml @@ -19,6 +19,7 @@ features = [ "navigation.footer", "navigation.tracking", "content.code.copy", + "toc.follow", ] [project.theme.icon]