Repository: ivijs/ivi Branch: master Commit: 305896b59e30 Files: 257 Total size: 576.1 KB Directory structure: gitextract_8ywn65uv/ ├── .clippy.toml ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── renovate.json │ └── workflows/ │ ├── ci.yml │ ├── napi-libraries.yml │ └── napi-release.yml ├── .gitignore ├── .rustfmt.toml ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── crates/ │ └── ivi_compiler/ │ ├── Cargo.toml │ └── src/ │ ├── chunk/ │ │ └── mod.rs │ ├── context.rs │ ├── import.rs │ ├── lib.rs │ ├── module/ │ │ └── mod.rs │ ├── oveo.rs │ └── tpl/ │ ├── emit.rs │ ├── html.rs │ ├── mod.rs │ ├── opcodes.rs │ └── parser.rs ├── docs/ │ ├── internals/ │ │ ├── dynamic-lists.md │ │ ├── misc.md │ │ ├── perf.md │ │ └── template-compiler.md │ └── misc/ │ └── migrating-from-react.md ├── justfile ├── napi.just ├── package.json ├── packages/ │ ├── @ivi/ │ │ ├── compiler/ │ │ │ ├── .gitignore │ │ │ ├── Cargo.toml │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── build.rs │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── package.json │ │ │ ├── packages/ │ │ │ │ ├── darwin-arm64/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── package.json │ │ │ │ ├── darwin-x64/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── package.json │ │ │ │ ├── linux-arm64-gnu/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── package.json │ │ │ │ ├── linux-x64-gnu/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── package.json │ │ │ │ ├── win32-arm64-msvc/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── package.json │ │ │ │ └── win32-x64-msvc/ │ │ │ │ ├── README.md │ │ │ │ └── package.json │ │ │ ├── src/ │ │ │ │ └── lib.rs │ │ │ └── tsconfig.json │ │ ├── identity/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.ts │ │ │ └── tsconfig.json │ │ ├── mock-dom/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ ├── global.ts │ │ │ │ └── index.ts │ │ │ └── tsconfig.json │ │ ├── portal/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.ts │ │ │ └── tsconfig.json │ │ ├── rolldown/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.ts │ │ │ └── tsconfig.json │ │ ├── rollup-plugin/ │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.ts │ │ │ └── tsconfig.json │ │ └── vite-plugin/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.json │ └── ivi/ │ ├── LICENSE │ ├── README.md │ ├── oveo.json │ ├── package.json │ ├── src/ │ │ ├── html/ │ │ │ ├── index.ts │ │ │ └── parser.ts │ │ ├── index.ts │ │ ├── lib/ │ │ │ ├── core.ts │ │ │ ├── equal.ts │ │ │ ├── state.ts │ │ │ ├── template.ts │ │ │ └── utils.ts │ │ ├── template/ │ │ │ ├── compiler.ts │ │ │ ├── ir.ts │ │ │ ├── parser.ts │ │ │ └── shared.ts │ │ └── test-utils/ │ │ └── index.ts │ └── tsconfig.json ├── rust-toolchain.toml ├── tests/ │ ├── compiler/ │ │ ├── chunk/ │ │ │ └── strings/ │ │ │ ├── data/ │ │ │ │ ├── 01-basic/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ └── 02-multiple/ │ │ │ │ ├── input.js │ │ │ │ └── output.js │ │ │ └── strings.test.ts │ │ ├── module/ │ │ │ ├── data/ │ │ │ │ ├── 01-text/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 02-element/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 03-element/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 04-nested-text/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 05-nested-expr/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 06-text-before-expr/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 07-text-after-expr/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 09-text-before-and-after-expr/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 103-hoist-return-fn/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 11-multiple-roots/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 12-multiple-roots/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 13-multiple-nested-expr/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 14-svg-element/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 15-svg-template/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 16-nested-mix/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 17-void-element/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 18-expr-before-element/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 19-comment/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 20-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 21-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 22-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 23-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 24-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 25-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 26-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 27-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 28-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 29-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 30-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 31-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 32-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 33-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 34-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 35-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 36-whitespace/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 40-attribute/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 41-attribute/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 42-attribute/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 43-attributes/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 44-attributes/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 50-prop/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 51-prop/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 60-style/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 61-style/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 62-style/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 63-style-mix/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 70-event/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 80-directive/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ ├── 90-hoist-class-identifier/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ └── 91-hoist-class-static-member/ │ │ │ │ ├── input.js │ │ │ │ └── output.js │ │ │ └── module.test.ts │ │ ├── module-all-disabled/ │ │ │ ├── data/ │ │ │ │ ├── 01-event/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ └── 02-hoist-return-fn/ │ │ │ │ ├── input.js │ │ │ │ └── output.js │ │ │ └── module.test.ts │ │ ├── module-oveo-disabled/ │ │ │ ├── data/ │ │ │ │ ├── 01-event/ │ │ │ │ │ ├── input.js │ │ │ │ │ └── output.js │ │ │ │ └── 02-hoist-return-fn/ │ │ │ │ ├── input.js │ │ │ │ └── output.js │ │ │ └── module.test.ts │ │ └── normalize.ts │ ├── package.json │ └── runtime/ │ ├── array.test.ts │ ├── component.test.ts │ ├── containsDOMElement.test.ts │ ├── context.test.ts │ ├── equal.test.ts │ ├── findDOMNode.test.ts │ ├── hasDOMElement.test.ts │ ├── hole.test.ts │ ├── html/ │ │ └── parser.test.ts │ ├── list.test.ts │ ├── mock-dom/ │ │ ├── document.test.ts │ │ ├── element.test.ts │ │ ├── innerHTML.test.ts │ │ ├── node.test.ts │ │ └── template.test.ts │ ├── template/ │ │ ├── attribute.test.ts │ │ ├── className.test.ts │ │ ├── directive.test.ts │ │ ├── event.test.ts │ │ ├── htm.test.ts │ │ ├── innerHTML.test.ts │ │ ├── property.test.ts │ │ ├── propertyDiffDOM.test.ts │ │ ├── style.test.ts │ │ ├── svg.test.ts │ │ └── textContent.test.ts │ ├── text.test.ts │ ├── useAnimationFrameEffect.test.ts │ ├── useEffect.test.ts │ ├── useIdleEffect.test.ts │ ├── useMemo.test.ts │ ├── useReducer.test.ts │ └── useState.test.ts ├── tsconfig.composite.json └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clippy.toml ================================================ disallowed-methods = [ { path = "str::to_ascii_lowercase", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_to_ascii_lowercase` instead." }, { path = "str::to_ascii_uppercase", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_to_ascii_uppercase` instead." }, { path = "str::to_lowercase", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_to_lowercase` instead." }, { path = "str::to_uppercase", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_to_uppercase` instead." }, { path = "str::replace", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_replace` instead." }, { path = "str::replacen", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_replacen` instead." }, ] ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 end_of_line = lf trim_trailing_whitespace = true insert_final_newline = true [*.{mts,ts,js,mjs}] indent_style = space indent_size = 2 [*.{css}] indent_style = space indent_size = 2 [*.{json,yml,yaml}] indent_style = space indent_size = 2 [*.{md}] indent_style = space indent_size = 2 ================================================ FILE: .gitattributes ================================================ * text=auto *.sh text eol=lf merge=union *.rs text eol=lf merge=union *.js text eol=lf merge=union *.mjs text eol=lf merge=union *.ts text eof=lf merge=union *.mts text eof=lf merge=union *.toml text eol=lf merge=union *.json text eol=lf merge=union *.yaml text eol=lf merge=union *.yml text eol=lf merge=union *.md text eol=lf merge=union *.css text eol=lf merge=union *.html text eol=lf merge=union *.csv text eol=lf merge=union ================================================ FILE: .github/renovate.json ================================================ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended", "npm:unpublishSafe", ":semanticCommits" ], "timezone": "Etc/UTC", "schedule": [ "* 0-4 * * 1-3" ], "labels": [ "dependencies" ], "commitMessagePrefix": "chore: ", "commitMessageAction": "bump up", "commitMessageTopic": "{{depName}} version", "ignoreDeps": [], "packageRules": [ { "groupName": "all non-major dependencies", "groupSlug": "all-minor-patch", "matchPackageNames": [ "*" ], "matchUpdateTypes": [ "minor", "patch" ] }, { "groupName": "all devDependencies", "groupSlug": "all-dev-dependencies", "matchDepTypes": [ "devDependencies" ], "rangeStrategy": "bump" } ] } ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI env: ACTION_CACHE_PATH: | ~/.bun/install/cache node_modules/ permissions: contents: write id-token: write on: workflow_dispatch: inputs: publish: required: false type: boolean push: paths: - packages/** - bun.lock - "!crates/**" - "!packages/@ivi/compiler/**" - "!tests/compiler/**" branches: - master pull_request: paths: - packages/** - bun.lock - "!crates/**" - "!packages/@ivi/compiler/**" - "!tests/compiler/**" branches: - master concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: publish: name: Publish if: "${{ github.event_name != 'pull_request' && (inputs.publish || startsWith(github.event.head_commit.message, 'publish:')) }}" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 - uses: oven-sh/setup-bun@v2 - uses: extractions/setup-just@v4 - uses: actions/cache@v5 with: path: ${{ env.ACTION_CACHE_PATH }} key: CI - run: just init - run: just tsc - name: Publish to NPM run: | npm config set provenance true echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc just publish --provenance --access public ================================================ FILE: .github/workflows/napi-libraries.yml ================================================ name: NAPI Libraries env: DEBUG: napi:* MACOSX_DEPLOYMENT_TARGET: "10.13" CARGO_INCREMENTAL: "1" ACTION_CACHE_PATH: | ~/.cargo/bin/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ target/ ~/.bun/install/cache node_modules/ permissions: contents: write id-token: write on: workflow_dispatch: inputs: publish: required: false type: boolean push: paths: - crates/** - packages/@ivi/compiler/** - tests/compiler/** - Cargo.lock branches: - master pull_request: paths: - crates/** - packages/@ivi/compiler/** - tests/compiler/** - Cargo.lock branches: - master concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build-napi: strategy: fail-fast: false matrix: settings: - host: ubuntu-latest target: x86_64-unknown-linux-gnu build: just napi build --release --target x86_64-unknown-linux-gnu --use-napi-cross - host: ubuntu-latest target: aarch64-unknown-linux-gnu build: just napi build --release --target aarch64-unknown-linux-gnu --use-napi-cross - host: macos-latest target: x86_64-apple-darwin build: just napi build --release --target x86_64-apple-darwin - host: macos-latest target: aarch64-apple-darwin build: just napi build --release --target aarch64-apple-darwin - host: windows-latest target: x86_64-pc-windows-msvc build: just napi build --release --target x86_64-pc-windows-msvc - host: windows-latest target: aarch64-pc-windows-msvc build: just napi build --release --target aarch64-pc-windows-msvc name: stable - ${{ matrix.settings.target }} runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 - uses: oven-sh/setup-bun@v2 - uses: extractions/setup-just@v4 - uses: dtolnay/rust-toolchain@stable with: toolchain: stable targets: ${{ matrix.settings.target }} - uses: actions/cache@v5 with: path: ${{ env.ACTION_CACHE_PATH }} key: NAPI-${{ matrix.settings.target }}-${{ matrix.settings.host }} - run: just init - run: ${{ matrix.settings.build }} - uses: actions/upload-artifact@v7 with: name: NAPI-${{ matrix.settings.target }} path: ./packages/@ivi/compiler/ivi-compiler.*.node if-no-files-found: error test: name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} needs: - build-napi strategy: fail-fast: false matrix: settings: - host: ubuntu-latest target: x86_64-unknown-linux-gnu architecture: x64 - host: ubuntu-24.04-arm target: aarch64-unknown-linux-gnu architecture: arm64 - host: windows-latest target: x86_64-pc-windows-msvc architecture: x64 # Bun doesn't have windows arm64 binaries # - host: windows-11-arm # target: aarch64-pc-windows-msvc # architecture: arm64 - host: macos-latest target: aarch64-apple-darwin architecture: arm64 # Bun is broken on x86_64 macos # - host: macos-latest # target: x86_64-apple-darwin # architecture: x64 runs-on: ${{ matrix.settings.host }} steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 architecture: ${{ matrix.settings.architecture }} - uses: oven-sh/setup-bun@v2 - uses: extractions/setup-just@v4 - uses: actions/cache@v5 with: path: ${{ env.ACTION_CACHE_PATH }} key: ${{ matrix.settings.target }}-${{ matrix.settings.host }} - run: just init if: steps.cache.outputs.cache-hit != 'true' - uses: actions/download-artifact@v8 with: name: NAPI-${{ matrix.settings.target }} path: ./packages/@ivi/compiler/ - run: just napi test publish: name: Publish if: "${{ github.event_name != 'pull_request' && (inputs.publish || startsWith(github.event.head_commit.message, 'publish:')) }}" runs-on: ubuntu-latest needs: - test steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 - uses: oven-sh/setup-bun@v2 - uses: extractions/setup-just@v4 - uses: actions/cache@v5 with: path: ${{ env.ACTION_CACHE_PATH }} key: NAPI-x86_64-unknown-linux-gnu-ubuntu-latest - run: just init if: steps.cache.outputs.cache-hit != 'true' - uses: actions/download-artifact@v8 with: pattern: NAPI-* path: napi-artifacts - run: just napi artifacts - name: Publish to NPM run: | npm config set provenance true echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc just napi publish --provenance --access public ================================================ FILE: .github/workflows/napi-release.yml ================================================ name: NAPI Release permissions: contents: write id-token: write on: workflow_dispatch: inputs: increment: type: choice required: true description: Increment version options: - patch - minor - major jobs: publish: name: Release new @ivi/compiler version runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: node-version: 24 - uses: oven-sh/setup-bun@v2 - uses: extractions/setup-just@v4 - name: Increment versions and push changes run: | just napi increment-versions ${{ github.event.inputs.increment }} git config --global user.name "GitHub Action" git config --global user.email "username@users.noreply.github.com" git commit -a -m "publish: @ivi/compiler $(jq -r .version ./packages/@ivi/compiler/package.json)" git push ================================================ FILE: .gitignore ================================================ /examples/**/dist /packages/**/dist /tests/dist .rsync-filter .DS_Store # Rust /target/ # NAPI-rs /napi-artifacts/ # JS node_modules *.tsbuildinfo npm-debug.log # Editors /.idea/ /.vscode/ !/.vscode/settings.json ================================================ FILE: .rustfmt.toml ================================================ unstable_features = true version = "Two" style_edition = "2024" edition = "2024" format_strings = true format_code_in_doc_comments = true hex_literal_case = "Lower" wrap_comments = true reorder_modules = true reorder_impl_items = true use_field_init_shorthand = true use_small_heuristics = "Max" group_imports = "StdExternalCrate" imports_granularity = "Crate" ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing ## Requirements - Bun - Just ## Getting Started 1. Clone the git repository: `git clone git@github.com:localvoid/ivi.git` 2. Go into the cloned folder: `cd ivi/` 3. Install all dependencies: `just init` ## Tasks - `just init` - initializes development environment. - `just napi build` - builds NAPI bindings. - `just tsc` - builds typescript packages. - `just napi test` - runs NAPI tests. - `just test` - runs tests. ================================================ FILE: Cargo.toml ================================================ [workspace] members = ["crates/*", "packages/@ivi/compiler"] resolver = "3" [workspace.dependencies] thiserror = "2" rustc-hash = "2" indexmap = "2.10" oxc_allocator = "0.121" oxc_ast = "0.121" oxc_codegen = "0.121" oxc_data_structures = "0.121" oxc_diagnostics = "0.121" oxc_ecmascript = "0.121" oxc_parser = "0.121" oxc_semantic = "0.121" oxc_span = "0.121" oxc_syntax = "0.121" oxc_traverse = "0.121" napi = "3" napi-derive = "3" napi-build = "2" ivi_compiler = { path = "./crates/ivi_compiler" } [workspace.lints.clippy] dbg_macro = "warn" todo = "warn" unimplemented = "warn" print_stdout = "warn" print_stderr = "warn" allow_attributes = "warn" clone_on_ref_ptr = "warn" self_named_module_files = "warn" empty_drop = "warn" empty_structs_with_brackets = "warn" exit = "warn" get_unwrap = "warn" rc_buffer = "warn" rc_mutex = "warn" rest_pat_in_fully_bound_structs = "warn" unnecessary_safety_comment = "warn" undocumented_unsafe_blocks = "warn" infinite_loop = "warn" map_with_unused_argument_over_ranges = "warn" unused_result_ok = "warn" pathbuf_init_then_push = "warn" collapsible_if = "allow" collapsible_else_if = "allow" collapsible_match = "allow" [profile.release] opt-level = 3 codegen-units = 1 lto = "fat" panic = "abort" strip = true split-debuginfo = "packed" [profile.dev] debug = false [profile.release-with-debug] inherits = "release" strip = false debug = true [profile.coverage] inherits = "release" opt-level = 2 codegen-units = 256 lto = "thin" debug-assertions = true overflow-checks = true ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016-2024 Boris Kaul . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: crates/ivi_compiler/Cargo.toml ================================================ [package] name = "ivi_compiler" version = "0.1.0" edition = "2024" authors = ["Boris Kaul "] license = "MIT" homepage = "https://github.com/localvoid/ivi" repository = "https://github.com/localvoid/ivi" description = "ivi template compiler" [dependencies] thiserror.workspace = true rustc-hash.workspace = true indexmap.workspace = true oxc_allocator.workspace = true oxc_ast.workspace = true oxc_codegen.workspace = true oxc_data_structures.workspace = true oxc_diagnostics.workspace = true oxc_ecmascript.workspace = true oxc_parser.workspace = true oxc_semantic.workspace = true oxc_span.workspace = true oxc_syntax.workspace = true oxc_traverse.workspace = true [lints] workspace = true ================================================ FILE: crates/ivi_compiler/src/chunk/mod.rs ================================================ use oxc_allocator::{Allocator, Vec as ArenaVec}; use oxc_ast::ast::*; use oxc_semantic::Scoping; use oxc_span::SPAN; use oxc_traverse::{Traverse, traverse_mut}; use rustc_hash::FxHashMap; use crate::{ context::{TraverseCtx, TraverseCtxState}, tpl::opcodes::prop_op, }; pub fn compile_chunk<'a>( program: &mut Program<'a>, allocator: &'a Allocator, scoping: Scoping, strings: &FxHashMap, ) { let mut t = ChunkCompiler::new(strings); traverse_mut(&mut t, allocator, program, scoping, TraverseCtxState::default()); } struct ChunkCompiler<'ctx> { strings: &'ctx FxHashMap, } impl<'ctx> ChunkCompiler<'ctx> { pub fn new(strings: &'ctx FxHashMap) -> Self { Self { strings } } } impl<'a> Traverse<'a, TraverseCtxState<'a>> for ChunkCompiler<'_> { fn enter_expression(&mut self, node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { match node { Expression::ArrayExpression(expr) => { if expr.elements.len() == 1 { if let Some(Expression::StringLiteral(s)) = expr.elements[0].as_expression() { if s.value == STRINGS_UUID { let mut indexed: Vec<_> = self.strings.iter().collect(); indexed.sort_by_key(|e| e.1); let mut strings = ctx.ast.vec_with_capacity(self.strings.len()); for (s, _) in indexed { strings.push(ArrayExpressionElement::StringLiteral( ctx.ast.alloc_string_literal(SPAN, ctx.ast.atom(s), None), )); } *node = ctx.ast.expression_array(SPAN, strings); } } } } Expression::CallExpression(expr) => { if let Some("__IVI_TPL__") = expr.callee_name() { if let Some(mut arg0) = expr.arguments.pop() { if let Some(Expression::CallExpression(call)) = arg0.as_expression_mut() { if call.arguments.len() > 5 { if let Some(Argument::ArrayExpression(tpl_strings)) = call.arguments.pop() { let prop_op_codes = &mut call.arguments[2]; update_prop_op_codes( prop_op_codes.as_expression_mut().unwrap(), &tpl_strings.elements, self.strings, ); } } *node = arg0.into_expression(); } } } } _ => {} } } } fn update_prop_op_codes<'a>( expr: &mut Expression, tpl_strings: &ArenaVec<'a, ArrayExpressionElement<'a>>, strings: &FxHashMap, ) { match expr { // dedupe(op_codes) Expression::CallExpression(c) => { if let Some(e) = c.arguments.get_mut(0).and_then(|a| a.as_expression_mut()) { update_prop_op_codes(e, tpl_strings, strings); } } Expression::ArrayExpression(a) => { for el in &mut a.elements { if let Some(Expression::NumericLiteral(op)) = el.as_expression_mut() { let v = op.value as u32; let ty = v & prop_op::TYPE_MASK; if ty != prop_op::SET_NODE && ty != prop_op::COMMON && ty != prop_op::DIRECTIVE { let i = v >> prop_op::DATA_SHIFT; let s = &tpl_strings[i as usize]; if let ArrayExpressionElement::StringLiteral(s) = s { if let Some(new_index) = strings.get(s.value.as_str()) { op.value = ((v & ((1 << prop_op::DATA_SHIFT) - 1)) | ((*new_index as u32) << prop_op::DATA_SHIFT)) as f64; } } } } } } _ => {} } } const STRINGS_UUID: &str = "IVI:fa7327d9-0034-492d-bfdf-576548b2d9cc"; ================================================ FILE: crates/ivi_compiler/src/context.rs ================================================ use std::marker::PhantomData; pub type TraverseCtx<'a> = oxc_traverse::TraverseCtx<'a, TraverseCtxState<'a>>; #[derive(Default)] pub struct TraverseCtxState<'a> { data: PhantomData<&'a ()>, } ================================================ FILE: crates/ivi_compiler/src/import.rs ================================================ use oxc_ast::{ NONE, ast::{ Expression, ImportDeclarationSpecifier, ImportOrExportKind, ModuleExportName, Statement, }, }; use oxc_semantic::SymbolFlags; use oxc_span::SPAN; use oxc_traverse::BoundIdentifier; use crate::context::TraverseCtx; #[derive(Default)] pub struct ImportSymbols<'a> { descriptor_id: Option>, // _T html_id: Option>, // _hN html_el_id: Option>, // _hE svg_id: Option>, // _sN svg_el_id: Option>, // _sE tpl_id: Option>, // _t empty_array_id: Option>, // _t hoist_id: Option>, dedupe_id: Option>, } impl<'a> ImportSymbols<'a> { pub fn template_descriptor(&mut self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { get(&mut self.descriptor_id, "_T", ctx) } pub fn html_template(&mut self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { get(&mut self.html_id, "_hN", ctx) } pub fn html_element(&mut self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { get(&mut self.html_el_id, "_hE", ctx) } pub fn svg_template(&mut self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { get(&mut self.svg_id, "_sN", ctx) } pub fn svg_element(&mut self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { get(&mut self.svg_el_id, "_sE", ctx) } pub fn create_from_template(&mut self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { get(&mut self.tpl_id, "_t", ctx) } pub fn empty_array(&mut self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { get(&mut self.empty_array_id, "EMPTY_ARRAY", ctx) } pub fn hoist(&mut self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { get(&mut self.hoist_id, "hoist", ctx) } pub fn dedupe(&mut self, ctx: &mut TraverseCtx<'a>) -> Expression<'a> { get(&mut self.dedupe_id, "dedupe", ctx) } pub fn create_import_statements(&self, ctx: &mut TraverseCtx<'a>) -> Vec> { let mut imports = Vec::new(); let mut specifiers = ctx.ast.vec(); if let Some(id) = &self.descriptor_id { specifiers.push(spec("_T", id, ctx)); } if let Some(id) = &self.html_id { specifiers.push(spec("_hN", id, ctx)); } if let Some(id) = &self.html_el_id { specifiers.push(spec("_hE", id, ctx)); } if let Some(id) = &self.svg_id { specifiers.push(spec("_sN", id, ctx)); } if let Some(id) = &self.svg_el_id { specifiers.push(spec("_sE", id, ctx)); } if let Some(id) = &self.tpl_id { specifiers.push(spec("_t", id, ctx)); } if let Some(id) = &self.empty_array_id { specifiers.push(spec("EMPTY_ARRAY", id, ctx)); } if !specifiers.is_empty() { imports.push(Statement::ImportDeclaration(ctx.ast.alloc_import_declaration( SPAN, Some(specifiers), ctx.ast.string_literal(SPAN, ctx.ast.atom("ivi"), None), None, NONE, ImportOrExportKind::Value, ))); } let mut specifiers = ctx.ast.vec(); if let Some(id) = &self.hoist_id { specifiers.push(spec("hoist", id, ctx)); } if let Some(id) = &self.dedupe_id { specifiers.push(spec("dedupe", id, ctx)); } if !specifiers.is_empty() { imports.push(Statement::ImportDeclaration(ctx.ast.alloc_import_declaration( SPAN, Some(specifiers), ctx.ast.string_literal(SPAN, ctx.ast.atom("oveo"), None), None, NONE, ImportOrExportKind::Value, ))); } imports } } fn get<'a>( cell: &mut Option>, name: &'static str, ctx: &mut TraverseCtx<'a>, ) -> Expression<'a> { if let Some(id) = cell { id.create_read_expression(ctx) } else { let uid = ctx.generate_uid_in_root_scope(name, SymbolFlags::ConstVariable); let read = uid.create_read_expression(ctx); *cell = Some(uid); read } } fn spec<'a>( name: &'static str, id: &BoundIdentifier<'a>, ctx: &mut TraverseCtx<'a>, ) -> ImportDeclarationSpecifier<'a> { ImportDeclarationSpecifier::ImportSpecifier(ctx.ast.alloc_import_specifier( SPAN, ModuleExportName::IdentifierName(ctx.ast.identifier_name(SPAN, ctx.ast.atom(name))), id.create_binding_identifier(ctx), ImportOrExportKind::Value, )) } ================================================ FILE: crates/ivi_compiler/src/lib.rs ================================================ use std::path::PathBuf; use oxc_allocator::Allocator; use oxc_codegen::{Codegen, CodegenOptions}; use oxc_diagnostics::GraphicalReportHandler; use oxc_parser::Parser; use oxc_semantic::SemanticBuilder; use oxc_span::SourceType; use rustc_hash::{FxHashMap, FxHashSet}; mod chunk; mod context; mod import; mod module; mod oveo; mod tpl; #[derive(Default, Debug)] pub struct CompilerOptions { pub dedupe_strings: bool, pub oveo: bool, } pub struct CompilerOutput { pub code: String, pub map: String, } #[derive(Debug, thiserror::Error)] pub enum CompilerError { #[error("Invalid module type: {0}")] ModuleType(String), #[error("Unable to parse javascript file: {0}")] SyntaxError(String), #[error("Unable to parse javascript file: {0}")] SemanticError(String), #[error("Invalid template: {0}")] InvalidTemplate(String), } pub fn compile_module( source_text: &str, module_type: &str, options: &CompilerOptions, strings: &mut FxHashSet, ) -> Result { let allocator = Allocator::default(); let source_type = match module_type { "js" => SourceType::mjs(), "jsx" => SourceType::jsx(), "ts" => SourceType::ts(), "tsx" => SourceType::tsx(), _ => return Err(CompilerError::ModuleType(module_type.to_string())), }; let ret = Parser::new(&allocator, source_text, source_type).parse(); if let Some(err) = ret.errors.first() { return Err(CompilerError::SyntaxError(err.to_string())); } let mut program = ret.program; let ret = SemanticBuilder::new().with_excess_capacity(1.0).build(&program); if let Some(err) = ret.errors.first() { return Err(CompilerError::SemanticError(err.to_string())); } let scoping = ret.semantic.into_scoping(); let mut errors = module::compile_module(&mut program, &allocator, scoping, options, strings); if let Some(err) = errors.drain(..).next() { let report_handler = GraphicalReportHandler::new(); let mut s = String::new(); let _ = report_handler .render_report(&mut s, err.with_source_code(source_text.to_string()).as_ref()); return Err(CompilerError::InvalidTemplate(s)); } let result = Codegen::new() .with_options(CodegenOptions { source_map_path: Some(PathBuf::new()), ..Default::default() }) .build(&program); Ok(CompilerOutput { code: result.code, map: result.map.map_or_else(String::default, |v| v.to_json_string()), }) } pub fn compile_chunk( source_text: &str, strings: &FxHashMap, ) -> Result { let allocator = Allocator::default(); let source_type = SourceType::default(); let ret = Parser::new(&allocator, source_text, source_type).parse(); if let Some(err) = ret.errors.first() { return Err(CompilerError::SyntaxError(err.to_string())); } let mut program = ret.program; let ret = SemanticBuilder::new().with_excess_capacity(0.1).build(&program); if let Some(err) = ret.errors.first() { return Err(CompilerError::SemanticError(err.to_string())); } let scoping = ret.semantic.into_scoping(); chunk::compile_chunk(&mut program, &allocator, scoping, strings); let result = Codegen::new() .with_options(CodegenOptions { source_map_path: Some(PathBuf::new()), ..Default::default() }) .build(&program); Ok(CompilerOutput { code: result.code, map: result.map.map_or_else(String::default, |v| v.to_json_string()), }) } ================================================ FILE: crates/ivi_compiler/src/module/mod.rs ================================================ use std::collections::hash_map; use oxc_allocator::{Address, Allocator, GetAddress, TakeIn}; use oxc_ast::ast::*; use oxc_diagnostics::OxcDiagnostic; use oxc_semantic::{Scoping, SymbolId}; use oxc_traverse::{Traverse, traverse_mut}; use rustc_hash::{FxHashMap, FxHashSet}; use crate::{ CompilerOptions, context::{TraverseCtx, TraverseCtxState}, import::ImportSymbols, oveo::oveo_intrinsic, tpl::{TemplateKind, compile_template}, }; pub fn compile_module<'a>( program: &mut Program<'a>, allocator: &'a Allocator, scoping: Scoping, options: &CompilerOptions, strings: &mut FxHashSet, ) -> Vec { let mut t = ModuleCompiler::new(options, strings); traverse_mut(&mut t, allocator, program, scoping, TraverseCtxState::default()); t.errors } struct ModuleCompiler<'a, 'ctx> { options: &'ctx CompilerOptions, strings: &'ctx mut FxHashSet, ivi_module: FxHashMap, imports: ImportSymbols<'a>, statements: Vec
, templates: FxHashMap>>, errors: Vec, } impl<'a, 'ctx> ModuleCompiler<'a, 'ctx> { pub fn new(options: &'ctx CompilerOptions, strings: &'ctx mut FxHashSet) -> Self { Self { options, strings, ivi_module: FxHashMap::default(), imports: ImportSymbols::default(), statements: Vec::new(), templates: FxHashMap::default(), errors: Vec::new(), } } fn resolve(&self, expr: &Expression<'a>, scoping: &Scoping) -> Option { match expr { Expression::Identifier(id) => { let r = scoping.get_reference(id.reference_id()); if let Some(symbol_id) = r.symbol_id() { self.ivi_module.get(&symbol_id).copied() } else { None } } Expression::StaticMemberExpression(expr) => { if let Some(IviSymbol::Module) = self.resolve(&expr.object, scoping) { match expr.property.name.as_str() { "component" => Some(IviSymbol::Component), "html" => Some(IviSymbol::Html), "svg" => Some(IviSymbol::Svg), _ => None, } } else { None } } _ => None, } } fn add_template_decl(&mut self, address: Address, decl: Statement<'a>) { match self.templates.entry(address) { hash_map::Entry::Occupied(mut entry) => { entry.get_mut().push(decl); } hash_map::Entry::Vacant(entry) => { entry.insert(vec![decl]); } } } } impl<'a> Traverse<'a, TraverseCtxState<'a>> for ModuleCompiler<'a, '_> { fn enter_statement(&mut self, node: &mut Statement<'a>, _ctx: &mut TraverseCtx<'a>) { self.statements.push(node.address()); } fn exit_statement(&mut self, _node: &mut Statement<'a>, _ctx: &mut TraverseCtx<'a>) { self.statements.pop(); } fn exit_expression(&mut self, node: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { match node { Expression::CallExpression(expr) if self.options.oveo => { // hoist render functions // component(() => hoist(() => { .. })); if let Some(IviSymbol::Component) = self.resolve(&expr.callee, ctx.scoping()) { if let Some(Argument::ArrowFunctionExpression(expr)) = expr.arguments.get_mut(0) { if expr.expression { if let Some(Statement::ExpressionStatement(expr_stmt)) = &mut expr.body.statements.get_mut(0) { expr_stmt.expression = oveo_intrinsic( expr_stmt.expression.take_in(ctx.ast.allocator), self.imports.hoist(ctx), ctx, ); } } } } } Expression::TaggedTemplateExpression(expr) => { if let Some(ivi) = self.resolve(&expr.tag, ctx.scoping()) { let kind = match ivi { IviSymbol::Html => TemplateKind::Html, IviSymbol::Svg => TemplateKind::Svg, _ => { return; } }; match compile_template( &mut expr.quasi, ctx, kind, &mut self.imports, self.options.oveo, self.options.dedupe_strings, ) { Ok(result) => { for s in result.strings { self.strings.insert(s); } let address = self.statements[0]; for decl in result.decl { self.add_template_decl(address, decl); } *node = result.expr; } Err(error) => { self.errors.push(error); } } } } _ => {} } } fn exit_import_declaration( &mut self, node: &mut ImportDeclaration<'a>, _ctx: &mut TraverseCtx<'a>, ) { // Resolve ivi module if let Some(specifiers) = &node.specifiers { let source = &node.source; if source.value != "ivi" { return; } for spec in specifiers { match spec { // import { imported } from "source" // import { imported as local } from "source" ImportDeclarationSpecifier::ImportSpecifier(spec) => { let s = match spec.imported.name().as_str() { "component" => IviSymbol::Component, "html" => IviSymbol::Html, "svg" => IviSymbol::Svg, _ => { continue; } }; self.ivi_module.insert(spec.local.symbol_id(), s); } // import * as local from "source" ImportDeclarationSpecifier::ImportNamespaceSpecifier(spec) => { self.ivi_module.insert(spec.local.symbol_id(), IviSymbol::Module); } _ => {} } } } } fn exit_program(&mut self, node: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) { let imports = self.imports.create_import_statements(ctx); if !imports.is_empty() { let index = node .body .iter() .position(|stmt| !matches!(stmt, Statement::ImportDeclaration(_))) .unwrap_or(node.body.len()); node.body.splice(index..index, imports); } if !self.templates.is_empty() { let statements = &mut node.body; let mut new_statements = ctx.ast.vec_with_capacity(statements.len() + self.templates.len()); for stmt in statements.drain(..) { if let Some(s) = self.templates.remove(&stmt.address()) { new_statements.extend(s.into_iter()); } new_statements.push(stmt); } *statements = new_statements; } } } #[derive(Clone, Copy)] enum IviSymbol { Module, Component, Html, Svg, } ================================================ FILE: crates/ivi_compiler/src/oveo.rs ================================================ use oxc_ast::{NONE, ast::Expression}; use oxc_span::SPAN; use crate::context::TraverseCtx; // annotation(expr) pub fn oveo_intrinsic<'a>( expr: Expression<'a>, callee: Expression<'a>, ctx: &mut TraverseCtx<'a>, ) -> Expression<'a> { ctx.ast.expression_call(SPAN, callee, NONE, ctx.ast.vec_from_array([expr.into()]), false) } ================================================ FILE: crates/ivi_compiler/src/tpl/emit.rs ================================================ use indexmap::IndexSet; use oxc_allocator::{TakeIn, Vec as ArenaVec}; use oxc_ast::{ AstBuilder, ast::{Expression, TemplateElement, TemplateElementValue}, }; use oxc_span::SPAN; use crate::{ context::TraverseCtx, import::ImportSymbols, oveo::oveo_intrinsic, tpl::{ TemplateKind, opcodes::{child_op, common_prop_type, prop_op, state_op, template_flags}, parser::{ TElement, TNode, TNodeKind, TProperty, TPropertyAttributeValue, TPropertyStyleValue, }, }, }; pub enum TemplateNode<'a> { Block(TemplateBlock<'a>), Text(String), Expr(usize), } pub struct TemplateBlock<'a> { pub statics: Expression<'a>, pub flags: u32, pub props_op_codes: Vec, pub child_op_codes: Vec, pub state_op_codes: Vec, pub strings: IndexSet, pub expressions: Vec, } pub fn emit_root_element<'a>( node: &TNode, kind: TemplateKind, expressions: &mut ArenaVec<'a, Expression<'a>>, ctx: &mut TraverseCtx<'a>, imports: &mut ImportSymbols<'a>, oveo: bool, ) -> TemplateNode<'a> { match &node.kind { TNodeKind::Element(e) => { let statics = emit_static_template(e, expressions, &mut ctx.ast); let expr_map = create_expr_map(e, ctx, expressions, imports, oveo); let state_op_codes = emit_state_op_codes(e); let (props_op_codes, strings) = emit_props_op_codes(node, &expr_map); let child_op_codes = emit_child_op_codes(node, &expr_map); let state_slots = count_state_slots(&state_op_codes); let child_slots = count_child_slots(&child_op_codes); let mut flags = state_slots | (child_slots << template_flags::CHILDREN_SIZE_SHIFT); if let TemplateKind::Svg = kind { flags |= template_flags::SVG; } TemplateNode::Block(TemplateBlock { statics, flags, props_op_codes, child_op_codes, state_op_codes, strings, expressions: expr_map.iter().copied().collect(), }) } TNodeKind::Text(t) => TemplateNode::Text(t.value.clone()), TNodeKind::Expr(e) => TemplateNode::Expr(e.index.inner()), } } fn count_state_slots(op_codes: &[u32]) -> u32 { let mut count = 1; for op in op_codes { if *op & state_op::SAVE != 0 || (*op & state_op::ENTER_OR_REMOVE != 0 && (op >> state_op::OFFSET_SHIFT) == 0) { count += 1 } } count } fn count_child_slots(op_codes: &[u32]) -> u32 { let mut count = 0; for op in op_codes { if op & child_op::TYPE == child_op::CHILD { count += 1; } } count } fn create_expr_map<'a>( root: &TElement, ctx: &mut TraverseCtx<'a>, expressions: &mut ArenaVec<'a, Expression<'a>>, imports: &mut ImportSymbols<'a>, oveo: bool, ) -> IndexSet { let mut map = IndexSet::default(); _create_expr_map(&mut map, root, ctx, expressions, imports, oveo); map } fn _create_expr_map<'a>( map: &mut IndexSet, node: &TElement, ctx: &mut TraverseCtx<'a>, expressions: &mut ArenaVec<'a, Expression<'a>>, imports: &mut ImportSymbols<'a>, oveo: bool, ) { for p in &node.properties { match p { TProperty::Attribute(p) => { if let TPropertyAttributeValue::Expr(v) = &p.value && !v.hoist { map.insert(v.index.inner()); } } TProperty::Value(p) => { map.insert(p.value.inner()); } TProperty::DOMValue(p) => { map.insert(p.value.inner()); } TProperty::Style(p) => { if let TPropertyStyleValue::Expr(v) = &p.value { map.insert(v.inner()); } } TProperty::Event(p) => { let i = p.value.inner(); if oveo { expressions[i] = oveo_intrinsic( expressions[i].take_in(ctx.ast.allocator), imports.hoist(ctx), ctx, ); } map.insert(i); } TProperty::Directive(p) => { map.insert(p.inner()); } } } for c in &node.children { match &c.kind { TNodeKind::Element(e) => { _create_expr_map(map, e, ctx, expressions, imports, oveo); } TNodeKind::Expr(e) => { map.insert(e.index.inner()); } _ => {} } } } fn emit_static_template<'a>( node: &TElement, template_expressions: &mut ArenaVec<'a, Expression<'a>>, ast: &mut AstBuilder<'a>, ) -> Expression<'a> { let mut static_part = String::new(); let mut quasis = ast.vec(); let mut expressions = ast.vec(); // Node doesn't have any children elements/texts or static properties let mut is_simple_node = true; _emit_static_template( &mut is_simple_node, node, &mut static_part, &mut quasis, &mut expressions, template_expressions, ast, ); if is_simple_node { ast.expression_string_literal(SPAN, ast.atom(&node.tag), None) } else { quasis.push(ast.template_element( SPAN, TemplateElementValue { raw: ast.atom(&static_part), cooked: None }, true, false, )); ast.expression_template_literal(SPAN, quasis, expressions) } } fn _emit_static_template<'a>( is_simple_node: &mut bool, node: &TElement, static_part: &mut String, quasis: &mut ArenaVec<'a, TemplateElement<'a>>, expressions: &mut ArenaVec<'a, Expression<'a>>, template_expressions: &mut ArenaVec<'a, Expression<'a>>, ast: &mut AstBuilder<'a>, ) { static_part.push('<'); static_part.push_str(&node.tag); let mut style = String::new(); for p in &node.properties { match p { TProperty::Attribute(p) => match &p.value { TPropertyAttributeValue::String(v) => { if p.key == "style" { if style.is_empty() { style = v.clone(); } else { style.push(';'); style.push_str(v); } } else { *is_simple_node = false; static_part.push(' '); static_part.push_str(&p.key); static_part.push_str("=\""); static_part.push_str(v); static_part.push('"'); } } TPropertyAttributeValue::Bool => { *is_simple_node = false; static_part.push(' '); static_part.push_str(&p.key); } TPropertyAttributeValue::Expr(v) => { if v.hoist { *is_simple_node = false; static_part.push(' '); static_part.push_str(&p.key); static_part.push_str("=\""); quasis.push(ast.template_element( SPAN, TemplateElementValue { raw: ast.atom(static_part), cooked: None }, false, false, )); expressions .push(template_expressions[v.index.inner()].take_in(ast.allocator)); static_part.clear(); static_part.push('"'); } } }, TProperty::Style(p) => { if let TPropertyStyleValue::String(v) = &p.value { if !style.is_empty() { style.push(';'); } style.push_str(&p.key); style.push(':'); style.push_str(v); } } _ => {} } } if !style.is_empty() { *is_simple_node = false; static_part.push_str(" style=\""); static_part.push_str(&style); static_part.push('"'); } static_part.push('>'); if node.void { return; } let mut siblings_state = 0; for c in &node.children { match &c.kind { TNodeKind::Element(c) => { *is_simple_node = false; _emit_static_template( is_simple_node, c, static_part, quasis, expressions, template_expressions, ast, ); siblings_state = 0; } TNodeKind::Text(n) => { *is_simple_node = false; if (siblings_state & 3) == 3 { static_part.push_str(""); } siblings_state = 1; static_part.push_str(&n.value); } TNodeKind::Expr(_) => { siblings_state |= 2; } } } static_part.push_str("'); } fn emit_props_op_codes(node: &TNode, expr_map: &IndexSet) -> (Vec, IndexSet) { let mut op_codes = Vec::new(); let mut strings = IndexSet::new(); _emit_props_op_codes(&mut op_codes, node, true, &mut strings, expr_map); (op_codes, strings) } fn _emit_props_op_codes( op_codes: &mut Vec, node: &TNode, is_root: bool, strings: &mut IndexSet, expr_map: &IndexSet, ) { fn string_index(strings: &mut IndexSet, key: &str) -> u32 { if let Some(i) = strings.get_index_of(key) { i as u32 } else { let (i, _) = strings.insert_full(key.to_string()); i as u32 } } if let TNodeKind::Element(e) = &node.kind { if node.props_exprs > 0 { if !is_root { op_codes .push(prop_op::SET_NODE | ((node.state_index as u32) << prop_op::DATA_SHIFT)); } for p in &e.properties { match p { TProperty::Attribute(p) => { if let TPropertyAttributeValue::Expr(expr) = &p.value { if let Some(i) = expr_map.get_index_of(&expr.index.inner()) { if p.key == "class" { op_codes.push( prop_op::COMMON | (common_prop_type::CLASS_NAME << prop_op::DATA_SHIFT) | ((i as u32) << prop_op::INPUT_SHIFT), ); } else { op_codes.push( prop_op::ATTRIBUTE | (string_index(strings, &p.key) << prop_op::DATA_SHIFT) | ((i as u32) << prop_op::INPUT_SHIFT), ); } } } } TProperty::Value(p) => { match p.key.as_str() { "textContent" => { op_codes.push( prop_op::COMMON | (common_prop_type::TEXT_CONTENT << prop_op::DATA_SHIFT) | ((p.value.inner() as u32) << prop_op::INPUT_SHIFT), ); } "innerHTML" => { op_codes.push( prop_op::COMMON | (common_prop_type::INNER_HTML << prop_op::DATA_SHIFT) | ((p.value.inner() as u32) << prop_op::INPUT_SHIFT), ); } _ => { if let Some(i) = expr_map.get_index_of(&p.value.inner()) { op_codes.push( prop_op::PROPERTY | (string_index(strings, &p.key) << prop_op::DATA_SHIFT) | ((i as u32) << prop_op::INPUT_SHIFT), ); } } }; } TProperty::DOMValue(p) => { match p.key.as_str() { "textContent" => { op_codes.push( prop_op::COMMON | (common_prop_type::TEXT_CONTENT << prop_op::DATA_SHIFT) | ((p.value.inner() as u32) << prop_op::INPUT_SHIFT), ); } "innerHTML" => { op_codes.push( prop_op::COMMON | (common_prop_type::INNER_HTML << prop_op::DATA_SHIFT) | ((p.value.inner() as u32) << prop_op::INPUT_SHIFT), ); } _ => { if let Some(i) = expr_map.get_index_of(&p.value.inner()) { op_codes.push( prop_op::DIFF_DOM_PROPERTY | (string_index(strings, &p.key) << prop_op::DATA_SHIFT) | ((i as u32) << prop_op::INPUT_SHIFT), ); } } }; } TProperty::Style(p) => { if let TPropertyStyleValue::Expr(expr_index) = &p.value { if let Some(i) = expr_map.get_index_of(&expr_index.inner()) { op_codes.push( prop_op::STYLE | (string_index(strings, &p.key) << prop_op::DATA_SHIFT) | ((i as u32) << prop_op::INPUT_SHIFT), ); } } } TProperty::Event(p) => { if let Some(i) = expr_map.get_index_of(&p.value.inner()) { op_codes.push( prop_op::EVENT | (string_index(strings, &p.key) << prop_op::DATA_SHIFT) | ((i as u32) << prop_op::INPUT_SHIFT), ); } } TProperty::Directive(p) => { if let Some(i) = expr_map.get_index_of(&p.inner()) { op_codes .push(prop_op::DIRECTIVE | ((i as u32) << prop_op::INPUT_SHIFT)); } } } } } for c in &e.children { _emit_props_op_codes(op_codes, c, false, strings, expr_map); } } } fn emit_state_op_codes(node: &TElement) -> Vec { let mut op_codes = Vec::new(); _emit_state_op_codes(&mut op_codes, node); op_codes } fn _emit_state_op_codes(op_codes: &mut Vec, node: &TElement) { mod state_flags { pub const PREV_TEXT: u32 = 1; pub const PREV_EXPR: u32 = 1 << 1; } let mut state = 0; 'outer: for c in &node.children { match &c.kind { TNodeKind::Element(e) => { let mut op = 0; if state & state_flags::PREV_EXPR != 0 || c.children_exprs > 0 || c.props_exprs > 0 { op = state_op::SAVE; } let current_op_index = op_codes.len(); op_codes.push(op); if c.flags & TNode::HAS_EXPRESSIONS != 0 { _emit_state_op_codes(op_codes, e); let children_offset = op_codes.len() - (current_op_index + 1); if children_offset > 0 { op |= state_op::ENTER_OR_REMOVE | ((children_offset as u32) << state_op::OFFSET_SHIFT); op_codes[current_op_index] = op; } } if c.flags & (TNode::HAS_NEXT_EXPRESSION | TNode::HAS_NEXT_DOM_NODE) != (TNode::HAS_NEXT_EXPRESSION | TNode::HAS_NEXT_DOM_NODE) { if op == 0 { op_codes.pop(); } break 'outer; } state = 0; } TNodeKind::Text(_) => { if state & (state_flags::PREV_TEXT | state_flags::PREV_EXPR) == (state_flags::PREV_TEXT | state_flags::PREV_EXPR) { op_codes.push(state_op::ENTER_OR_REMOVE); } else if state & state_flags::PREV_EXPR != 0 { op_codes.push(state_op::SAVE); } else if c.flags & (TNode::HAS_NEXT_EXPRESSION | TNode::HAS_NEXT_DOM_NODE) != (TNode::HAS_NEXT_EXPRESSION | TNode::HAS_NEXT_DOM_NODE) { break 'outer; } else { op_codes.push(0); } state = state_flags::PREV_TEXT; } TNodeKind::Expr(_) => { state |= state_flags::PREV_EXPR; } } } } fn emit_child_op_codes(node: &TNode, expr_map: &IndexSet) -> Vec { let mut op_codes = Vec::new(); _emit_child_op_codes(&mut op_codes, node, true, expr_map); op_codes } fn _emit_child_op_codes( op_codes: &mut Vec, node: &TNode, is_root: bool, expr_map: &IndexSet, ) { if let TNodeKind::Element(e) = &node.kind { if node.children_exprs > 0 { if !is_root { op_codes.push( child_op::SET_PARENT | ((node.state_index as u32) << child_op::VALUE_SHIFT), ); } let mut prev_state_index = None; let mut prev_expr = false; for c in e.children.iter().rev() { if let TNodeKind::Expr(expr_index) = &c.kind { if let Some(prev_state_index) = prev_state_index && !prev_expr { op_codes.push( child_op::SET_NEXT | ((prev_state_index as u32) << child_op::VALUE_SHIFT), ); } op_codes.push( child_op::CHILD | ((expr_map.get_index_of(&expr_index.index.inner()).unwrap() as u32) << child_op::VALUE_SHIFT), ); prev_expr = true; } else { prev_expr = false; prev_state_index = Some(c.state_index); } } } for c in e.children.iter().rev() { _emit_child_op_codes(op_codes, c, false, expr_map); } } } ================================================ FILE: crates/ivi_compiler/src/tpl/html.rs ================================================ pub fn is_html_void_element(tag: &str) -> bool { matches!( tag, "audio" | "video" | "embed" | "input" | "param" | "source" | "track" | "area" | "base" | "link" | "meta" | "br" | "col" | "hr" | "img" | "wbr" ) } ================================================ FILE: crates/ivi_compiler/src/tpl/mod.rs ================================================ use indexmap::IndexSet; use oxc_allocator::TakeIn; use oxc_ast::{NONE, ast::*}; use oxc_diagnostics::OxcDiagnostic; use oxc_semantic::SymbolFlags; use oxc_span::SPAN; use crate::{ context::TraverseCtx, import::ImportSymbols, oveo::oveo_intrinsic, tpl::emit::TemplateNode, }; mod emit; mod html; pub mod opcodes; mod parser; pub struct CompiledTemplate<'a> { pub decl: Vec>, pub expr: Expression<'a>, pub strings: Vec, } #[derive(Clone, Copy)] pub enum TemplateKind { Html, Svg, } pub fn compile_template<'a>( tpl: &mut TemplateLiteral<'a>, ctx: &mut TraverseCtx<'a>, kind: TemplateKind, imports: &mut ImportSymbols<'a>, oveo: bool, dedupe_strings: bool, ) -> Result, OxcDiagnostic> { let mut decl = Vec::new(); let mut exprs = Vec::new(); let mut strings = Vec::new(); let nodes = parser::parse_template(tpl, ctx.scoping())?; for n in &nodes { let e = emit::emit_root_element(n, kind, &mut tpl.expressions, ctx, imports, oveo); match e { TemplateNode::Block(t) => { let uid = ctx.generate_uid_in_root_scope("_TPL_", SymbolFlags::ConstVariable); // const _TPL_ = __IVI_TPL__(_T(statics, ..opcodes)); let statics = if let Expression::StringLiteral(_) = t.statics { ctx.ast.expression_call( SPAN, match kind { TemplateKind::Html => imports.html_element(ctx), TemplateKind::Svg => imports.svg_element(ctx), }, NONE, ctx.ast.vec1(t.statics.into()), false, ) } else { ctx.ast.expression_call( SPAN, match kind { TemplateKind::Html => imports.html_template(ctx), TemplateKind::Svg => imports.svg_template(ctx), }, NONE, ctx.ast.vec1(t.statics.into()), false, ) }; let statics = if oveo { oveo_intrinsic(statics, imports.dedupe(ctx), ctx) } else { statics }; let mut arguments = ctx.ast.vec_with_capacity(6); arguments.push(statics.into()); arguments.push( ctx.ast .expression_numeric_literal(SPAN, t.flags as f64, None, NumberBase::Decimal) .into(), ); arguments .push(op_codes_into_expression(&t.props_op_codes, ctx, imports, oveo).into()); arguments .push(op_codes_into_expression(&t.child_op_codes, ctx, imports, oveo).into()); arguments .push(op_codes_into_expression(&t.state_op_codes, ctx, imports, oveo).into()); if !t.strings.is_empty() { arguments.push(strings_into_expression(&t.strings, ctx).into()); strings.extend(t.strings.into_iter()); } let template_descriptor = ctx.ast.expression_call( SPAN, imports.template_descriptor(ctx), NONE, arguments, false, ); let v = ctx.ast.declaration_variable( SPAN, VariableDeclarationKind::Const, ctx.ast.vec1(ctx.ast.variable_declarator( SPAN, VariableDeclarationKind::Const, ctx.ast.binding_pattern_binding_identifier(SPAN, uid.name), NONE, Some(if dedupe_strings { ctx.ast.expression_call( SPAN, ctx.ast.expression_identifier(SPAN, ctx.ast.atom("__IVI_TPL__")), NONE, ctx.ast.vec_from_array([template_descriptor.into()]), false, ) } else { template_descriptor }), false, )), false, ); decl.push(v.into()); // _t(_TPL_, [expressions]) let call_expressions = if t.expressions.is_empty() { ctx.ast.vec_from_array([uid.create_read_expression(ctx).into()]) } else { ctx.ast.vec_from_array([ uid.create_read_expression(ctx).into(), ctx.ast .expression_array( SPAN, ctx.ast.vec_from_iter(t.expressions.iter().map(|i| { tpl.expressions[*i].take_in(ctx.ast.allocator).into() })), ) .into(), ]) }; let call = ctx.ast.expression_call( SPAN, imports.create_from_template(ctx), NONE, call_expressions, false, ); exprs.push(call); } TemplateNode::Text(text) => { exprs.push(ctx.ast.expression_string_literal(SPAN, ctx.ast.atom(&text), None)); } TemplateNode::Expr(i) => { exprs.push(tpl.expressions[i].take_in(ctx.ast.allocator)); } } } let expr = if exprs.len() > 1 { ctx.ast.expression_array(SPAN, ctx.ast.vec_from_iter(exprs.into_iter().map(|e| e.into()))) } else { exprs.pop().unwrap() }; Ok(CompiledTemplate { decl, expr, strings }) } fn op_codes_into_expression<'a>( op_codes: &[u32], ctx: &mut TraverseCtx<'a>, imports: &mut ImportSymbols<'a>, oveo: bool, ) -> Expression<'a> { if op_codes.is_empty() { imports.empty_array(ctx) } else { let expr = ctx.ast.expression_array( SPAN, ctx.ast.vec_from_iter(op_codes.iter().map(|o| { ctx.ast .expression_numeric_literal(SPAN, *o as f64, None, NumberBase::Decimal) .into() })), ); if oveo { oveo_intrinsic(expr, imports.dedupe(ctx), ctx) } else { expr } } } fn strings_into_expression<'a>( strings: &IndexSet, ctx: &mut TraverseCtx<'a>, ) -> Expression<'a> { ctx.ast.expression_array( SPAN, ctx.ast.vec_from_iter( strings .iter() .map(|s| ctx.ast.expression_string_literal(SPAN, ctx.ast.atom(s), None).into()), ), ) } ================================================ FILE: crates/ivi_compiler/src/tpl/opcodes.rs ================================================ pub mod template_flags { pub const CHILDREN_SIZE_SHIFT: u32 = 6; pub const SVG: u32 = 1 << 12; } pub mod state_op { pub const SAVE: u32 = 0b01; pub const ENTER_OR_REMOVE: u32 = 0b10; pub const OFFSET_SHIFT: u32 = 2; } pub mod common_prop_type { pub const CLASS_NAME: u32 = 0; pub const TEXT_CONTENT: u32 = 1; pub const INNER_HTML: u32 = 2; } pub mod prop_op { pub const SET_NODE: u32 = 0; pub const COMMON: u32 = 1; pub const ATTRIBUTE: u32 = 2; pub const PROPERTY: u32 = 3; pub const DIFF_DOM_PROPERTY: u32 = 4; pub const STYLE: u32 = 5; pub const EVENT: u32 = 6; pub const DIRECTIVE: u32 = 7; pub const TYPE_MASK: u32 = 0b111; pub const INPUT_SHIFT: u32 = 3; pub const DATA_SHIFT: u32 = 9; } pub mod child_op { pub const CHILD: u32 = 0b00; pub const SET_NEXT: u32 = 0b01; pub const SET_PARENT: u32 = 0b11; pub const TYPE: u32 = 0b11; pub const VALUE_SHIFT: u32 = 2; } ================================================ FILE: crates/ivi_compiler/src/tpl/parser.rs ================================================ use oxc_ast::ast::{Expression, TemplateElement, TemplateLiteral}; use oxc_diagnostics::OxcDiagnostic; use oxc_semantic::Scoping; use crate::tpl::html::is_html_void_element; #[derive(Clone, Copy)] pub struct ExprIndex(usize); impl ExprIndex { pub fn inner(self) -> usize { self.0 } } pub struct THoistableExpr { pub index: ExprIndex, pub hoist: bool, } pub struct TNode { pub kind: TNodeKind, pub flags: u8, pub state_index: u16, pub children_exprs: u16, pub props_exprs: u16, } pub enum TNodeKind { Element(TElement), Text(TText), Expr(TExpr), } impl TNode { pub const HAS_EXPRESSIONS: u8 = 1; pub const HAS_NEXT_EXPRESSION: u8 = 1 << 1; pub const HAS_NEXT_DOM_NODE: u8 = 1 << 2; fn new(kind: TNodeKind) -> Self { Self { kind, flags: 0, state_index: 0, children_exprs: 0, props_exprs: 0 } } fn text(text: &str) -> Self { Self { kind: TNodeKind::Text(TText { value: text.to_string() }), flags: 0, state_index: 0, children_exprs: 0, props_exprs: 0, } } fn space_text() -> Self { Self::text(" ") } } pub struct TElement { pub tag: String, pub properties: Vec, pub children: Vec, pub void: bool, } pub struct TText { pub value: String, } pub struct TExpr { pub index: ExprIndex, } pub enum TProperty { Attribute(TPropertyAttribute), Value(TPropertyValue), DOMValue(TPropertyDOMValue), Style(TPropertyStyle), Event(TPropertyEvent), Directive(ExprIndex), } pub struct TPropertyAttribute { pub key: String, pub value: TPropertyAttributeValue, } pub enum TPropertyAttributeValue { String(String), Bool, Expr(THoistableExpr), } pub struct TPropertyValue { pub key: String, pub value: ExprIndex, } pub struct TPropertyDOMValue { pub key: String, pub value: ExprIndex, } pub struct TPropertyStyle { pub key: String, pub value: TPropertyStyleValue, } pub enum TPropertyStyleValue { String(String), Expr(ExprIndex), } pub struct TPropertyEvent { pub key: String, pub value: ExprIndex, } pub fn parse_template<'a>( tpl: &'a TemplateLiteral, scoping: &'a Scoping, ) -> Result, OxcDiagnostic> { let mut parser = Parser::new(scoping, &tpl.quasis, &tpl.expressions); let mut nodes = parser.parse_children_list()?; for n in &mut nodes { update_flags(n); assign_state_slots(n); } Ok(nodes) } #[derive(Debug, Clone)] struct Parser<'a> { scoping: &'a Scoping, quasis: &'a [TemplateElement<'a>], expressions: &'a [Expression<'a>], text: &'a str, expr_cursor: usize, } impl<'a> Parser<'a> { fn new( scoping: &'a Scoping, quasis: &'a [TemplateElement<'a>], expressions: &'a [Expression<'a>], ) -> Self { Self { scoping, quasis, expressions, text: quasis[0].value.cooked.unwrap().as_str(), expr_cursor: 0, } } fn current_element(&self) -> &TemplateElement<'a> { &self.quasis[self.expr_cursor] } fn is_end(&self) -> bool { self.text.is_empty() && self.expr_cursor == self.expressions.len() } fn peek_char(&self) -> Option { self.text.chars().next() } fn peek_nth_char(&self, n: usize) -> Option { self.text.chars().nth(n) } fn try_consume_char(&mut self, expected: char) -> Option { if let Some(c) = self.text.chars().next() && c == expected { self.text = &self.text[expected.len_utf8()..]; Some(expected) } else { None } } fn consume_char(&mut self, expected: char) -> Result<(), OxcDiagnostic> { if let Some(c) = self.text.chars().next() && c == expected { self.text = &self.text[expected.len_utf8()..]; Ok(()) } else { let parts = self.text.split_at(self.text.len().min(10)); Err(OxcDiagnostic::error(format!("Expected a '{expected}' char: {}", parts.0)) .with_label(self.current_element().span)) } } fn advance(&mut self, i: usize) { self.text = &self.text[i..]; } fn consume_expr(&mut self) -> Result { if self.text.is_empty() && (self.expr_cursor) < self.expressions.len() { let i = self.expr_cursor; self.expr_cursor += 1; self.text = self.current_element().value.cooked.unwrap().as_str(); Ok(i) } else { Err(OxcDiagnostic::error("Expected an expression") .with_label(self.current_element().span)) } } fn consume_whitespace(&mut self) -> WhitespaceState { let mut state = 0; let mut len = self.text.len(); for (i, c) in self.text.char_indices() { match c { ' ' | '\t' => { state |= WhitespaceState::WHITESPACE; } '\n' | '\r' => { state |= WhitespaceState::WHITESPACE | WhitespaceState::CONTAINS_NEWLINE; } '\x0b' => { state |= WhitespaceState::WHITESPACE | WhitespaceState::CONTAINS_VERTICAL_TAB; } _ => { len = i; break; } } } if len == 0 { WhitespaceState(0) } else { self.advance(len); WhitespaceState(state) } } fn parse_tag_name(&mut self) -> Result { let mut len = self.text.len(); for (i, c) in self.text.char_indices() { match c { '0'..='9' | 'a'..='z' | 'A'..='Z' | '_' => {} '-' => { if i == 0 { len = 0; break; } } _ => { len = i; break; } } } if len > 0 { let id = self.text[..len].to_string(); self.advance(len); Ok(id) } else { let parts = self.text.split_at(self.text.len().min(10)); Err(OxcDiagnostic::error(format!("Invalid tag name: {}", parts.0)) .with_label(self.current_element().span)) } } fn parse_attribute_name(&mut self) -> Result { let mut len = self.text.len(); for (i, c) in self.text.char_indices() { match c { '0'..='9' | 'a'..='z' | 'A'..='Z' | '_' => {} '-' => { if i == 0 { len = 0; break; } } _ => { len = i; break; } } } if len > 0 { let id = self.text[..len].to_string(); self.advance(len); Ok(id) } else { let parts = self.text.split_at(self.text.len().min(10)); Err(OxcDiagnostic::error(format!("Invalid attribute name: {}", parts.0)) .with_label(self.current_element().span)) } } fn parse_js_property(&mut self) -> Result { let mut len = self.text.len(); for (i, c) in self.text.char_indices() { match c { '0'..='9' | 'a'..='z' | 'A'..='Z' | '_' | '$' => {} _ => { len = i; break; } } } if len > 0 { let id = self.text[..len].to_string(); self.advance(len); Ok(id) } else { let parts = self.text.split_at(self.text.len().min(10)); Err(OxcDiagnostic::error(format!("Invalid property name: {}", parts.0)) .with_label(self.current_element().span)) } } fn parse_style_name(&mut self) -> Result { let mut len = self.text.len(); for (i, c) in self.text.char_indices() { match c { '0'..='9' | 'a'..='z' | 'A'..='Z' | '-' | '_' => {} _ => { len = i; break; } } } if len > 0 { let id = self.text[..len].to_string(); self.advance(len); Ok(id) } else { let parts = self.text.split_at(self.text.len().min(10)); Err(OxcDiagnostic::error(format!("Invalid style name: {}", parts.0)) .with_label(self.current_element().span)) } } fn parse_children_list(&mut self) -> Result, OxcDiagnostic> { let mut children = Vec::new(); let mut whitespace_state = self.consume_whitespace(); while !self.is_end() { if let Some(c) = self.peek_char() { if c == '<' { if whitespace_state.should_insert_whitespace() { children.push(TNode::space_text()); } match self.peek_nth_char(1) { Some('/') => { break; } Some('!') => { self.parse_comment(); } _ => { children.push(TNode::new(TNodeKind::Element(self.parse_element()?))); } } } else { children.push(TNode::new(TNodeKind::Text(self.parse_text(whitespace_state)?))); } } else { let index = self.consume_expr()?; if whitespace_state.should_insert_whitespace() { children.push(TNode::space_text()); } children.push(TNode::new(TNodeKind::Expr(TExpr { index: ExprIndex(index) }))); } whitespace_state = self.consume_whitespace(); } Ok(children) } fn parse_comment(&mut self) { let mut len = self.text.len(); for (i, c) in self.text.char_indices() { if c == '>' { len = i + 1; break; } } if len > 0 { self.advance(len); } } fn parse_element(&mut self) -> Result { self.advance(1); let tag = self.parse_tag_name()?; self.consume_whitespace(); let properties = self.parse_attributes()?; let mut children = Vec::new(); let mut void = false; if self.try_consume_char('/').is_some() { self.consume_char('>')?; } else { self.consume_char('>')?; if is_html_void_element(&tag) { void = true; } else { children = self.parse_children_list()?; self.consume_char('<')?; self.consume_char('/')?; self.advance(tag.len()); self.consume_whitespace(); self.consume_char('>')?; } } Ok(TElement { tag, properties, children, void }) } fn parse_text(&mut self, whitespace_state: WhitespaceState) -> Result { let mut text = String::new(); let mut len = self.text.len(); let mut whitespace_state = whitespace_state; for (i, c) in self.text.char_indices() { match c { '<' => { len = i; break; } ' ' | '\t' => { whitespace_state.0 |= WhitespaceState::WHITESPACE; continue; } '\n' | '\r' => { whitespace_state.0 |= WhitespaceState::CONTAINS_NEWLINE; continue; } '\x0b' => { whitespace_state.0 |= WhitespaceState::CONTAINS_VERTICAL_TAB; continue; } _ => {} } if whitespace_state.0 & WhitespaceState::WHITESPACE != 0 && (whitespace_state.0 & (WhitespaceState::TEXT_CONTENT | WhitespaceState::CONTAINS_VERTICAL_TAB) != 0 || whitespace_state.0 & WhitespaceState::CONTAINS_NEWLINE == 0) { text.push(' '); } whitespace_state.0 = WhitespaceState::TEXT_CONTENT; text.push(c); } if whitespace_state.should_insert_whitespace() { text.push(' '); } self.advance(len); if text.len() <= (1 << 16) { Ok(TText { value: text }) } else { // Text nodes are splitted into two nodes when they exceed their length limit (64k). // https://github.com/chromium/chromium/blob/91159249db3086f17b28b7a060f55ec0345c24c7/third_party/blink/renderer/core/dom/text.h#L42 Err(OxcDiagnostic::error("Text is too long (>64Kb)") .with_label(self.current_element().span)) } } fn parse_attributes(&mut self) -> Result, OxcDiagnostic> { let mut properties = Vec::new(); while !self.is_end() { if let Some(c) = self.peek_char() { match c { '/' | '>' => { return Ok(properties); } '.' => { self.advance(1); let key = self.parse_js_property()?; self.consume_char('=')?; let expr_index = self.consume_expr()?; properties.push(TProperty::Value(TPropertyValue { key, value: ExprIndex(expr_index), })); } '*' => { self.advance(1); let key = self.parse_js_property()?; self.consume_char('=')?; let expr_index = self.consume_expr()?; properties.push(TProperty::DOMValue(TPropertyDOMValue { key, value: ExprIndex(expr_index), })); } '@' => { self.advance(1); let key = self.parse_js_property()?; self.consume_char('=')?; let expr_index = self.consume_expr()?; properties.push(TProperty::Event(TPropertyEvent { key, value: ExprIndex(expr_index), })); } '~' => { self.advance(1); let key = self.parse_style_name()?; self.consume_char('=')?; let value = if self.peek_char().is_some() { TPropertyStyleValue::String(self.parse_attribute_string()?) } else { TPropertyStyleValue::Expr(ExprIndex(self.consume_expr()?)) }; properties.push(TProperty::Style(TPropertyStyle { key, value })); } _ => { let key = self.parse_attribute_name()?; let mut hoist = false; let value; if self.try_consume_char('=').is_some() { if let Some('"') = self.peek_char() { value = TPropertyAttributeValue::String(self.parse_attribute_string()?); } else { let expr_index = self.consume_expr()?; if key == "class" { let expr = &self.expressions[expr_index]; // Hoist symbols from the root scope if is_hoistable_expr(expr, self.scoping) { hoist = true; } } value = TPropertyAttributeValue::Expr(THoistableExpr { index: ExprIndex(expr_index), hoist, }); } } else { value = TPropertyAttributeValue::Bool; } properties.push(TProperty::Attribute(TPropertyAttribute { key, value })); } } } else { properties.push(TProperty::Directive(ExprIndex(self.consume_expr()?))) } self.consume_whitespace(); } let parts = self.text.split_at(self.text.len().min(10)); Err(OxcDiagnostic::error(format!("Expected a '>' char: {}", parts.0)) .with_label(self.current_element().span)) } fn parse_attribute_string(&mut self) -> Result { let delim; let mut chars = self.text.char_indices(); if let Some((_, c)) = chars.next() { match c { '\'' | '"' => { delim = c; } _ => { return Err(OxcDiagnostic::error( "Invalid string value, it should start with '\"' char.", ) .with_label(self.current_element().span)); } } for (i, c) in chars { if c == delim { let v = self.text[1..i].to_string(); self.advance(i + 1); return Ok(v); } } Err(OxcDiagnostic::error("Invalid string value, it should end with '\"' char.") .with_label(self.current_element().span)) } else { Err(OxcDiagnostic::error("Invalid string value") .with_label(self.current_element().span)) } } } #[derive(Clone, Copy)] struct WhitespaceState(u8); impl WhitespaceState { const WHITESPACE: u8 = 1; const CONTAINS_NEWLINE: u8 = 1 << 1; const CONTAINS_VERTICAL_TAB: u8 = 1 << 2; const TEXT_CONTENT: u8 = 1 << 3; fn should_insert_whitespace(self) -> bool { self.0 & WhitespaceState::WHITESPACE != 0 && (self.0 & WhitespaceState::CONTAINS_NEWLINE == 0 || self.0 & WhitespaceState::CONTAINS_VERTICAL_TAB != 0) } } // RTL pass fn update_flags(node: &mut TNode) { _update_flags(node, 0); } fn _update_flags(node: &mut TNode, flags: u8) -> u8 { let mut flags = flags; if let TNodeKind::Element(e) = &mut node.kind { let mut props_exprs = 0; for p in &e.properties { match p { TProperty::Attribute(p) => { if let TPropertyAttributeValue::Expr(e) = &p.value && !e.hoist { props_exprs += 1; break; } } TProperty::Style(p) => { if let TPropertyStyleValue::Expr(_) = &p.value { props_exprs += 1; break; } } TProperty::Value(_) | TProperty::DOMValue(_) | TProperty::Event(_) | TProperty::Directive(_) => { props_exprs += 1; break; } } } let mut siblings_flags = 0; let mut children_exprs = 0; for c in e.children.iter_mut().rev() { match &mut c.kind { TNodeKind::Element(_) => { let f = _update_flags(c, siblings_flags); if f & TNode::HAS_EXPRESSIONS != 0 { flags |= TNode::HAS_EXPRESSIONS; siblings_flags |= TNode::HAS_NEXT_EXPRESSION | TNode::HAS_NEXT_DOM_NODE; } else { siblings_flags |= TNode::HAS_NEXT_DOM_NODE; } } TNodeKind::Text(_) => { c.flags = siblings_flags; siblings_flags |= TNode::HAS_NEXT_DOM_NODE; } TNodeKind::Expr(_) => { siblings_flags |= TNode::HAS_NEXT_EXPRESSION; c.flags = siblings_flags; children_exprs += 1; } } } if props_exprs > 0 || children_exprs > 0 { flags |= TNode::HAS_EXPRESSIONS; } node.flags = flags; node.props_exprs = props_exprs; node.children_exprs = children_exprs; } flags } fn assign_state_slots(node: &mut TNode) { _assign_state_slots(node, 1); } fn _assign_state_slots(node: &mut TNode, mut state_index: u16) -> u16 { if let TNodeKind::Element(e) = &mut node.kind { let mut prev_expr = false; for c in &mut e.children { match c.kind { TNodeKind::Element(_) => { if prev_expr { prev_expr = false; c.state_index = state_index; state_index += 1; } else if c.props_exprs > 0 || c.children_exprs > 0 { c.state_index = state_index; state_index += 1; } state_index = _assign_state_slots(c, state_index); } TNodeKind::Text(_) => { if prev_expr { prev_expr = false; c.state_index = state_index; state_index += 1; } } TNodeKind::Expr(_) => { prev_expr = true; } } } } state_index } fn is_hoistable_expr<'a>(expr: &Expression<'a>, scoping: &Scoping) -> bool { match expr { Expression::Identifier(id) => { let r = scoping.get_reference(id.reference_id()); if let Some(symbol_id) = r.symbol_id() && scoping.symbol_scope_id(symbol_id) == scoping.root_scope_id() { return true; } } Expression::StaticMemberExpression(expr) => { return is_hoistable_expr(&expr.object, scoping); } _ => {} } false } ================================================ FILE: docs/internals/dynamic-lists.md ================================================ # Dynamic Lists Just some reminders: ## Lazy rendering It may seem like a good idea to render dynamic lists lazily and even avoid storing keys in memory, but it may lead to some subtle bugs with mutable entries. ## Dynamic Lists with Immutable Entries It is possible to create stateless node for immutable lists by storing entries, key function and render function in a stateless node. On initial mount we will need to invoke render function for each entry and keys can be completely ignored. And when dynamic list is updated, we can lazily recover old keys from previous entries. Don't think that it is worth it in most real-world scenarios, and we can always just memoize stateless node with dynamic list in a component state. ================================================ FILE: docs/internals/misc.md ================================================ # Misc ## Type Casting The current code base contains a lot of type casting. In idiomatic typescript it should be implemented with type guards, and if javascript toolchains supported inlining in some reliable way instead of relying on their heuristics, the code could be way much cleaner with type guard functions. ## Root Entry Functions Entry functions (update, dirtyCheck, unmount, etc) should save and restore render context to avoid edge cases when they invoked synchronously in a different root context. ================================================ FILE: docs/internals/perf.md ================================================ # Perf ## `var` vs `let` In some places variables are declared with `var` instead of `let`, it is a micro optimization that propably won't have any significant impact on performance, especially when the JIT kicks in. ```js function __var(i) { var j = i; return function _var() { return j; } } function __let(i) { let j = i; return function _let() { return j; }; } ``` In the example above, `_var` function will have the following bytecode in V8: ```txt LdaImmutableCurrentContextSlot [2] Return ``` And `_let` function: ```txt LdaImmutableCurrentContextSlot [2] ThrowReferenceErrorIfHole[0]; Return ``` ## `if (a === true) {}` vs `if (a) {}` In a lot of places there are explicit strict equality checks to avoid `toBool()` coercion. Sometimes we can avoid explicit checks when JIT compiler is going to inline functions and will be able to eliminate `toBool()` coercion. For example, `_isArray()` calls doesn't use strict equality checks. `(a === true)` ```txt 0x738b024 24 488b5518 REX.W movq rdx, [rbp + 0x18]; 0x738b028 28 493995b0000000 REX.W cmpq[r13 + 0xb0](root(true_value)), rdx; 0x738b02f 2f 0f8424000000 jz 0x738b059 < +0x59 > 0x738b035 35 48b80000000014000000 REX.W movq rax, 0x1400000000; 0x738b03f 3f 488b4de8 REX.W movq rcx, [rbp - 0x18]; 0x738b043 43 488be5 REX.W movq rsp, rbp; 0x738b046 46 5d pop rbp; 0x738b047 47 4883f902 REX.W cmpq rcx, 0x2; 0x738b04b 4b 7f03 jg 0x738b050 < +0x50 > 0x738b04d 4d c21000 ret 0x10; `(a)`, inlines `toBool()` 0x618b024 24 488b5518 REX.W movq rdx, [rbp + 0x18]; 0x618b028 28 f6c201 testb rdx, 0x1; 0x618b02b 2b 0f84a7000000 jz 0x618b0d8 < +0xd8 > 0x618b031 31 493995b8000000 REX.W cmpq[r13 + 0xb8](root(false_value)), rdx; 0x618b038 38 0f8459000000 jz 0x618b097 < +0x97 > 0x618b03e 3e 493995c0000000 REX.W cmpq[r13 + 0xc0](root(empty_string)), rdx; 0x618b045 45 0f844c000000 jz 0x618b097 < +0x97 > 0x618b04b 4b 488b4aff REX.W movq rcx, [rdx - 0x1]; 0x618b04f 4f f6410d10 testb[rcx + 0xd], 0x10; 0x618b053 53 0f853e000000 jnz 0x618b097 < +0x97 > 0x618b059 59 49398d38010000 REX.W cmpq[r13 + 0x138](root(heap_number_map)), rcx; 0x618b060 60 0f8484000000 jz 0x618b0ea < +0xea > 0x618b066 66 49398db8010000 REX.W cmpq[r13 + 0x1b8](root(bigint_map)), rcx; 0x618b06d 6d 0f846c000000 jz 0x618b0df < +0xdf > 0x618b073 73 48b8000000000a000000 REX.W movq rax, 0xa00000000; 0x618b07d 7d 488b4de8 REX.W movq rcx, [rbp - 0x18]; 0x618b081 81 488be5 REX.W movq rsp, rbp; 0x618b084 84 5d pop rbp; 0x618b085 85 4883f902 REX.W cmpq rcx, 0x2; 0x618b089 89 7f03 jg 0x618b08e < +0x8e > 0x618b08b 8b c21000 ret 0x10; ``` ## Polymorphic Call-Sites It is not always a good idea to optimize for monomorphic call-sites. If there is a low degree polymorphism, it can be better to use different shapes. In some cases compiler can optimize several polymorphic call-sites and perform just one shape check. To understand how to reorganize code, so that compiler could better optimize it, it is necessary to understand aliasing: https://en.wikipedia.org/wiki/Aliasing_(computing) ## Additional Resources - https://v8.dev/docs/turbofan - https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html - https://benediktmeurer.de/2017/06/29/javascript-optimization-patterns-part2/ - https://github.com/thlorenz/v8-perf/blob/master/language-features.md ================================================ FILE: docs/internals/template-compiler.md ================================================ # Template Compiler ## Markers Comment markers `` should be inserted into template to delineate a slot position for an expression when it is inserted between two text nodes. E.g.
prefix${expr}suffix
Should produce the following HTML template:
prefixsuffix
## OpCodes When properties `PropOpCode` are starting to update: - currentNode is assigned to template root node - properties are updated starting from the root node Because of this invariants we can avoid adding SetNode opCode for root nodes. When children `ChildOpCode` are starting to update: - parentNode is assigned to root node - nextNode is assigned to null Because of this invariants we can avoid generating state and children opcodes in cases like: ```html
${expr}
``` Here we don't need to traverse DOM tree when mounting, because we already know its root node and dynamic child is positioned in the end. So, `stateOpCodes` should be empty, and `childOpCodes` should have only `UpdateChild` opCode. ## Ideas ### SMI Arrays vs Strings for OpCodes It is possible to encode OpCodes as strings and get deduplication via string interning. OpCodes encoded as strings should also have smaller size and faster to parse. But overall it is probably not worth it. ### Optimize PropOpCode encoding to reduce code size In majority of templates, the size of expressions array, state array, etc is lower than 16. So instead of storing indexes in a contiguous set of bits, we can store lowest bits in the first 4 bits and the rest in the highest bits, e.g.: ```txt PropOpCode { type:3, expr_lo:4, data_lo:4, expr_hi:6, data_hi:.., } expr = ((op >> 3) & Mask4) | ((op >> 11) & Mask6); data = ((op >> 7) & Mask4) | (op >> 17); ``` And since we are deduplicating all data and storing it in a shared array, we can sort it by the number of occurences in templates, so that indices for the 16 most common keys will be able to fit into 4 bits. ### Store string length in StateOpCodes to separate static text nodes It is possible to avoid injecting `` comment nodes to separate static strings by replacing remove opCode with split opCode that is going to store string length. ================================================ FILE: docs/misc/migrating-from-react.md ================================================ # Migrating From React This document shows how to rewrite examples from the https://react.dev/learn/ documentation with ivi API. ## Table of Contents - [Describing the UI](#describing-the-ui) - [Your first component](#your-first-component) - [Importing and exporting component](#importing-and-exporting-components) - [Writing markup with JSX](#writing-markup-with-jsx) - [Javascript in JSX with curly braces](#javascript-in-jsx-with-curly-braces) - [Passing props to components](#passing-props-to-a-component) - [Conditional rendering](#conditional-rendering) - [Rendering lists](#rendering-lists) - [Keeping components pure](#keeping-components-pure) - [Adding interactivity](#adding-interactivity) - [Responding to events](#responding-to-events) - [State: a component's memory](#state-a-components-memory) - [Updating objects in state](#updating-objects-in-state) - [Managing state](#managing-state) - [Reacting to input with state](#reacting-to-input-with-state) - [Choosing the state structure](#choosing-the-state-structure) - [Preserving and resetting state](#preserving-and-resetting-state) - [Extracting state logic into a reducer](#extracting-state-logic-into-a-reducer) - [Passing data deeply with context](#passing-data-deeply-with-context) - [Escape hatches](#escape-hatches) - [Referencing values with refs](#referencing-values-with-refs) - [Manipulating the DOM with refs](#manipulating-the-dom-with-refs) - [Synchronizing with Effects](#synchronizing-with-effects) - [Removing Effect dependencies](#removing-effect-dependencies) - [Reusing logic with custom hooks](#reusing-logic-with-custom-hooks) ## [Describing the UI](https://react.dev/learn/describing-the-ui) ### [Your first component](https://react.dev/learn/describing-the-ui#your-first-component) React: ```js function Profile() { return ( Katherine Johnson ); } export default function Gallery() { return (

Amazing scientists

); } ``` ivi: ```js import { html } from 'ivi'; const Profile = () => html` Katherine Johnson `; const Gallery = () => html`

Amazing scientists

${Profile()} ${Profile()} ${Profile()}
`; export default Gallery; ``` ### [Importing and exporting components](https://react.dev/learn/describing-the-ui#importing-and-exporting-components) React: ```js import Profile from './Profile.js'; export default function Gallery() { return (

Amazing scientists

); } ``` ivi: ```js import { html } from 'ivi'; import Profile from './Profile.js'; const Gallery = () => html` section h1 'Amazing scientists' ${Profile()} ${Profile()} ${Profile()} `; export default Gallery; ``` ### [Writing markup with JSX](https://react.dev/learn/describing-the-ui#writing-markup-with-jsx) React: ```js export default function TodoList() { return ( <>

Hedy Lamarr's Todos

Hedy Lamarr
  • Invent new traffic lights
  • Rehearse a movie scene
  • Improve spectrum technology
); } ``` ivi: ```js import { html } from 'ivi'; const TodoList = () => html`

Hedy Lamarr's Todos

Hedy Lamarr
  • Invent new traffic lights
  • Rehearse a movie scene
  • Improve spectrum technology
`; export default TodoList; ``` ### [JavaScript in JSX with curly braces](https://react.dev/learn/describing-the-ui#javascript-in-jsx-with-curly-braces) React: ```js const person = { name: 'Gregorio Y. Zara', theme: { backgroundColor: 'black', color: 'pink' } }; export default function TodoList() { return (

{person.name}'s Todos

Gregorio Y. Zara
  • Improve the videophone
  • Prepare aeronautics lectures
  • Work on the alcohol-fuelled engine
); } ``` ivi: ```js const person = { name: 'Gregorio Y. Zara', theme: { backgroundColor: 'black', color: 'pink' } }; const TodoList = () => html`

${person.name}'s Todos

Gregorio Y. Zara
  • Improve the videophone
  • Prepare aeronautics lectures
  • Work on the alcohol-fuelled engine
`; export default TodoList; ``` ### [Passing props to a component](https://react.dev/learn/describing-the-ui#passing-props-to-a-component) React: ```jsx import { getImageUrl } from './utils.js' export default function Profile() { return ( ); } ``` ivi: ```js import { getImageUrl } from './utils.js' const Profile = () => ( Card( Avatar({ size: 100, person: { name: 'Katsuko Saruhashi', imageId: 'YfeOqp2' }, }), ) ); export default Profile; ``` ### [Conditional Rendering](https://react.dev/learn/describing-the-ui#conditional-rendering) React: ```js function Item({ name, isPacked }) { return (
  • {name} {isPacked && '✔'}
  • ); } export default function PackingList() { return (

    Sally Ride's Packing List

    ); } ``` ivi: ```js import { html } from "ivi"; const Item = ({ name, isPacked }) => html`
  • ${name} ${isPacked && '✔'}
  • `; const PackingList = () => html`

    Sally Ride's Packing List

      ${Item({ isPacked: true, name: 'Space suit' })} ${Item({ isPacked: true, name: 'Helmet with a golden leaf' })} ${Item({ isPacked: false, name: 'Photo of Tam' })}
    `; export default PackingList; ``` ### [Rendering lists](https://react.dev/learn/describing-the-ui#rendering-lists) React: ```js import { people } from './data.js'; import { getImageUrl } from './utils.js'; export default function List() { const listItems = people.map(person =>
  • {person.name}

    {person.name}: {' ' + person.profession + ' '} known for {person.accomplishment}

  • ); return (

    Scientists

      {listItems}
    ); } ``` ivi: ```js import { List } from 'ivi'; import { html } from 'ivi'; import { people } from './data.js'; import { getImageUrl } from './utils.js'; const ScientistsList = () => html`

    Scientists

      ${List(people, (person) => person.id, (person) => html`
    • ${person.name}

      ${person.name}: \v ${person.profession} \v known for ${person.accomplishment}

    • `)}
    `; export default ScientistsList; ``` ### [Keeping components pure](https://react.dev/learn/describing-the-ui#keeping-components-pure) React: ```js function Cup({ guest }) { return

    Tea cup for guest #{guest}

    ; } export default function TeaSet() { return ( <> ); } ``` ivi: ```js import { html } from 'ivi'; const Cup = ({ guest }) => html`

    Tea cup for guest #${guest}

    `; const TeaSet = () => [ Cup({ guest: 1 }), Cup({ guest: 2 }), Cup({ guest: 3 }), ]; export default TeaSet; ``` ## [Adding Interactivity](https://react.dev/learn/adding-interactivity) ### [Responding to events](https://react.dev/learn/adding-interactivity#responding-to-events) React: ```js export default function App() { return ( alert('Playing!')} onUploadImage={() => alert('Uploading!')} /> ); } function Toolbar({ onPlayMovie, onUploadImage }) { return (
    ); } function Button({ onClick, children }) { return ( ); } ``` ivi: ```js import { component, useState } from 'ivi'; import { html } from 'ivi'; const App = component((c) => { const onPlayMovie = () => alert('Playing!'); const onUploadImage = () => alert('Uploading!'); return () => ToolBar({ onPlayMovie, onUploadImage }); }); export default App; const Toolbar = ({ onPlayMovie, onUploadImage }) => html`
    ${Button({ onClick: onPlayMovie, children: 'Play Movie' })} ${Button({ onClick: onUploadImage, children: 'Upload Image' })}
    `; const Button = ({ onClick, children }) => html` `; ``` ### [State: a component’s memory](https://react.dev/learn/adding-interactivity#state-a-components-memory) React: ```js import { useState } from 'react'; import { sculptureList } from './data.js'; export default function Gallery() { const [index, setIndex] = useState(0); const [showMore, setShowMore] = useState(false); function handleNextClick() { setIndex(index + 1); } function handleMoreClick() { setShowMore(!showMore); } let sculpture = sculptureList[index]; return ( <>

    {sculpture.name} by {sculpture.artist}

    ({index + 1} of {sculptureList.length})

    {showMore &&

    {sculpture.description}

    } {sculpture.alt} ); } ``` ivi: ```js import { component, useState } from 'ivi'; import { html } from 'ivi'; import { sculptureList } from './data.js'; const Gallery = () => { const [index, setIndex] = useState(0); const [showMore, setShowMore] = useState(false); function handleNextClick() { setIndex(index + 1); } function handleMoreClick() { setShowMore(!showMore); } return () => { const index = index(); const showMore = showMore(); const sculpture = sculptureList[index]; return html`

    ${sculpture.name} \v by ${sculpture.artist}

    (${index + 1} of ${sculptureList.length}

    ${showMore && html`

    ${sculpture.description}

    `} ${sculpture.alt} `; }; } export default Gallery; ``` ### [State as a snapshot](https://react.dev/learn/adding-interactivity#state-as-a-snapshot) React: ```js console.log(count); // 0 setCount(count + 1); // Request a re-render with 1 console.log(count); // Still 0! ``` ivi: Like regular javascript variables, ivi state is updated immediately. ```js console.log(count()); // 0 setCount(count() + 1); // Request a re-render with 1 console.log(count()); // 1 ``` ### [Updating objects in state](https://react.dev/learn/adding-interactivity#updating-objects-in-state) React: ```js import { useState } from 'react'; export default function Form() { const [person, setPerson] = useState({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', } }); function handleNameChange(e) { setPerson({ ...person, name: e.target.value }); } function handleTitleChange(e) { setPerson({ ...person, artwork: { ...person.artwork, title: e.target.value } }); } function handleCityChange(e) { setPerson({ ...person, artwork: { ...person.artwork, city: e.target.value } }); } function handleImageChange(e) { setPerson({ ...person, artwork: { ...person.artwork, image: e.target.value } }); } return ( <>

    {person.artwork.title} {' by '} {person.name}
    (located in {person.artwork.city})

    {person.artwork.title} ); } ``` ivi: ```js import { component, useState } from 'ivi'; import { html } from 'ivi'; const Form = component((c) => { const [person, setPerson] = useState(c, { name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', } }); function handleNameChange(e) { setPerson({ ...person(), name: e.target.value }); } function handleTitleChange(e) { const p = person(); setPerson({ ...p, artwork: { ...p.artwork, title: e.target.value } }); } function handleCityChange(e) { const p = person(); setPerson({ ...p, artwork: { ...p.artwork, city: e.target.value } }); } function handleImageChange(e) { const p = person(); setPerson({ ...p, artwork: { ...p.artwork, image: e.target.value } }); } return () => { const p = person(); return html`

    ${p.artwork.title} \v by ${p.name}
    (located in ${p.artwork.city} )

    ${p.artwork.title} `; }; }); export default Form; ``` ## [Managing State](https://react.dev/learn/managing-state) ### [Reacting to input with state](https://react.dev/learn/managing-state#reacting-to-input-with-state) React: ```js import { useState } from 'react'; export default function Form() { const [answer, setAnswer] = useState(''); const [error, setError] = useState(null); const [status, setStatus] = useState('typing'); if (status === 'success') { return

    That's right!

    } async function handleSubmit(e) { e.preventDefault(); setStatus('submitting'); try { await submitForm(answer); setStatus('success'); } catch (err) { setStatus('typing'); setError(err); } } function handleTextareaChange(e) { setAnswer(e.target.value); } return ( <>

    City quiz

    In which city is there a billboard that turns air into drinkable water?