diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 000000000..12a6c0a13 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,25 @@ +{ + "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 + } + ] +} diff --git a/.github/actions/setup-python-env/action.yml b/.github/actions/setup-python-env/action.yml index 39a62e96e..31f741a31 100644 --- a/.github/actions/setup-python-env/action.yml +++ b/.github/actions/setup-python-env/action.yml @@ -10,6 +10,10 @@ inputs: description: "uv version to use" required: true default: "0.9" + node-version: + description: "Node.js version to use" + required: true + default: "20" runs: using: "composite" @@ -28,3 +32,9 @@ runs: - name: Install Python dependencies run: uv sync --frozen shell: bash + + - uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + cache: 'npm' + cache-dependency-path: idf-editor/package-lock.json diff --git a/.github/workflows/convert-docs.yml b/.github/workflows/convert-docs.yml index f0883b509..914769382 100644 --- a/.github/workflows/convert-docs.yml +++ b/.github/workflows/convert-docs.yml @@ -75,10 +75,21 @@ jobs: sudo apt-get update sudo apt-get install -y pandoc + - 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,23 +97,17 @@ 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) + - name: Clone EnergyPlus source (sparse, doc/ + idd/) if: steps.cache.outputs.cache-hit != 'true' run: | git clone --filter=blob:none --no-checkout --depth=1 \ --branch ${{ matrix.version }} --single-branch \ https://github.com/NatLabRockies/EnergyPlus.git \ build/sources/${{ matrix.version }} - git -C build/sources/${{ matrix.version }} sparse-checkout set doc + git -C build/sources/${{ matrix.version }} sparse-checkout set doc idd git -C build/sources/${{ matrix.version }} checkout - name: Convert docs 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/Makefile b/Makefile index 654e23f3e..5ac060dfb 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 @@ -51,7 +58,7 @@ convert: ## Convert a single EnergyPlus version (usage: make convert VERSION=v25 git clone --filter=blob:none --no-checkout --depth=1 \ --branch $(VERSION) --single-branch \ https://github.com/NatLabRockies/EnergyPlus.git build/sources/$(VERSION) && \ - git -C build/sources/$(VERSION) sparse-checkout set doc && \ + git -C build/sources/$(VERSION) sparse-checkout set doc idd && \ git -C build/sources/$(VERSION) checkout; \ fi @uv run python -m scripts.convert \ 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..8f1b2e79b --- /dev/null +++ b/idf-editor/src/editor-manager.ts @@ -0,0 +1,290 @@ +/** + * 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'; + +/** Monaco editor version to load from CDN */ +const MONACO_VERSION = '0.55.1'; +const MONACO_CDN_BASE = `https://cdn.jsdelivr.net/npm/monaco-editor@${MONACO_VERSION}/min/vs`; + +/** Copy icon SVG */ +const COPY_ICON = ``; +const CHECK_ICON = ``; + +/** Tracked editor instance */ +interface EditorInstance { + editor: Monaco.editor.IStandaloneCodeEditor; + container: HTMLElement; +} + +export class EditorManager { + private monaco: typeof Monaco; + private editors: EditorInstance[] = []; + private observer: IntersectionObserver | null = null; + private themeObserver: MutationObserver | null = null; + private pendingBlocks: Map = new Map(); + + constructor(monaco: typeof Monaco) { + this.monaco = monaco; + } + + /** + * Initialize the editor manager for the current page. + * Finds all IDF code blocks and sets up lazy loading. + */ + initialize(codeBlocks: NodeListOf): void { + if (codeBlocks.length === 0) return; + + // Set up IntersectionObserver for lazy loading + this.observer = new IntersectionObserver( + (entries) => this.handleIntersection(entries), + { rootMargin: PRELOAD_MARGIN } + ); + + // Process each code block. + // Structure:
......
+ codeBlocks.forEach((codeEl) => { + const pre = codeEl.parentElement; + if (!pre || pre.tagName !== 'PRE') return; + + // The wrapper div has the language-idf class + const wrapper = pre.parentElement; + if (!wrapper) return; + + // Skip if already converted + if (wrapper.dataset.idfEditor === 'true') return; + + const code = codeEl.textContent || ''; + this.pendingBlocks.set(wrapper, { wrapper: wrapper as HTMLElement, code }); + this.observer!.observe(wrapper); + }); + + // Set up theme change listener + this.watchThemeChanges(); + } + + /** Dispose all editors and observers */ + dispose(): void { + // Dispose editors + for (const { editor } of this.editors) { + editor.dispose(); + } + this.editors = []; + + // Disconnect observers + this.observer?.disconnect(); + this.observer = null; + this.themeObserver?.disconnect(); + this.themeObserver = null; + + // Clear pending blocks + this.pendingBlocks.clear(); + } + + /** Handle IntersectionObserver callbacks */ + private handleIntersection(entries: IntersectionObserverEntry[]): void { + for (const entry of entries) { + if (!entry.isIntersecting) continue; + + const block = this.pendingBlocks.get(entry.target); + if (!block) continue; + + // Stop observing this element + this.observer?.unobserve(entry.target); + this.pendingBlocks.delete(entry.target); + + // Create the editor + this.createEditor(block.wrapper, block.code); + } + } + + /** Create a Monaco editor replacing the given wrapper element */ + private createEditor(wrapper: HTMLElement, code: string): void { + // Calculate height based on line count + const lineCount = code.split('\n').length; + const height = Math.max(MIN_HEIGHT, Math.min(MAX_HEIGHT, lineCount * LINE_HEIGHT + PADDING)); + + // Create container + const container = document.createElement('div'); + container.className = 'idf-editor-container'; + container.style.height = `${height}px`; + + // Replace the wrapper
element + wrapper.parentNode?.replaceChild(container, wrapper); + wrapper.dataset.idfEditor = 'true'; + + // Create the editor + const editor = this.monaco.editor.create(container, { + value: code, + language: 'idf', + theme: getCurrentTheme(), + readOnly: true, + domReadOnly: true, + minimap: { enabled: false }, + lineNumbers: 'on', + scrollBeyondLastLine: false, + wordWrap: 'off', + folding: false, + glyphMargin: false, + lineDecorationsWidth: 0, + 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', + // Accessibility for read-only blocks + accessibilitySupport: 'off', + ariaLabel: 'EnergyPlus IDF code example', + }); + + // Add copy button + this.addCopyButton(container, editor); + + // Track the editor + this.editors.push({ editor, container }); + } + + /** Add a copy-to-clipboard button to the editor container */ + private addCopyButton(container: HTMLElement, editor: Monaco.editor.IStandaloneCodeEditor): void { + const btn = document.createElement('button'); + btn.className = 'idf-editor-copy'; + btn.title = 'Copy to clipboard'; + btn.innerHTML = COPY_ICON; + + btn.addEventListener('click', async () => { + try { + const text = editor.getValue(); + await navigator.clipboard.writeText(text); + btn.innerHTML = CHECK_ICON; + btn.classList.add('copied'); + setTimeout(() => { + btn.innerHTML = COPY_ICON; + btn.classList.remove('copied'); + }, 2000); + } catch { + // Fallback for older browsers + const textarea = document.createElement('textarea'); + textarea.value = editor.getValue(); + textarea.style.position = 'fixed'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + btn.innerHTML = CHECK_ICON; + btn.classList.add('copied'); + setTimeout(() => { + btn.innerHTML = COPY_ICON; + btn.classList.remove('copied'); + }, 2000); + } + }); + + container.appendChild(btn); + } + + /** Watch for Zensical theme changes and update all editors */ + private watchThemeChanges(): void { + this.themeObserver = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.attributeName === 'data-md-color-scheme') { + const scheme = document.body.getAttribute('data-md-color-scheme'); + const theme = scheme === 'slate' ? THEME_DARK : THEME_LIGHT; + this.monaco.editor.setTheme(theme); + } + } + }); + + this.themeObserver.observe(document.body, { + attributes: true, + attributeFilter: ['data-md-color-scheme'], + }); + } +} + +/** + * Load Monaco editor from CDN using the AMD loader. + * + * Returns the Monaco editor module. The loader and editor core are + * cached by the browser across page navigations. + */ +export function loadMonacoFromCDN(): Promise { + return new Promise((resolve, reject) => { + // Check if Monaco is already loaded + const win = window as Record; + if (win.monaco) { + resolve(win.monaco as typeof Monaco); + return; + } + + // Check if the AMD loader is already present + if (typeof (win.require as Function) === 'function' && (win.require as Record).config) { + configureAndLoadMonaco(resolve, reject); + return; + } + + // Inject the AMD loader script + const script = document.createElement('script'); + script.src = `${MONACO_CDN_BASE}/loader.js`; + script.onload = () => configureAndLoadMonaco(resolve, reject); + script.onerror = () => reject(new Error('Failed to load Monaco AMD loader')); + document.head.appendChild(script); + }); +} + +function configureAndLoadMonaco( + resolve: (monaco: typeof Monaco) => void, + reject: (error: Error) => void +): void { + const win = window as Record; + const require = win.require as { + config: (opts: Record) => void; + (deps: string[], callback: (monaco: typeof Monaco) => void, errorback?: (err: Error) => void): void; + }; + + require.config({ paths: { vs: MONACO_CDN_BASE } }); + require( + ['vs/editor/editor.main'], + (monaco: typeof Monaco) => { + resolve(monaco); + }, + (err: Error) => { + reject(err); + } + ); +} diff --git a/idf-editor/src/idd-schema-loader.ts b/idf-editor/src/idd-schema-loader.ts new file mode 100644 index 000000000..c2a674192 --- /dev/null +++ b/idf-editor/src/idd-schema-loader.ts @@ -0,0 +1,85 @@ +/** + * IDD Schema Loader + * + * Fetches and caches the compact IDD schema JSON for hover documentation. + * The schema is loaded lazily from assets/idd-schema.json relative to the + * current page. + */ + +import type { CompactIDDSchema } from './types'; + +/** Cached schema instance */ +let cachedSchema: CompactIDDSchema | null = null; + +/** Whether a fetch is in progress */ +let fetchPromise: Promise | null = null; + +/** + * Get the IDD schema, loading it if necessary. + * + * Returns null if the schema hasn't been loaded yet or if loading failed. + * The schema is fetched once per session and cached. + */ +export function getSchema(): CompactIDDSchema | null { + return cachedSchema; +} + +/** + * Load the IDD schema from the assets directory. + * + * The schema JSON is expected at assets/idd-schema.json relative to + * the current page's base URL. If loading fails (e.g., the schema + * doesn't exist for older versions), hover docs simply won't be available. + */ +export async function loadSchema(): Promise { + if (cachedSchema) return cachedSchema; + if (fetchPromise) return fetchPromise; + + fetchPromise = (async () => { + try { + // Resolve the schema URL relative to the current page + const schemaUrl = resolveSchemaUrl(); + const response = await fetch(schemaUrl); + if (!response.ok) { + console.debug(`[idf-editor] IDD schema not available (${response.status}), hover docs disabled`); + return null; + } + cachedSchema = (await response.json()) as CompactIDDSchema; + console.debug(`[idf-editor] IDD schema loaded: ${cachedSchema.version}`); + return cachedSchema; + } catch (error) { + console.debug('[idf-editor] Failed to load IDD schema:', error); + return null; + } finally { + fetchPromise = null; + } + })(); + + return fetchPromise; +} + +/** + * Clear the cached schema (used when navigating between versions). + */ +export function clearSchema(): void { + cachedSchema = null; + fetchPromise = null; +} + +/** + * Resolve the URL for the IDD schema JSON. + * + * The schema lives alongside the idf-editor.js script in the assets/ directory. + * We derive the URL from the script's own src attribute so it works regardless + * of the current page path (deep links, multi-version deployment, etc.). + */ +function resolveSchemaUrl(): string { + // Find our own script tag and resolve relative to it + const script = document.querySelector('script[src*="idf-editor"]'); + if (script) { + const scriptSrc = (script as HTMLScriptElement).src; + return new URL('idd-schema.json', scriptSrc).href; + } + // Fallback: absolute path from site root + return new URL('/assets/idd-schema.json', window.location.origin).href; +} diff --git a/idf-editor/src/idf-editor.css b/idf-editor/src/idf-editor.css new file mode 100644 index 000000000..06b9e5b59 --- /dev/null +++ b/idf-editor/src/idf-editor.css @@ -0,0 +1,75 @@ +/** + * 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;
+}
+
+/* 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..79fdfd185
--- /dev/null
+++ b/idf-editor/src/idf-hover-service.ts
@@ -0,0 +1,250 @@
+/**
+ * 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
+  if (field.choices && field.choices.length > 0) {
+    contents.push({ value: `Choices: ${field.choices.map((c) => `\`${c}\``).join(', ')}` });
+  }
+
+  // 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..893a4344b
--- /dev/null
+++ b/idf-editor/src/main.ts
@@ -0,0 +1,122 @@
+/**
+ * IDF Editor — Main Entry Point
+ *
+ * Scans documentation pages for IDF code blocks and progressively replaces
+ * them with rich Monaco editor instances featuring syntax highlighting,
+ * hover documentation, and code folding.
+ *
+ * Monaco is loaded lazily from CDN only when IDF code blocks are present.
+ */
+
+import './idf-editor.css';
+import { EditorManager, loadMonacoFromCDN } from './editor-manager';
+import { idfLanguageConfiguration, idfTokensProvider, IDF_LANGUAGE_ID } from './idf-language';
+import { registerIDFThemes } from './idf-themes';
+import { registerHoverProvider } from './idf-hover-service';
+import { getSchema, loadSchema } from './idd-schema-loader';
+import type * as Monaco from 'monaco-editor';
+
+/** Whether the IDF language has been registered (once globally) */
+let languageRegistered = false;
+
+/** Current editor manager (recreated on each page navigation) */
+let currentManager: EditorManager | null = null;
+
+/**
+ * Register the IDF language, themes, and providers with Monaco.
+ * Only done once globally (Monaco language registration is persistent).
+ */
+function registerLanguage(monaco: typeof Monaco): void {
+  if (languageRegistered) return;
+
+  // Register language
+  monaco.languages.register({
+    id: IDF_LANGUAGE_ID,
+    extensions: ['.idf', '.imf'],
+    aliases: ['IDF', 'EnergyPlus IDF', 'Input Data File'],
+    mimetypes: ['text/x-idf'],
+  });
+  monaco.languages.setLanguageConfiguration(IDF_LANGUAGE_ID, idfLanguageConfiguration);
+  monaco.languages.setMonarchTokensProvider(IDF_LANGUAGE_ID, idfTokensProvider);
+
+  // Register themes
+  registerIDFThemes(monaco);
+
+  // Register hover provider (schema may not be loaded yet; getSchema returns null until it is)
+  registerHoverProvider(monaco, getSchema);
+
+  languageRegistered = true;
+}
+
+/**
+ * Initialize editors for the current page.
+ * Called on initial load and after each instant navigation.
+ */
+async function initPage(): Promise {
+  // Dispose previous editors (from prior page)
+  if (currentManager) {
+    currentManager.dispose();
+    currentManager = null;
+  }
+
+  // Check if there are IDF code blocks on this page.
+  // Zensical/pymdownx puts the language class on a wrapper 
, not on . + // Structure:
...
+ const codeBlocks = document.querySelectorAll('div.language-idf pre > code'); + if (codeBlocks.length === 0) return; + + try { + // Load Monaco from CDN (cached after first load) + const monaco = await loadMonacoFromCDN(); + + // Register language and providers (once) + registerLanguage(monaco); + + // Start loading IDD schema in the background (for hover docs) + loadSchema(); + + // Create editor manager and initialize editors + currentManager = new EditorManager(monaco); + currentManager.initialize(codeBlocks); + } catch (error) { + console.error('[idf-editor] Failed to initialize:', error); + } +} + +/** + * Hook into Zensical's instant navigation system. + * The document$ observable emits on each page navigation. + */ +function hookInstantNav(): boolean { + const win = window as Record; + const document$ = win.document$ as { subscribe: (fn: () => void) => void } | undefined; + + if (!document$) return false; + + document$.subscribe(() => { + // Small delay to ensure the DOM is updated + requestAnimationFrame(() => initPage()); + }); + + return true; +} + +// --- Bootstrap --- + +// Initialize on page load +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => initPage()); +} else { + initPage(); +} + +// Hook into instant navigation (may not be available immediately) +if (!hookInstantNav()) { + let attempts = 0; + const interval = setInterval(() => { + attempts++; + if (hookInstantNav() || attempts > 50) { + clearInterval(interval); + } + }, 100); +} diff --git a/idf-editor/src/types.ts b/idf-editor/src/types.ts new file mode 100644 index 000000000..bd4a1fd10 --- /dev/null +++ b/idf-editor/src/types.ts @@ -0,0 +1,77 @@ +/** + * Compact IDD Schema Types for Browser + * + * These are simplified versions of the Envelop project's IDD types, + * optimized for the hover documentation use case. They use plain + * objects/Records instead of Maps for JSON serialization. + */ + +/** Field type enumeration */ +export type IDDFieldType = + | 'real' + | 'integer' + | 'alpha' + | 'choice' + | 'object-list' + | 'external-list' + | 'node'; + +/** Compact field definition for hover docs */ +export interface CompactIDDField { + /** Field identifier (A1, A2, N1, N2, etc.) */ + id: string; + /** Field name from \field tag */ + name: string; + /** Data type */ + type: IDDFieldType; + /** Whether this field is required */ + required: boolean; + /** Default value */ + default?: string; + /** Unit specification (e.g., "m", "W", "degC") */ + units?: string; + /** Minimum value */ + minimum?: number; + /** Whether minimum is exclusive */ + exclusiveMinimum?: boolean; + /** Maximum value */ + maximum?: number; + /** Whether maximum is exclusive */ + exclusiveMaximum?: boolean; + /** Valid choices for 'choice' type fields */ + choices?: string[]; + /** Documentation text */ + memo: string; + /** Whether this field can be autosized */ + autosizable: boolean; + /** Whether this field can be autocalculated */ + autocalculatable: boolean; +} + +/** Compact object type definition for hover docs */ +export interface CompactIDDObjectType { + /** Object class name (e.g., "Building", "Zone") */ + name: string; + /** Group this object belongs to */ + group: string; + /** Documentation from \memo tags */ + memo: string; + /** Field definitions */ + fields: CompactIDDField[]; + /** Minimum number of fields required */ + minFields: number; + /** Only one instance allowed */ + isUnique: boolean; + /** Must exist in every model */ + isRequired: boolean; + /** Number of fields in extensible group */ + extensible: number; +} + +/** The complete compact IDD schema */ +export interface CompactIDDSchema { + /** EnergyPlus version */ + version: string; + /** Object types keyed by lowercase name */ + objectTypes: Record; +} diff --git a/idf-editor/tsconfig.json b/idf-editor/tsconfig.json new file mode 100644 index 000000000..039320107 --- /dev/null +++ b/idf-editor/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "declaration": false, + "sourceMap": true + }, + "include": [ + "src" + ] +} diff --git a/idf-editor/vite.config.ts b/idf-editor/vite.config.ts new file mode 100644 index 000000000..782156a81 --- /dev/null +++ b/idf-editor/vite.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vite'; +import path from 'path'; + +export default defineConfig({ + build: { + lib: { + entry: path.resolve(__dirname, 'src/main.ts'), + name: 'IDFEditor', + formats: ['iife'], + fileName: () => 'idf-editor.js', + }, + outDir: path.resolve(__dirname, 'dist'), + cssFileName: 'idf-editor', + minify: 'esbuild', + sourcemap: false, + rollupOptions: { + output: { + assetFileNames: 'idf-editor.[ext]', + }, + }, + }, +}); diff --git a/pyproject.toml b/pyproject.toml index 6a6eb064e..0345ef27a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,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..7b5fc3e55 --- /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}.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..fbe926e35 --- /dev/null +++ b/scripts/assets/idf-editor.js @@ -0,0 +1,2 @@ +(function(){"use strict";const k=[{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"}],w=[{token:"comment",foreground:"008000"},{token:"comment.doc",foreground:"008000",fontStyle:"italic"},{token:"type",foreground:"267F99"},{token:"type.identifier",foreground:"267F99",fontStyle:"bold"},{token:"keyword",foreground:"0000FF"},{token:"number",foreground:"098658"},{token:"number.float",foreground:"098658"},{token:"string",foreground:"A31515"},{token:"string.date",foreground:"795E26"},{token:"delimiter",foreground:"000000"},{token:"delimiter.semicolon",foreground:"000000",fontStyle:"bold"},{token:"constant",foreground:"0070C1"}],m="idf-docs-light",g="idf-docs-dark";function A(e){e.editor.defineTheme(g,{base:"vs-dark",inherit:!0,rules:k,colors:{"editor.background":"#212121","editor.foreground":"#e2e8f0","editor.lineHighlightBackground":"#2d2d2d","editor.selectionBackground":"#264f78","editorLineNumber.foreground":"#6b7280","editorCursor.foreground":"#e2e8f0","editorWidget.background":"#2d2d2d","editorWidget.border":"#404040","editorHoverWidget.background":"#2d2d2d","editorHoverWidget.border":"#404040"}}),e.editor.defineTheme(m,{base:"vs",inherit:!0,rules:w,colors:{"editor.background":"#f5f5f5","editor.foreground":"#1a202c","editor.lineHighlightBackground":"#f0f0f0","editor.selectionBackground":"#c8e1ff","editorLineNumber.foreground":"#a0aec0","editorCursor.foreground":"#1a202c","editorWidget.background":"#ffffff","editorWidget.border":"#e2e8f0","editorHoverWidget.background":"#ffffff","editorHoverWidget.border":"#e2e8f0"}})}function D(){return document.body.getAttribute("data-md-color-scheme")==="slate"?g:m}const L=60,E=720,M=19,N=20,O="200px",p="https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs",f='',C='';class H{constructor(n){this.editors=[],this.observer=null,this.themeObserver=null,this.pendingBlocks=new Map,this.monaco=n}initialize(n){n.length!==0&&(this.observer=new IntersectionObserver(o=>this.handleIntersection(o),{rootMargin:O}),n.forEach(o=>{const t=o.parentElement;if(!t||t.tagName!=="PRE")return;const r=t.parentElement;if(!r||r.dataset.idfEditor==="true")return;const i=o.textContent||"";this.pendingBlocks.set(r,{wrapper:r,code:i}),this.observer.observe(r)}),this.watchThemeChanges())}dispose(){var n,o;for(const{editor:t}of this.editors)t.dispose();this.editors=[],(n=this.observer)==null||n.disconnect(),this.observer=null,(o=this.themeObserver)==null||o.disconnect(),this.themeObserver=null,this.pendingBlocks.clear()}handleIntersection(n){var o;for(const t of n){if(!t.isIntersecting)continue;const r=this.pendingBlocks.get(t.target);r&&((o=this.observer)==null||o.unobserve(t.target),this.pendingBlocks.delete(t.target),this.createEditor(r.wrapper,r.code))}}createEditor(n,o){var l;const t=o.split(` +`).length,r=Math.max(L,Math.min(E,t*M+N)),i=document.createElement("div");i.className="idf-editor-container",i.style.height=`${r}px`,(l=n.parentNode)==null||l.replaceChild(i,n),n.dataset.idfEditor="true";const s=this.monaco.editor.create(i,{value:o,language:"idf",theme:D(),readOnly:!0,domReadOnly:!0,minimap:{enabled:!1},lineNumbers:"on",scrollBeyondLastLine:!1,wordWrap:"off",folding:!1,glyphMargin:!1,lineDecorationsWidth:0,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",accessibilitySupport:"off",ariaLabel:"EnergyPlus IDF code example"});this.addCopyButton(i,s),this.editors.push({editor:s,container:i})}addCopyButton(n,o){const t=document.createElement("button");t.className="idf-editor-copy",t.title="Copy to clipboard",t.innerHTML=f,t.addEventListener("click",async()=>{try{const r=o.getValue();await navigator.clipboard.writeText(r),t.innerHTML=C,t.classList.add("copied"),setTimeout(()=>{t.innerHTML=f,t.classList.remove("copied")},2e3)}catch{const r=document.createElement("textarea");r.value=o.getValue(),r.style.position="fixed",r.style.opacity="0",document.body.appendChild(r),r.select(),document.execCommand("copy"),document.body.removeChild(r),t.innerHTML=C,t.classList.add("copied"),setTimeout(()=>{t.innerHTML=f,t.classList.remove("copied")},2e3)}}),n.appendChild(t)}watchThemeChanges(){this.themeObserver=new MutationObserver(n=>{for(const o of n)if(o.attributeName==="data-md-color-scheme"){const r=document.body.getAttribute("data-md-color-scheme")==="slate"?g:m;this.monaco.editor.setTheme(r)}}),this.themeObserver.observe(document.body,{attributes:!0,attributeFilter:["data-md-color-scheme"]})}}function x(){return new Promise((e,n)=>{const o=window;if(o.monaco){e(o.monaco);return}if(typeof o.require=="function"&&o.require.config){b(e,n);return}const t=document.createElement("script");t.src=`${p}/loader.js`,t.onload=()=>b(e,n),t.onerror=()=>n(new Error("Failed to load Monaco AMD loader")),document.head.appendChild(t)})}function b(e,n){const t=window.require;t.config({paths:{vs:p}}),t(["vs/editor/editor.main"],r=>{e(r)},r=>{n(r)})}const c="idf",I={comments:{lineComment:"!"},brackets:[],autoClosingPairs:[],surroundingPairs:[],folding:{markers:{start:/^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i,end:/^!-\s*={3,}\s*ALL OBJECTS IN CLASS:/i}},wordPattern:/[A-Za-z][A-Za-z0-9:_-]*/},F={defaultToken:"invalid",tokenPostfix:".idf",ignoreCase:!0,classes:["Version","SimulationControl","Building","Timestep","RunPeriod","Site:Location","SizingPeriod:DesignDay","GlobalGeometryRules","Zone","ZoneList","BuildingSurface:Detailed","FenestrationSurface:Detailed","Wall:Exterior","Wall:Interior","Roof","Floor:GroundContact","Window","Door","Material","Material:NoMass","Material:AirGap","WindowMaterial:SimpleGlazingSystem","WindowMaterial:Glazing","Construction","Schedule:Compact","Schedule:Constant","Schedule:Day:Interval","Schedule:Week:Daily","Schedule:Year","ScheduleTypeLimits","People","Lights","ElectricEquipment","ZoneInfiltration:DesignFlowRate","ZoneVentilation:DesignFlowRate","Sizing:Zone","Sizing:System","Sizing:Plant","ZoneHVAC:IdealLoadsAirSystem","ZoneHVAC:EquipmentList","ZoneHVAC:EquipmentConnections","ThermostatSetpoint:SingleHeating","ThermostatSetpoint:SingleCooling","ThermostatSetpoint:DualSetpoint","ZoneControl:Thermostat","AirLoopHVAC","AirLoopHVAC:ZoneSplitter","AirLoopHVAC:ZoneMixer","Fan:ConstantVolume","Fan:VariableVolume","Fan:OnOff","Coil:Heating:Electric","Coil:Heating:Fuel","Coil:Heating:Water","Coil:Cooling:DX:SingleSpeed","Coil:Cooling:DX:TwoSpeed","Coil:Cooling:Water","Controller:OutdoorAir","AirLoopHVAC:OutdoorAirSystem","OutdoorAir:Mixer","SetpointManager:Scheduled","SetpointManager:SingleZone:Reheat","PlantLoop","Pump:ConstantSpeed","Pump:VariableSpeed","Boiler:HotWater","Chiller:Electric:EIR","CoolingTower:SingleSpeed","Output:Variable","Output:Meter","Output:Table:Monthly","Output:Table:SummaryReports","OutputControl:Table:Style"],keywords:["Yes","No","On","Off","True","False","autocalculate","autosize","Continuous","Discrete","Any Number","Hourly","Timestep","Daily","Monthly","RunPeriod","Annual","SummerDesignDay","WinterDesignDay","Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Holiday","CustomDay1","CustomDay2","AllDays","Weekdays","Weekends","AllOtherDays"],operators:[",",";"],digits:/\d+/,floatDigits:/\d*\.\d+([eE][+-]?\d+)?/,tokenizer:{root:[{include:"@whitespace"},[/!-.*$/,"comment.doc"],[/!.*$/,"comment"],[/^([A-Za-z][A-Za-z0-9:_-]*)\s*(,)/,[{cases:{"@classes":"type.identifier","@default":"type"}},"delimiter"]],{include:"@fieldValue"},[/[,]/,"delimiter"],[/[;]/,"delimiter.semicolon"]],whitespace:[[/[ \t\r\n]+/,"white"]],fieldValue:[[/-?\d*\.\d+([eE][+-]?\d+)?/,"number.float"],[/-?\d+([eE][+-]?\d+)?/,"number"],[/[A-Za-z][A-Za-z0-9_-]*/,{cases:{"@keywords":"keyword","@default":"string"}}],[/\*/,"constant"],[/Through:\s*\d+\/\d+/,"string.date"],[/For:\s*[A-Za-z,\s]+/,"string.date"],[/Until:\s*\d+:\d+/,"string.date"],[/Interpolate:\s*[A-Za-z]+/,"string.date"]]}};function T(e,n){return e.languages.registerHoverProvider(c,{provideHover(o,t){const r=n();if(!r)return null;const i=z(o,t);if(!i)return null;const s=r.objectTypes[i.className.toLowerCase()];if(!s)return null;if(i.isClassName)return B(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 B(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 P(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})}e.default&&o.push({value:`Default: \`${e.default}\``}),e.choices&&e.choices.length>0&&o.push({value:`Choices: ${e.choices.map(i=>`\`${i}\``).join(", ")}`});const r=[];return e.required&&r.push("required"),e.autosizable&&r.push("autosizable"),e.autocalculatable&&r.push("autocalculatable"),r.length>0&&o.push({value:`\`${r.join(" | ")}\``}),{contents:o}}let a=null,d=null;function V(){return a}async function W(){return a||d||(d=(async()=>{try{const e=Z(),n=await fetch(e);return n.ok?(a=await n.json(),console.debug(`[idf-editor] IDD schema loaded: ${a.version}`),a):(console.debug(`[idf-editor] IDD schema not available (${n.status}), hover docs disabled`),null)}catch(e){return console.debug("[idf-editor] Failed to load IDD schema:",e),null}finally{d=null}})(),d)}function Z(){const e=document.querySelector('script[src*="idf-editor"]');if(e){const n=e.src;return new URL("idd-schema.json",n).href}return new URL("/assets/idd-schema.json",window.location.origin).href}let v=!1,u=null;function _(e){v||(e.languages.register({id:c,extensions:[".idf",".imf"],aliases:["IDF","EnergyPlus IDF","Input Data File"],mimetypes:["text/x-idf"]}),e.languages.setLanguageConfiguration(c,I),e.languages.setMonarchTokensProvider(c,F),A(e),T(e,V),v=!0)}async function h(){u&&(u.dispose(),u=null);const e=document.querySelectorAll("div.language-idf pre > code");if(e.length!==0)try{const n=await x();_(n),W(),u=new H(n),u.initialize(e)}catch(n){console.error("[idf-editor] Failed to initialize:",n)}}function S(){const n=window.document$;return n?(n.subscribe(()=>{requestAnimationFrame(()=>h())}),!0):!1}if(document.readyState==="loading"?document.addEventListener("DOMContentLoaded",()=>h()):h(),!S()){let e=0;const n=setInterval(()=>{e++,(S()||e>50)&&clearInterval(n)},100)}})(); diff --git a/scripts/convert.py b/scripts/convert.py index 8ca5aa6b0..5edf0cd8f 100644 --- a/scripts/convert.py +++ b/scripts/convert.py @@ -33,6 +33,7 @@ IMAGE_EXTENSIONS, version_to_title, ) +from scripts.idd_schema_builder import build_compact_schema, find_idd_file from scripts.latex_preprocessor import preprocess from scripts.markdown_postprocessor import postprocess from scripts.models import ConversionResult, DocSet, DocSetResult, LabelRef, VersionResult @@ -644,13 +645,14 @@ def generate_zensical_config( # Tags for cmd-k search filtering extra["tags"] = {ds.title: ds.slug for ds in doc_sets} - # MathJax with equation numbering + equation tooltips + # MathJax with equation numbering + equation tooltips + IDF Monaco 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"] + project["extra_css"] = ["assets/eq-tooltips.css", "assets/figures.css", "assets/idf-editor.css"] config_path = output_dir / "zensical.toml" config_path.write_text(tomli_w.dumps(config)) @@ -740,9 +742,20 @@ 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, IDF editor) copy_assets(output_dir) + # Generate IDD schema JSON for Monaco hover documentation + idd_path = find_idd_file(source_dir) + if idd_path: + schema_output = output_dir / "docs" / "assets" / "idd-schema.json" + if build_compact_schema(idd_path, schema_output): + logger.info("IDD schema generated for Monaco hover docs") + else: + logger.warning("Failed to generate IDD schema — hover docs will be unavailable") + else: + logger.warning("No IDD file found in %s — hover docs will be unavailable", source_dir) + # Build site if not skip_build: logger.info("Building Zensical site for %s...", version) diff --git a/scripts/convert_all.py b/scripts/convert_all.py index 9757d65fc..9609a7522 100644 --- a/scripts/convert_all.py +++ b/scripts/convert_all.py @@ -47,7 +47,7 @@ def clone_version(version: str, clone_dir: Path) -> Path: shutil.rmtree(target) - logger.info("Cloning EnergyPlus %s (sparse, doc/ only)...", version) + logger.info("Cloning EnergyPlus %s (sparse, doc/ + idd/)...", version) subprocess.run( [ @@ -66,8 +66,9 @@ def clone_version(version: str, clone_dir: Path) -> Path: capture_output=True, text=True, ) + # Sparse-checkout doc/ (LaTeX source) and idd/ (IDD for Monaco hover docs) subprocess.run( - ["git", "-C", str(target), "sparse-checkout", "set", "doc"], + ["git", "-C", str(target), "sparse-checkout", "set", "doc", "idd"], check=True, capture_output=True, ) diff --git a/scripts/idd_schema_builder.py b/scripts/idd_schema_builder.py new file mode 100644 index 000000000..68e8e4d6b --- /dev/null +++ b/scripts/idd_schema_builder.py @@ -0,0 +1,373 @@ +"""Build a compact IDD schema JSON for the Monaco hover documentation provider. + +Parses the EnergyPlus IDD (Input Data Dictionary) file and produces a compact +JSON file containing object type definitions, field metadata, and documentation +text. This JSON is loaded by the browser-side IDF editor to power hover +tooltips. + +The parser handles the standard IDD format: + + \\group Group Name + + ClassName, + \\memo Description of the object + \\unique-object + \\min-fields 5 + A1 , \\field Field Name + \\required-field + \\type choice + \\key Option1 + \\key Option2 + N1 ; \\field Field Name + \\type real + \\units W + \\minimum 0.0 + \\default 1.0 +""" + +from __future__ import annotations + +import contextlib +import json +import logging +import re +from dataclasses import dataclass, field +from pathlib import Path + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Data models +# --------------------------------------------------------------------------- + +FIELD_TYPE_MAP: dict[str, str] = { + "real": "real", + "integer": "integer", + "alpha": "alpha", + "choice": "choice", + "object-list": "object-list", + "external-list": "external-list", + "node": "node", +} + + +@dataclass +class CompactField: + id: str + name: str = "" + type: str = "alpha" + required: bool = False + default: str | None = None + units: str | None = None + minimum: float | None = None + exclusive_minimum: bool = False + maximum: float | None = None + exclusive_maximum: bool = False + choices: list[str] = field(default_factory=list) + memo: str = "" + autosizable: bool = False + autocalculatable: bool = False + + def to_dict(self) -> dict: + d: dict = {"id": self.id, "name": self.name, "type": self.type, "required": self.required, "memo": self.memo} + if self.default is not None: + d["default"] = self.default + if self.units: + d["units"] = self.units + if self.minimum is not None: + d["minimum"] = self.minimum + if self.exclusive_minimum: + d["exclusiveMinimum"] = True + if self.maximum is not None: + d["maximum"] = self.maximum + if self.exclusive_maximum: + d["exclusiveMaximum"] = True + if self.choices: + d["choices"] = self.choices + d["autosizable"] = self.autosizable + d["autocalculatable"] = self.autocalculatable + return d + + +@dataclass +class CompactObjectType: + name: str + group: str = "" + memo: str = "" + fields: list[CompactField] = field(default_factory=list) + min_fields: int = 0 + is_unique: bool = False + is_required: bool = False + extensible: int = 0 + + def to_dict(self) -> dict: + return { + "name": self.name, + "group": self.group, + "memo": self.memo, + "fields": [f.to_dict() for f in self.fields], + "minFields": self.min_fields, + "isUnique": self.is_unique, + "isRequired": self.is_required, + "extensible": self.extensible, + } + + +# --------------------------------------------------------------------------- +# IDD parser +# --------------------------------------------------------------------------- + +_PROPERTY_RE = re.compile(r"\\(\S+)\s*(.*)") +_FIELD_ID_RE = re.compile(r"^\s*([AN]\d+)\s*[,;]") +_CLASS_NAME_RE = re.compile(r"^[A-Za-z][A-Za-z0-9:_ -]*[,;]$") + + +def _strip_comments(line: str) -> str: + """Remove inline comments and strip trailing whitespace.""" + pos = line.find("!") + return line[:pos].rstrip() if pos >= 0 else line + + +def _is_class_name_line(stripped: str, raw_line: str) -> bool: + """Determine if a line is a class name definition (not a field).""" + if raw_line and raw_line[0] in (" ", "\t"): + return False + return bool(_CLASS_NAME_RE.match(stripped)) + + +def _append_memo(existing: str, addition: str) -> str: + """Append text to an existing memo string.""" + return (existing + " " + addition).strip() if existing else addition + + +def _apply_object_property(obj: CompactObjectType, fld: CompactField | None, name: str, value: str) -> bool: + """Apply an object-level IDD property. Return True if handled.""" + if name == "memo": + if fld is None: + obj.memo = _append_memo(obj.memo, value) + else: + fld.memo = _append_memo(fld.memo, value) + return True + if name == "unique-object": + obj.is_unique = True + return True + if name == "required-object": + obj.is_required = True + return True + if name == "min-fields": + with contextlib.suppress(ValueError): + obj.min_fields = int(value) + return True + if name == "extensible": + with contextlib.suppress(ValueError): + obj.extensible = int(value.split(":")[0]) if ":" in value else int(value) + return True + return False + + +def _set_field_range(fld: CompactField, name: str, value: str) -> None: + """Set a min/max range constraint on a field.""" + with contextlib.suppress(ValueError): + if name.startswith("min"): + fld.minimum = float(value) + fld.exclusive_minimum = name == "minimum>" + else: + fld.maximum = float(value) + fld.exclusive_maximum = name == "maximum<" + + +def _apply_field_property(fld: CompactField, name: str, value: str) -> None: + """Apply a field-level IDD property.""" + # Simple flag properties + _FIELD_FLAGS = {"required-field": "required", "autosizable": "autosizable", "autocalculatable": "autocalculatable"} + if name in _FIELD_FLAGS: + setattr(fld, _FIELD_FLAGS[name], True) + return + + # Range properties + if name in ("minimum", "minimum>", "maximum", "maximum<"): + _set_field_range(fld, name, value) + return + + # Value properties + if name == "note": + fld.memo = _append_memo(fld.memo, value) + elif name == "field": + fld.name = value + elif name == "type": + fld.type = FIELD_TYPE_MAP.get(value.lower(), value.lower()) + elif name == "key": + fld.choices.append(value) + if fld.type == "alpha": + fld.type = "choice" + elif name == "units": + fld.units = value + elif name == "default": + fld.default = value + + +def _handle_property(state: dict, stripped: str) -> None: + """Parse and apply a property line (\\name value).""" + m = _PROPERTY_RE.match(stripped) + if not m: + return + prop_name = m.group(1).lower() + prop_value = m.group(2).strip() + obj, fld = state["obj"], state["fld"] + if (obj is not None and not _apply_object_property(obj, fld, prop_name, prop_value) and fld is not None) or ( + obj is None and fld is not None + ): + _apply_field_property(fld, prop_name, prop_value) + + +def _handle_class_name(state: dict, stripped: str) -> None: + """Start a new object type definition.""" + if state["obj"] is not None: + state["types"][state["obj"].name.lower()] = state["obj"] + class_name = stripped.rstrip(",;").strip() + state["obj"] = CompactObjectType(name=class_name, group=state["group"]) + state["fld"] = None + + +def _handle_field(state: dict, line: str) -> None: + """Add a new field definition to the current object. + + IDD fields can have properties on the same line, e.g.: + A1 , \\field Name of the Zone + We parse the field ID first, then check for inline properties. + """ + field_match = _FIELD_ID_RE.match(line) + if not field_match or state["obj"] is None: + return + field_id = field_match.group(1) + field_type = "alpha" if field_id.startswith("A") else "real" + state["fld"] = CompactField(id=field_id, type=field_type) + state["obj"].fields.append(state["fld"]) + + # Check for inline properties after the comma/semicolon (e.g., \field Name) + remainder = line[field_match.end() :].strip() + if remainder.startswith("\\"): + _handle_property(state, remainder) + + +def _process_line(line: str, raw_line: str, state: dict) -> None: + """Process a single non-empty, non-comment IDD line.""" + stripped = line.strip() + + if stripped.startswith("\\group"): + state["group"] = stripped[7:].strip() + elif stripped.startswith("\\"): + _handle_property(state, stripped) + elif _is_class_name_line(stripped, raw_line): + _handle_class_name(state, stripped) + else: + _handle_field(state, line) + + +def _iter_idd_lines(text: str) -> list[tuple[str, str]]: + """Yield (processed_line, raw_line) tuples, skipping blanks, comments, and header.""" + result = [] + in_header = True + for raw_line in text.splitlines(): + line = raw_line.rstrip() + if not line.strip(): + continue + if in_header: + if line.startswith("!"): + continue + in_header = False + if line.lstrip().startswith("!"): + continue + line = _strip_comments(line) + if line.strip(): + result.append((line, raw_line)) + return result + + +def _extract_version(types: dict[str, CompactObjectType]) -> str: + """Extract the EnergyPlus version from the parsed Version object.""" + if "version" not in types: + return "" + for f in types["version"].fields: + if f.default: + return f.default + return "" + + +def parse_idd(idd_path: Path) -> dict: + """Parse an EnergyPlus IDD file and return a compact schema dict. + + Returns a dict with keys ``version`` and ``objectTypes`` (keyed by + lowercase class name). + """ + text = idd_path.read_text(encoding="utf-8", errors="replace") + state: dict = {"group": "", "obj": None, "fld": None, "types": {}} + + for line, raw_line in _iter_idd_lines(text): + _process_line(line, raw_line, state) + + # Save the last object + if state["obj"] is not None: + state["types"][state["obj"].name.lower()] = state["obj"] + + version = _extract_version(state["types"]) + return {"version": version, "objectTypes": {k: v.to_dict() for k, v in state["types"].items()}} + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def build_compact_schema(idd_path: Path, output_path: Path) -> bool: + """Build a compact IDD schema JSON file from an EnergyPlus IDD file. + + Args: + idd_path: Path to the Energy+.idd file. + output_path: Path where the compact JSON will be written. + + Returns: + True if the schema was built successfully, False otherwise. + """ + if not idd_path.exists(): + logger.warning("IDD file not found: %s", idd_path) + return False + + logger.info("Building compact IDD schema from %s", idd_path) + schema = parse_idd(idd_path) + + obj_count = len(schema.get("objectTypes", {})) + logger.info("Parsed %d object types (version: %s)", obj_count, schema.get("version", "unknown")) + + output_path.parent.mkdir(parents=True, exist_ok=True) + with open(output_path, "w", encoding="utf-8") as f: + json.dump(schema, f, separators=(",", ":")) + + size_kb = output_path.stat().st_size / 1024 + logger.info("Wrote compact schema: %s (%.0f KB)", output_path, size_kb) + return True + + +def find_idd_file(source_dir: Path) -> Path | None: + """Search for the Energy+.idd file in an EnergyPlus source directory. + + Checks several known locations across different EnergyPlus versions. + """ + candidates = [ + source_dir / "idd" / "Energy+.idd", + source_dir / "idd" / "Energy+.idd.in", # CMake template (source builds) + source_dir / "idd" / "V8-9-0-Energy+.idd", # v8.9 format + source_dir / "Energy+.idd", + ] + for candidate in candidates: + if candidate.exists(): + return candidate + + # Glob for any .idd file in idd/ directory + idd_dir = source_dir / "idd" + if idd_dir.exists(): + idd_files = list(idd_dir.glob("*Energy+.idd*")) + if idd_files: + return idd_files[0] + + return None 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/uv.lock b/uv.lock index 55f4a6408..ca9be2d5f 100644 --- a/uv.lock +++ b/uv.lock @@ -71,7 +71,7 @@ wheels = [ [[package]] name = "idfkit-docs" version = "0.0.1" -source = { virtual = "." } +source = { editable = "." } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "tomli-w" },