├── .devcontainer
├── Dockerfile
└── devcontainer.json
├── .gitattributes
├── .github
├── CLA.md
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug.yml
│ ├── config.yml
│ └── spec.yml
├── labeler.yml
├── pull_request_template.md
├── tools
│ ├── browser_path.ts
│ ├── bump_versions.ts
│ ├── expand_glob.ts
│ ├── fill_github_scopes.ts
│ ├── generate_mermaid.ts
│ ├── github_actions_deno_deploy.ts
│ ├── mod_html_to_readme_md.ts
│ └── vendor_imports.ts
└── workflows
│ ├── ci.yml
│ ├── issue.yml
│ ├── pr.yml
│ └── stale.yml
├── .gitignore
├── .npmrc
├── @mizu
├── bind
│ ├── README.md
│ ├── boolean.ts
│ ├── boolean_test.ts
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── clean
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── code
│ ├── README.md
│ ├── build.ts
│ ├── deno.jsonc
│ ├── mapping.json
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── custom-element
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── eval
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── event
│ ├── README.md
│ ├── deno.jsonc
│ ├── keyboard.ts
│ ├── keyboard_test.ts
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── for
│ ├── README.md
│ ├── deno.jsonc
│ ├── empty
│ │ ├── README.md
│ │ ├── mod.html
│ │ ├── mod.ts
│ │ ├── mod_test.html
│ │ └── mod_test.ts
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ ├── mod_test.ts
│ ├── parse.ts
│ └── parse_test.ts
├── html
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── http
│ ├── README.md
│ ├── deno.jsonc
│ ├── event
│ │ ├── README.md
│ │ ├── mod.html
│ │ ├── mod.ts
│ │ ├── mod_test.html
│ │ └── mod_test.ts
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── if
│ ├── README.md
│ ├── deno.jsonc
│ ├── else
│ │ ├── README.md
│ │ ├── mod.html
│ │ ├── mod.ts
│ │ ├── mod_test.html
│ │ └── mod_test.ts
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── internal
│ ├── README.md
│ ├── deno.jsonc
│ ├── engine
│ │ ├── directive.ts
│ │ ├── directive_test.ts
│ │ ├── mod.ts
│ │ ├── mod_test.ts
│ │ ├── phase.ts
│ │ ├── phase_test.ts
│ │ ├── renderer.ts
│ │ └── renderer_test.ts
│ ├── extras
│ │ ├── evaluate.ts
│ │ └── evaluate_test.ts
│ ├── testing
│ │ ├── filter.ts
│ │ ├── filter_test.ts
│ │ ├── fixtures
│ │ │ ├── markup_fmt-v0.13.1.wasm
│ │ │ └── mod_test.html
│ │ ├── format.ts
│ │ ├── format_test.ts
│ │ ├── mod.ts
│ │ ├── mod_test.ts
│ │ ├── test.ts
│ │ └── test_test.ts
│ └── vdom
│ │ ├── jsdom.ts
│ │ ├── jsdom_test.ts
│ │ ├── mod.ts
│ │ └── mod_test.ts
├── is
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── markdown
│ ├── README.md
│ ├── build.ts
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── mizu
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── model
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── mustache
│ ├── README.md
│ ├── capture.ts
│ ├── capture_test.ts
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── once
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── ref
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── refresh
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── render
│ ├── README.md
│ ├── client
│ │ ├── client.ts
│ │ ├── client_test.ts
│ │ ├── defaults.ts
│ │ ├── defaults_test.ts
│ │ ├── mod.ts
│ │ └── mod_test.ts
│ ├── deno.jsonc
│ └── server
│ │ ├── defaults.ts
│ │ ├── defaults_test.ts
│ │ ├── generate.ts
│ │ ├── generate_test.ts
│ │ ├── mod.ts
│ │ ├── mod_test.ts
│ │ ├── server.ts
│ │ └── server_test.ts
├── set
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── show
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── skip
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── test
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── text
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── toc
│ ├── README.md
│ ├── deno.jsonc
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
└── unstable
│ ├── README.md
│ ├── deno.jsonc
│ └── noop
│ ├── README.md
│ ├── mod.html
│ ├── mod.ts
│ ├── mod_test.html
│ └── mod_test.ts
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── deno.jsonc
├── deno.lock
├── package-lock.json
├── package.json
├── tsconfig.json
└── www
├── api
└── build.ts
├── build.ts
├── html
├── _
│ ├── footer.html
│ ├── header.html
│ ├── matcha-banner.html
│ └── mizu-banner.html
├── build.html
├── community.html
├── index.html
├── mizu
│ ├── api.html
│ ├── concepts.html
│ ├── demo.html
│ ├── directives.html
│ ├── examples
│ │ ├── browsers
│ │ │ ├── esm.html
│ │ │ └── iife.html
│ │ ├── bun
│ │ │ ├── ssg.ts
│ │ │ └── ssr.ts
│ │ ├── deno
│ │ │ ├── ssg.ts
│ │ │ └── ssr.ts
│ │ ├── node
│ │ │ ├── ssg.mjs
│ │ │ └── ssr.mjs
│ │ └── playground
│ │ │ └── example.html
│ ├── faq.html
│ ├── features.html
│ ├── logo.html
│ ├── tbd.html
│ └── usage.html
└── playground.html
├── misc
└── rendering.mermaid
├── serve.ts
├── static
├── 180x180.png
├── 32x32.png
├── components
│ ├── mizu-directive.html
│ ├── mizu-example.html
│ ├── mizu-import.html
│ ├── mizu-modifier.html
│ ├── mizu-note.html
│ ├── mizu-restriction.html
│ ├── mizu-tbd.html
│ ├── mizu-variable.html
│ └── mizu-warn.html
├── demo_browser.png
├── logo.png
├── minu.png
├── og.png
├── rendering.svg
├── styles.css
└── waves.svg
└── tools.ts
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/devcontainers/base:debian
2 |
3 | # Install deno
4 | ENV DENO_INSTALL=/deno
5 | RUN mkdir -p /deno \
6 | && curl -fsSL https://deno.land/install.sh | sh \
7 | && chown -R vscode /deno
8 |
9 | ENV PATH=${DENO_INSTALL}/bin:${PATH} \
10 | DENO_DIR=${DENO_INSTALL}/.cache/deno
11 |
12 | RUN deno upgrade
13 |
14 | # Install Node.js
15 | RUN apt-get update \
16 | && apt-get install -y curl \
17 | && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
18 | && apt-get install -y nodejs
19 |
20 | # Install Bun
21 | ENV BUN_INSTALL=/bun
22 | RUN curl -fsSL https://bun.sh/install | bash \
23 | && chown -R vscode /bun
24 | ENV PATH=${BUN_INSTALL}/bin:${PATH}
25 |
26 | # Setup Chrome
27 | RUN apt-get update \
28 | && apt-get install -y wget gnupg \
29 | && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
30 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
31 | && apt-get update \
32 | && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 --no-install-recommends \
33 | && rm -rf /var/lib/apt/lists/*
34 |
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Deno",
3 | "build": {
4 | "dockerfile": "Dockerfile"
5 | },
6 | "customizations": {
7 | "vscode": {
8 | "settings": {
9 | "editor.defaultFormatter": "denoland.vscode-deno",
10 | "deno.lint": true,
11 | "deno.enable": true
12 | },
13 | "extensions": [
14 | "ms-azuretools.vscode-docker",
15 | "denoland.vscode-deno",
16 | "GitHub.vscode-github-actions",
17 | "GitHub.copilot",
18 | "GitHub.copilot-chat"
19 | ]
20 | },
21 | "remoteUser": "vscode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
4 | .devcontainer/* -linguist-detectable
5 | .github/* -linguist-detectable
6 | *.html linguist-documentation
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: lowlighter
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F41B Bug Report"
2 | description: File a bug report.
3 | labels:
4 | - bug
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: Thank you for taking the time to report a bug!
9 | - type: dropdown
10 | id: scope
11 | attributes:
12 | label: Scope
13 | description: Select the scope(s) of the issue.
14 | multiple: true
15 | options:
16 | - bind
17 | - clean
18 | - code
19 | - custom-element
20 | - eval
21 | - event
22 | - for
23 | - html
24 | - http
25 | - if
26 | - internal
27 | - is
28 | - markdown
29 | - mizu
30 | - model
31 | - mustache
32 | - once
33 | - ref
34 | - refresh
35 | - render
36 | - repo
37 | - set
38 | - show
39 | - skip
40 | - test
41 | - text
42 | - toc
43 | - unstable
44 | - www
45 | validations:
46 | required: true
47 | - type: dropdown
48 | id: environment
49 | attributes:
50 | label: Environment
51 | description: Select the environment(s) where you encountered the issue.
52 | multiple: true
53 | options:
54 | - chromium
55 | - firefox
56 | - safari
57 | - node
58 | - deno
59 | - bun
60 | - linux
61 | - macos
62 | - windows
63 | - other
64 | validations:
65 | required: true
66 | - type: textarea
67 | id: content
68 | attributes:
69 | label: What is happening?
70 | description: Describe the issue and what you expected to happen.
71 | placeholder: |
72 | Provide a detailed description of the bug.
73 | Include screenshots, logs, or any other relevant information.
74 | validations:
75 | required: true
76 | - type: textarea
77 | id: reproduction
78 | attributes:
79 | label: Minimal reproduction example
80 | description: Describe how to reproduce the issue.
81 | placeholder: >
82 | Provide a minimal example to reproduce the issue.
83 |
84 | Failure to do so may result in the issue being closed without
85 | investigation.
86 | render: html
87 | validations:
88 | required: true
89 | - type: dropdown
90 | id: i-did-my-homework
91 | attributes:
92 | label: I have searched for existing issues
93 | description: You confirm that you took the time to search for existing issues.
94 | options:
95 | - ""
96 | - "Yes"
97 | validations:
98 | required: true
99 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/spec.yml:
--------------------------------------------------------------------------------
1 | name: "\U0001F4DC Spec definition"
2 | description: Propose a new specification.
3 | labels:
4 | - spec
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: Thank you for taking the time to propose a new specification!
9 | - type: dropdown
10 | id: scope
11 | attributes:
12 | label: Scope
13 | description: >-
14 | Select the scope(s) of the issue. To propose a new directive, use the
15 | `unstable` scope.
16 | multiple: true
17 | options:
18 | - bind
19 | - clean
20 | - code
21 | - custom-element
22 | - eval
23 | - event
24 | - for
25 | - html
26 | - http
27 | - if
28 | - internal
29 | - is
30 | - markdown
31 | - mizu
32 | - model
33 | - mustache
34 | - once
35 | - ref
36 | - refresh
37 | - render
38 | - repo
39 | - set
40 | - show
41 | - skip
42 | - test
43 | - text
44 | - toc
45 | - unstable
46 | - www
47 | validations:
48 | required: true
49 | - type: textarea
50 | id: content
51 | attributes:
52 | label: What do you want to define?
53 | description: Describe the specification you would like to see added.
54 | placeholder: |
55 | Provide a detailed description of the specification.
56 | Include diagrams, examples, or any other relevant information.
57 | validations:
58 | required: true
59 | - type: dropdown
60 | id: i-did-my-homework
61 | attributes:
62 | label: I have searched for existing issues
63 | description: You confirm that you took the time to search for existing issues.
64 | options:
65 | - ""
66 | - "Yes"
67 | validations:
68 | required: true
69 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 |
20 |
--------------------------------------------------------------------------------
/.github/tools/browser_path.ts:
--------------------------------------------------------------------------------
1 | // deno-lint-ignore-file no-console
2 | // Imports
3 | import { getBinary } from "@astral/astral"
4 |
5 | console.log(await getBinary("chrome", { cache: ".cache" }))
6 |
--------------------------------------------------------------------------------
/.github/tools/bump_versions.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import type { Directive } from "@mizu/internal/engine"
3 | import { Logger } from "@libs/logger"
4 | import { parseArgs } from "@std/cli"
5 | import * as git from "@libs/git"
6 | import * as semver from "@std/semver"
7 | const log = new Logger()
8 |
9 | /** Bump packages version and print changelogs. */
10 | if (import.meta.main) {
11 | const { "dry-run": dryrun } = parseArgs(Deno.args, { boolean: ["dry-run"] })
12 |
13 | // Compute scopes
14 | const scopes = [...await Array.fromAsync<{ name: string }>(Deno.readDir("@mizu"))]
15 | .filter(({ name }) => !["internal", "render", "coverage"].includes(name))
16 | .map(({ name }) => name)
17 |
18 | // Compute render scopes
19 | const render = { scopes: ["internal", "render"] }
20 | const mapping = new Map>()
21 | for (const scope of scopes) {
22 | if (scope === "unstable") {
23 | continue
24 | }
25 | const { default: exported } = await import(`@mizu/${scope}`)
26 | for (const directive of [exported].flat()) {
27 | if (!mapping.has(directive)) {
28 | mapping.set(directive, new Set())
29 | }
30 | mapping.get(directive)!.add(scope)
31 | }
32 | }
33 | for (const [directive, scopes] of mapping) {
34 | log.with({ directive: `${directive.name}`, scopes: [...scopes].join(",") }).debug()
35 | }
36 | for (const section of ["client", "server"] as const) {
37 | const { default: directives } = await import(`@mizu/render/${section}/defaults`)
38 | for (const directive of [directives].flat()) {
39 | render.scopes.push(...(mapping.get(directive) ?? []))
40 | }
41 | }
42 | render.scopes = [...new Set(render.scopes)]
43 |
44 | // Bump package versions
45 | for (const scope of [...scopes, "internal", "render"]) {
46 | log.with({ directive: scope }).debug("checking")
47 | const { version, changelog } = git.changelog(`@mizu/${scope}/deno.jsonc`, { write: !dryrun, scopes: [scope, "internal", ...(scope === "render" ? render.scopes : [])] })
48 | if (version.bump) {
49 | changelog.split("\n").map((line) => log.with({ directive: scope }).info(line))
50 | log.with({ directive: scope }).ok(`${semver.format(version.current)} → ${semver.format(version.next)}`)
51 | } else {
52 | log.with({ directive: scope }).debug("no changes")
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/.github/tools/expand_glob.ts:
--------------------------------------------------------------------------------
1 | // deno-lint-ignore-file no-console
2 | // Imports
3 | import { expandGlob } from "@std/fs"
4 | import { parseArgs } from "@std/cli"
5 |
6 | /*** Resolve globs. */
7 | if (import.meta.main) {
8 | const { _: globs, ...flags } = parseArgs(Deno.args, { boolean: ["break"], string: ["separator"] })
9 | const paths = []
10 | for (const glob of globs) {
11 | for await (const { path } of expandGlob(`${glob}`)) {
12 | paths.push(path)
13 | }
14 | if ((paths.length) && (flags.break)) {
15 | break
16 | }
17 | }
18 | console.log(paths.join(flags.separator ?? ","))
19 | }
20 |
--------------------------------------------------------------------------------
/.github/tools/fill_github_scopes.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import type { rw } from "@libs/typing/types"
3 | import { expandGlob } from "@std/fs"
4 | import { Logger } from "@libs/logger"
5 | import { command } from "@libs/run/command"
6 | import * as YAML from "@std/yaml"
7 | const log = new Logger()
8 |
9 | /*** Fill GitHub files with current existing scopes. */
10 | if (import.meta.main) {
11 | // List scopes
12 | const scopes = [...await Array.fromAsync<{ name: string }>(Deno.readDir("@mizu"))]
13 | .concat([{ name: "www" }, { name: "repo" }])
14 | .filter(({ name }) => !["coverage"].includes(name))
15 | .map(({ name }) => name)
16 | .sort()
17 | // Patch issues templates
18 | log.info("patching issue templates")
19 | for await (const { path, name } of expandGlob(".github/ISSUE_TEMPLATE/*.yml")) {
20 | if (name === "config.yml") {
21 | continue
22 | }
23 | const parsed = YAML.parse(await Deno.readTextFile(path)) as rw
24 | if (parsed.body.some((field: rw) => field.id === "scope")) {
25 | parsed.body.find((filed: rw) => filed.id === "scope").attributes.options = scopes
26 | await Deno.writeTextFile(path, YAML.stringify(parsed))
27 | await command("deno", ["fmt", path], { stdout: null, stderr: null })
28 | log.with({ path }).ok("done")
29 | }
30 | }
31 | // Patch pull request labeler
32 | {
33 | log.info("patching pull request labeler")
34 | const path = ".github/labeler.yml"
35 | const generated = YAML.stringify(Object.fromEntries(scopes.map((scope) => [`scope: ${scope}`, [
36 | { "changed-files": [{ "any-glob-to-any-file": `@mizu/${scope}/**` }] },
37 | ]])))
38 | await Deno.writeTextFile(path, (await Deno.readTextFile(path)).replace(/(## == Autogenerated configuration starts below == ##)[\s\S]*/g, `$1\n\n${generated}`))
39 | await command("deno", ["fmt", path], { stdout: null, stderr: null })
40 | log.with({ path }).ok("done")
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/.github/tools/github_actions_deno_deploy.ts:
--------------------------------------------------------------------------------
1 | // deno-lint-ignore-file no-explicit-any
2 | // Imports
3 | import * as JSONC from "@std/jsonc"
4 | import * as core from "@actions/core"
5 |
6 | // Parse deno.jsonc and print deno deploy flags
7 | const config = JSONC.parse(await Deno.readTextFile("./deno.jsonc")) as any
8 | await Deno.writeTextFile(".imports.json", JSON.stringify({ imports: config.imports }, null, 2))
9 | core.setOutput("project", config.deploy.project)
10 | core.setOutput("entrypoint", config.deploy.entrypoint)
11 | core.setOutput("include", config.deploy.include.join(","))
12 | core.setOutput("exclude", config.deploy.exclude.join(","))
13 | core.setOutput("import_map", ".imports.json")
14 |
--------------------------------------------------------------------------------
/.github/tools/vendor_imports.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import type { Promisable } from "@libs/typing"
3 | import { fromFileUrl, globToRegExp, join, parse } from "@std/path"
4 | import { emptyDir, exists } from "@std/fs"
5 | import { Logger } from "@libs/logger"
6 |
7 | /** Vendor imports. */
8 | export class Vendor {
9 | /** Constructor. */
10 | constructor({ directive, meta, name }: { directive: string | RegExp; meta: ImportMeta; name: string }) {
11 | this.#directory = join(fromFileUrl(meta.resolve(`./import/${name}`)))
12 | this.log = new Logger().with({ directive })
13 | }
14 |
15 | /** Exposed import directory. */
16 | readonly #directory
17 |
18 | /** Linked {@linkcode Logger}. */
19 | readonly log
20 |
21 | /** List files from github repository. */
22 | async #tree(repository: string, { branch, path }: { branch: string; path: string }) {
23 | const segments = [...path.split("/"), ""]
24 | let subtree = { url: `https://api.github.com/repos/${repository}/git/trees/${branch}`, tree: [] as Array<{ path: string; type: string }> }
25 | while (segments.length) {
26 | const segment = segments.shift()
27 | this.log.debug(`fetching ${subtree.url}`)
28 | const { tree } = await fetch(subtree.url).then((response) => response.json())
29 | subtree = segment ? tree.find(({ path }: { path: string }) => path === segment) : { url: subtree.url, tree }
30 | }
31 | return subtree.tree.filter(({ type }: { type: string }) => type === "blob").map(({ path }: { path: string }) => path)
32 | }
33 |
34 | /** Vendor files from github repository. */
35 | async github(
36 | { repository, branch, path, globs, destination = "", export: exporter, callback }: {
37 | repository: string
38 | branch: string
39 | path: string
40 | globs?: string[]
41 | destination?: string
42 | export?: (name: string) => string
43 | callback?: (name: string, _: { log: Logger }) => Promisable
44 | },
45 | ) {
46 | // Clean directory
47 | if (exporter) {
48 | destination = join(this.#directory, destination)
49 | await emptyDir(destination)
50 | this.log.with({ destination }).ok("cleaned")
51 | }
52 | // Search files from repository
53 | let files = await this.#tree(repository, { branch, path })
54 | if (globs) {
55 | files = files.filter((file) => globs.every((glob) => globToRegExp(glob, { extended: true }).test(file)))
56 | }
57 | this.log.with({ repository, branch, path }).info(`found ${files.length} matching files`)
58 | // Process files
59 | for (const filename of files) {
60 | const { name } = parse(filename)
61 | const log = this.log.with({ name }).debug("processing")
62 | // Re-export file
63 | if (exporter) {
64 | const exports = join(destination, `${name}.ts`)
65 | if (!await exists(exports)) {
66 | await Deno.writeTextFile(exports, exporter(name))
67 | log.with({ exports }).ok()
68 | }
69 | }
70 | // Apply callback
71 | await callback?.(name, { log })
72 | }
73 | return this
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/.github/workflows/issue.yml:
--------------------------------------------------------------------------------
1 | name: Issue
2 | on:
3 | issues:
4 | types:
5 | - opened
6 | - edited
7 |
8 | jobs:
9 | labeler:
10 | name: Labeler
11 | runs-on: ubuntu-latest
12 | permissions:
13 | contents: read
14 | issues: write
15 | steps:
16 | - uses: actions/checkout@v4
17 | - uses: actions/github-script@v7
18 | with:
19 | script: |
20 | for (const { section, prefix } of [
21 | { section: "Scope", prefix: "scope: " },
22 | { section: "Environment", prefix: "env: " },
23 | ]) {
24 | const captured = (context.payload.issue.body.match(new RegExp(`### ${section}\\s*(?.*?)\\s*###`))?.groups?.labels ?? "").trim()
25 | console.log(captured, captured.split(",").map(name => `${prefix}${name.trim()}`))
26 | if (!captured) {
27 | continue
28 | }
29 |
30 | // Validate labels
31 | const { data: allowed } = await github.rest.issues.listLabelsForRepo({ ...context.repo, per_page: 100 }).catch(() => ({ data: [] }))
32 | console.log(allowed.map(label => label.name))
33 | const labels = captured.split(",").map(name => `${prefix}${name.trim()}`).filter(name => allowed.some(label => label.name === name))
34 | console.log(`Issue #${context.issue.number} ${prefix}${JSON.stringify(labels)}`)
35 |
36 | // Add new labels
37 | await github.rest.issues.addLabels({ ...context.repo, issue_number: context.issue.number, labels }).catch(() => null)
38 |
39 | // Remove previous labels
40 | const { data: previous } = await github.rest.issues.listLabelsOnIssue({ ...context.repo, issue_number: context.issue.number, per_page: 100 }).catch(() => ({ data: [] }))
41 | for (const { name } of previous.filter(label => label.name.startsWith(prefix) && !labels.includes(label.name))) {
42 | console.log(`Removing previous label: ${name}`)
43 | await github.rest.issues.removeLabel({ ...context.repo, issue_number: context.issue.number, name })
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/.github/workflows/pr.yml:
--------------------------------------------------------------------------------
1 | name: Pull
2 | on:
3 | pull_request_target:
4 |
5 | jobs:
6 | labeler:
7 | name: Labeler
8 | runs-on: ubuntu-latest
9 | permissions:
10 | contents: read
11 | pull-requests: write
12 | steps:
13 | - uses: actions/labeler@v5
14 | with:
15 | sync-labels: true
16 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: Stale
2 | on:
3 | schedule:
4 | - cron: "0 2 * * *"
5 | workflow_dispatch:
6 |
7 | jobs:
8 | stale:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | issues: write
12 | pull-requests: write
13 | steps:
14 | - uses: actions/stale@v9
15 | with:
16 | stale-issue-label: stale
17 | stale-pr-label: stale
18 | exempt-all-milestones: true
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .cache
2 | .vercel
3 | .imports.json
4 | coverage
5 | node_modules
6 | www/.pages
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | @jsr:registry=https://npm.jsr.io
2 | engine-strict=true
--------------------------------------------------------------------------------
/@mizu/bind/boolean.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Checks whether an HTML attribute is a boolean attribute for the specified tag name.
3 | *
4 | * See {@link https://html.spec.whatwg.org/#attributes-3 | HTML attributes reference}.
5 | */
6 | export function boolean(tagname: string, attribute: string): boolean {
7 | if (/^autofocus|inert|itemscope$/.test(attribute)) {
8 | return true
9 | }
10 | if (attribute === "disabled") {
11 | return /^(?:BUTTON|INPUT|OPTGROUP|OPTION|SELECT|TEXTAREA|FIELDSET|LINK)$/.test(tagname)
12 | }
13 | switch (tagname) {
14 | // Form elements
15 | case "FORM":
16 | return attribute === "novalidate"
17 | case "TEXTAREA":
18 | return /^(?:readonly|required)$/.test(attribute)
19 | case "INPUT":
20 | if (/^(?:checked|formnovalidate|readonly)$/.test(attribute)) {
21 | return true
22 | }
23 | // fallthrough
24 | case "SELECT":
25 | return /^(?:multiple|required)$/.test(attribute)
26 | case "OPTION":
27 | return attribute === "selected"
28 | case "BUTTON":
29 | return attribute === "formnovalidate"
30 |
31 | // Media elements
32 | case "IMG":
33 | return attribute === "ismap"
34 | case "VIDEO":
35 | if (attribute === "playsinline") {
36 | return true
37 | }
38 | // fallthrough
39 | case "AUDIO":
40 | return /^(?:autoplay|controls|loop|muted)$/.test(attribute)
41 | case "TRACK":
42 | return attribute === "default"
43 |
44 | // Other elements
45 | case "TEMPLATE":
46 | return /^shadowroot(?:delegatesfocus|clonable|serializable)$/.test(attribute)
47 | case "SCRIPT":
48 | return /^(?:async|defer|nomodule)$/.test(attribute)
49 | case "IFRAME":
50 | return attribute === "allowfullscreen"
51 | case "OL":
52 | return attribute === "reversed"
53 | case "DETAILS":
54 | case "DIALOG":
55 | return attribute === "open"
56 | }
57 | return false
58 | }
59 |
--------------------------------------------------------------------------------
/@mizu/bind/boolean_test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@libs/testing"
2 | import { boolean } from "./boolean.ts"
3 |
4 | for (
5 | const [attribute, tags] of [
6 | ["autofocus", { p: true }],
7 | ["disabled", { button: true, input: true, optgroup: true, option: true, select: true, textarea: true, fieldset: true, link: true, p: false }],
8 | ["novalidate", { form: true, p: false }],
9 | ["readonly", { input: true, textarea: true, p: false }],
10 | ["required", { textarea: true, input: true, select: true, p: false }],
11 | ["checked", { input: true, p: false }],
12 | ["formnovalidate", { input: true, button: true, p: false }],
13 | ["multiple", { select: true, p: false }],
14 | ["selected", { option: true, p: false }],
15 | ["ismap", { img: true, p: false }],
16 | ["playsinline", { video: true, p: false }],
17 | ["autoplay", { video: true, audio: true, p: false }],
18 | ["controls", { video: true, audio: true, p: false }],
19 | ["loop", { video: true, audio: true, p: false }],
20 | ["muted", { video: true, audio: true, p: false }],
21 | ["default", { track: true, p: false }],
22 | ["shadowrootdelegatesfocus", { template: true, p: false }],
23 | ["shadowrootclonable", { template: true, p: false }],
24 | ["shadowrootserializable", { template: true, p: false }],
25 | ["async", { script: true, p: false }],
26 | ["defer", { script: true, p: false }],
27 | ["nomodule", { script: true, p: false }],
28 | ["allowfullscreen", { iframe: true, p: false }],
29 | ["reversed", { ol: true, p: false }],
30 | ["open", { details: true, dialog: true, p: false }],
31 | ["other", { p: false }],
32 | ] as const
33 | ) {
34 | test(`{:attribute} boolean() treats accordingly {${attribute}}`, () => {
35 | for (const [tagname, expected] of Object.entries(tags)) {
36 | expect(boolean(tagname.toUpperCase(), attribute)).toBe(expected)
37 | }
38 | })
39 | }
40 |
--------------------------------------------------------------------------------
/@mizu/bind/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/bind",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/bind/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/clean/README.md:
--------------------------------------------------------------------------------
1 | # `*clean`
2 |
3 | | Version | Phase |
4 | | -------------------------------------- | ----------------------- |
5 | |  | 49 — `CONTENT_CLEANING` |
6 |
7 | Clean up the element and its children from specified content.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Modifiers
16 |
17 | ### `.comments[boolean]`
18 |
19 | Remove all [`Comment`](https://developer.mozilla.org/docs/Web/API/Comment) nodes within the subtree.
20 |
21 | ### `.spaces[boolean]`
22 |
23 | Remove all spaces (except non-breaking spaces [` `](https://developer.mozilla.org/docs/Glossary/Character_reference)) within the subtree.
24 |
25 | ### `.templates[boolean]`
26 |
27 | Clear all [``](https://developer.mozilla.org/docs/Web/HTML/Element/template) nodes from the subtree **after fully processing it**.
28 |
29 | ### `.directives[boolean]`
30 |
31 | Strip all known directives from the subtree **after fully processing it**. If the `.comments` modifier is also enabled, comments generated by directives will be removed as well.
32 |
--------------------------------------------------------------------------------
/@mizu/clean/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/clean",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/clean/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *clean
3 |
4 | Clean up the element and its children from specified content.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Remove all Comment
nodes within the subtree.
13 |
14 |
15 | Remove all spaces (except non-breaking spaces
) within the subtree.
16 |
17 |
18 | Clear all <template>
nodes from the subtree after fully processing it .
19 |
20 |
21 | Strip all known directives from the subtree after fully processing it . If the .comments
modifier is also enabled, comments generated by directives will be removed as well.
22 |
23 |
24 |
--------------------------------------------------------------------------------
/@mizu/clean/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | foo bar
7 | foo bar baz
8 |
9 |
10 | foo bar
11 | foo bar baz
12 |
13 |
14 |
15 |
16 | foo bar foo bar baz
17 |
18 | foo bar
19 | foo bar baz
20 |
21 |
22 |
23 |
24 |
25 |
26 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 | foo
60 |
61 |
62 | bar
63 |
64 |
65 |
66 |
67 |
68 |
69 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/@mizu/clean/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/code/README.md:
--------------------------------------------------------------------------------
1 | # `*code="content"`
2 |
3 | | Version | Phase | Default |
4 | | ------------------------------------- | -------------- | ------------------ |
5 | |  | 41 — `CONTENT` | `this.textContent` |
6 |
7 | Set element's [`innerHTML`](https://developer.mozilla.org/docs/Web/API/Element/innerHTML) after performing syntax highlighting.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!IMPORTANT]
18 | > This directive dynamically imports [`highlight.js`](https://highlightjs.org).
19 |
20 | > [!NOTE]
21 | > Unsupported languages default to `plaintext`.
22 |
23 | ## Modifiers
24 |
25 | ### `[string]`
26 |
27 | Any supported [language identifier or alias](https://highlightjs.readthedocs.io/en/latest/supported-languages.html).
28 |
29 | ### `.trim[boolean=true]`
30 |
31 | Remove leading/trailing whitespaces and shared indentation.
32 |
--------------------------------------------------------------------------------
/@mizu/code/build.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { Vendor } from "@tools/vendor_imports.ts"
3 | import { fromFileUrl } from "@std/path"
4 | import { _code } from "./mod.ts"
5 | import { command } from "@libs/run/command"
6 | import * as JSONC from "@std/jsonc"
7 |
8 | // Get dependency
9 | const config = fromFileUrl(import.meta.resolve("./deno.jsonc"))
10 | const { imports: { "highlight.js": dependency } } = JSONC.parse(await Deno.readTextFile(config)) as { imports: Record }
11 |
12 | // Fetch highlight.js languages
13 | const { default: hljs } = await import(`${dependency}/lib/core`)
14 | const mapping = {} as Record
15 | const vendor = await new Vendor({ directive: _code.name, meta: import.meta, name: "highlight.js" })
16 | .github({
17 | repository: "highlightjs/highlight.js",
18 | branch: "main",
19 | path: "src/languages",
20 | globs: ["*.js"],
21 | async callback(name, { log }) {
22 | const { default: syntax } = await import(`${dependency}/lib/languages/${name}`)
23 | hljs.registerLanguage(name, syntax)
24 | mapping[name] = name
25 | for (const alias of (hljs.getLanguage(name)?.aliases ?? [])) {
26 | mapping[alias] = name
27 | log.trace("registered alias", alias)
28 | }
29 | },
30 | })
31 |
32 | // Write mapping.json
33 | await Deno.writeTextFile(fromFileUrl(import.meta.resolve("./mapping.json")), JSON.stringify(mapping))
34 | vendor.log.ok("mapping.json generated")
35 |
36 | // Update deno.jsonc
37 | const imports = {
38 | "highlight.js": dependency,
39 | "highlight.js/lib/core": `${dependency}/lib/core`,
40 | "highlight.js/lib/languages/____": `${dependency}/lib/languages`,
41 | ...Object.fromEntries(Object.entries(mapping).map(([alias, language]) => [`highlight.js/lib/languages/${alias}`, `${dependency}/lib/languages/${language}`])),
42 | }
43 | await Deno.writeTextFile(config, Deno.readTextFileSync(config).replace(/"imports": {[^}]+}/, `"imports": ${JSON.stringify(imports)}`))
44 | await command("deno", ["fmt", config], { stdout: null, stderr: null })
45 | vendor.log.ok(`updated ${config}`)
46 |
--------------------------------------------------------------------------------
/@mizu/code/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *code ="content"
3 |
4 | Set element's innerHTML
after performing syntax highlighting.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | This directive dynamically imports highlight.js .
13 |
14 |
15 | Unsupported languages default to plaintext
.
16 |
17 |
18 | string
19 | Any supported language identifier or alias .
20 |
21 |
22 | Remove leading/trailing whitespaces and shared indentation.
23 |
24 |
25 |
--------------------------------------------------------------------------------
/@mizu/code/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Directive, Phase } from "@mizu/internal/engine"
3 | import mapping from "./mapping.json" with { type: "json" }
4 | export type * from "@mizu/internal/engine"
5 |
6 | /** Typings. */
7 | export const typings = {
8 | modifiers: {
9 | trim: { type: Boolean, enforce: true },
10 | },
11 | } as const
12 |
13 | /** `*code` directive. */
14 | export const _code = {
15 | name: "*code",
16 | phase: Phase.CONTENT,
17 | typings,
18 | default: "this.textContent",
19 | async execute(renderer, element, { attributes: [attribute], ...options }) {
20 | if (!renderer.isHtmlElement(element)) {
21 | return
22 | }
23 |
24 | // Load language syntax
25 | const parsed = renderer.parseAttribute(attribute, this.typings, { modifiers: true })
26 | const language = (mapping as Record)[parsed.tag] ?? "plaintext"
27 | const { default: hljs } = await import("highlight.js/lib/core")
28 | if (!hljs.getLanguage(language)) {
29 | const { default: syntax } = await import(`highlight.js/lib/languages/${language}`)
30 | hljs.registerLanguage(language, syntax)
31 | }
32 |
33 | // Retrieve code
34 | let code = `${await renderer.evaluate(element, attribute.value || this.default, options)}`
35 |
36 | // Trim indentation
37 | if (parsed.modifiers.trim) {
38 | const trim = code.match(/^[ \t]*\S/m)?.[0].match(/\S/)?.index ?? 0
39 | code = code.replaceAll(new RegExp(`^[ \\t]{${trim}}`, "gm"), "")
40 | }
41 |
42 | // Highlight code
43 | code = hljs.highlight(code, { language }).value
44 | if (parsed.modifiers.trim) {
45 | code = code.trim()
46 | }
47 | element.innerHTML = code
48 |
49 | // Trim parent
50 | if ((parsed.modifiers.trim) && (element.parentElement?.tagName === "PRE")) {
51 | element.parentElement.innerHTML = element.parentElement.innerHTML.trim()
52 | }
53 | },
54 | } as Directive & { default: NonNullable }
55 |
56 | /** Default exports. */
57 | export default _code
58 |
--------------------------------------------------------------------------------
/@mizu/code/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | const foo = 'bar'
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | const foo = 'bar'
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | const foo = 'bar'
33 |
34 |
35 |
36 |
37 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | function foo() {
54 | return 'bar'
55 | }
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | function foo() {
66 | return 'bar'
67 | }
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | const foo = 'bar'
85 |
86 |
87 |
88 | const foo = 'bar'
89 |
90 |
91 |
92 | const foo = 'bar'
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/@mizu/code/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/custom-element/README.md:
--------------------------------------------------------------------------------
1 | # `*custom-element="tagname"`
2 |
3 | | Version | Phase |
4 | | ----------------------------------------------- | --------------------- |
5 | |  | 81 — `CUSTOM_ELEMENT` |
6 |
7 | Register a new [custom element](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements).
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!CAUTION]
18 | > Must be defined on a [``](https://developer.mozilla.org/docs/Web/HTML/Element/template) element.
19 |
20 | > [!CAUTION]
21 | > The `tagname` must be a [valid custom element name](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements#name).
22 |
23 | > [!NOTE]
24 | > Valid [custom element names](https://developer.mozilla.org/docs/Web/API/Web_components/Using_custom_elements#name) may be specified « as is ».
25 |
26 | > [!NOTE]
27 | > Custom elements registered this way do not use [Shadow DOM](https://developer.mozilla.org/docs/Web/API/Web_components/Using_shadow_DOM), their content is rendered directly.
28 |
29 | ## Variables
30 |
31 | ### `$slots: Record`
32 |
33 | A record of [`# slot`](#slot) elements by [``](https://developer.mozilla.org/docs/Web/HTML/Element/slot) name. The unnamed slot is accessible using `$slots[""]`.
34 |
35 | ### `$attrs: Record`
36 |
37 | A record of [HTML attributes](https://developer.mozilla.org/docs/Web/HTML/Attributes) specified on the custom element.
38 |
39 | ## Modifiers
40 |
41 | ### `.flat[boolean]`
42 |
43 | Replace occurrences of this custom element with its content. Note that `$slots` and `$attrs` variables are not accessible when using this modifier.
44 |
45 | # `#slot`
46 |
47 | | Version | Phase |
48 | | ----------------------------------------------- | ---------- |
49 | |  | 0 — `META` |
50 |
51 | Specify target [``](https://developer.mozilla.org/docs/Web/HTML/Element/slot) in an element defined by a [`*custom-element`](#custom-element) directive.
52 |
53 | ```html
54 |
55 |
56 |
57 | ```
58 |
59 | ## Notes
60 |
61 | > [!NOTE]
62 | > Elements without a [`#slot`](#slot) directive are appended to the default (unnamed) slot.
63 |
64 | > [!NOTE]
65 | > Elements targeting the same slot are appended in the order they are defined.
66 |
--------------------------------------------------------------------------------
/@mizu/custom-element/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/custom-element",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/custom-element/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/eval/README.md:
--------------------------------------------------------------------------------
1 | # `*eval="expression"`
2 |
3 | | Version | Phase |
4 | | ------------------------------------- | ------------------------ |
5 | |  | 89 — `CUSTOM_PROCESSING` |
6 |
7 | Evaluate a JavaScript expression in the context of the element.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!WARNING]
18 | > Use this directive sparingly, prefer alternative directives for better maintainability and security. This directive is intended for edge cases.
19 |
20 | > [!NOTE]
21 | > The expression runs **after** the element and all its children have been fully processed.
22 |
--------------------------------------------------------------------------------
/@mizu/eval/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/eval",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/eval/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *eval ="expression"
3 |
4 | Evaluate a JavaScript expression in the context of the element.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Use this directive sparingly, prefer alternative directives for better maintainability and security. This directive is intended for edge cases.
13 |
14 |
15 | The expression runs after the element and all its children have been fully processed.
16 |
17 |
18 |
--------------------------------------------------------------------------------
/@mizu/eval/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Cache, type Directive, Phase } from "@mizu/internal/engine"
3 | export type * from "@mizu/internal/engine"
4 |
5 | /** `*eval` directive. */
6 | export const _eval = {
7 | name: "*eval",
8 | phase: Phase.CUSTOM_PROCESSING,
9 | init(renderer) {
10 | renderer.cache>(this.name, new WeakMap())
11 | },
12 | execute(renderer, element, { attributes: [attribute], cache }) {
13 | if (!renderer.isHtmlElement(element)) {
14 | return
15 | }
16 | cache.set(element, attribute)
17 | },
18 | async cleanup(renderer, element, { cache, ...options }) {
19 | if (!renderer.isHtmlElement(element)) {
20 | return
21 | }
22 | if (cache.has(element)) {
23 | const attribute = cache.get(element)!
24 | cache.delete(element)
25 | await renderer.evaluate(element, attribute.value, { ...options, args: [] })
26 | }
27 | },
28 | } as Directive>
29 |
30 | /** Default exports. */
31 | export default _eval
32 |
--------------------------------------------------------------------------------
/@mizu/eval/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | foo
6 |
7 |
8 | bar
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
22 |
23 |
24 |
25 |
26 | foo
27 |
28 |
29 | bar
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/@mizu/eval/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/event/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/event",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/event/keyboard.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Keyboard expression matcher.
3 | *
4 | * Multiple keys can be combined into a single combination using a plus sign (`+`).
5 | * Multiple combinations can be combined into a single expression using a comma (`,`).
6 | * Spaces are always trimmed.
7 | *
8 | * The following aliases are supported:
9 | * - `alt` when `Alt` is pressed
10 | * - `ctrl` when `Control` is pressed
11 | * - `shift` when `Shift` is pressed
12 | * - `meta` when `Meta` is pressed
13 | * - `space` for `Space` key
14 | * - `key` for any key except `Alt`, `Control`, `Shift` and `Meta`
15 | *
16 | * If the event is not a {@link https://developer.mozilla.org/docs/Web/API/KeyboardEvent | KeyboardEvent}, the function will return `false`.
17 | *
18 | * {@link https://developer.mozilla.org/docs/Web/API/UI_Events/Keyboard_event_key_values | Reference}
19 | *
20 | * ```ts
21 | * import { Window } from "@mizu/internal/vdom"
22 | * const { KeyboardEvent } = new Window()
23 | *
24 | * const check = keyboard("a,ctrl+b")
25 | * console.assert(check(new KeyboardEvent("keydown", {key: "a"})))
26 | * console.assert(check(new KeyboardEvent("keydown", {key: "b", ctrlKey: true})))
27 | * console.assert(!check(new KeyboardEvent("keydown", {key: "c"})))
28 | * ```
29 | *
30 | * @author Simon Lecoq (lowlighter)
31 | * @license MIT
32 | */
33 | export function keyboard(keys: string): (event: KeyboardEvent) => boolean {
34 | const combinations = keys.split(",").map((combination) => combination.split("+").map((key) => key.trim().toLowerCase()))
35 | return function (event: KeyboardEvent) {
36 | if (!/^key(?:down|press|up)$/.test(event.type)) {
37 | return false
38 | }
39 | return combinations.some((combination) => {
40 | for (const key of combination) {
41 | switch (key) {
42 | case "alt":
43 | case "ctrl":
44 | case "shift":
45 | case "meta":
46 | if (!event[`${key}Key`]) {
47 | return false
48 | }
49 | break
50 | case "space":
51 | if (event.key !== " ") {
52 | return false
53 | }
54 | break
55 | case "key":
56 | if (/^(?:alt|ctrl|shift|meta)$/i.test(event.key)) {
57 | return false
58 | }
59 | break
60 | default:
61 | if (event.key.toLowerCase() !== key) {
62 | return false
63 | }
64 | }
65 | }
66 | return true
67 | })
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/@mizu/event/keyboard_test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test, type testing } from "@libs/testing"
2 | import { Window } from "@mizu/internal/vdom"
3 | import { keyboard } from "./keyboard.ts"
4 | const { KeyboardEvent, MouseEvent } = new Window()
5 |
6 | test(`{@event} keyboard() handles single keys`, () => {
7 | const fn = keyboard("a")
8 | expect(fn(new KeyboardEvent("keydown", { key: "a" }))).toBe(true)
9 | expect(fn(new KeyboardEvent("keydown", { key: "b" }))).toBe(false)
10 | })
11 |
12 | test(`{@event} keyboard() handles combination keys`, () => {
13 | for (const key of ["ctrl", "shift", "meta", "alt"] as const) {
14 | const fn = keyboard(`${key}+a`)
15 | expect(fn(new KeyboardEvent("keydown", { key: "a", [`${key}Key`]: true }))).toBe(true)
16 | expect(fn(new KeyboardEvent("keydown", { key: "a" }))).toBe(false)
17 | expect(fn(new KeyboardEvent("keydown", { key: "b", [`${key}Key`]: true }))).toBe(false)
18 | }
19 | })
20 |
21 | test(`{@event} keyboard() handles "key" wildcard`, () => {
22 | const fn = keyboard("key")
23 | expect(fn(new KeyboardEvent("keydown", { key: "a" }))).toBe(true)
24 | expect(fn(new KeyboardEvent("keydown", { key: "b" }))).toBe(true)
25 | expect(fn(new KeyboardEvent("keydown", { key: "ctrl" }))).toBe(false)
26 | expect(fn(new KeyboardEvent("keydown", { key: "shift" }))).toBe(false)
27 | expect(fn(new KeyboardEvent("keydown", { key: "meta" }))).toBe(false)
28 | expect(fn(new KeyboardEvent("keydown", { key: "alt" }))).toBe(false)
29 | })
30 |
31 | test(`{@event} keyboard() handles "key" wildcard within combinations`, () => {
32 | const fn = keyboard("alt+key")
33 | expect(fn(new KeyboardEvent("keydown", { key: "Alt", altKey: true }))).toBe(false)
34 | expect(fn(new KeyboardEvent("keydown", { key: "x", altKey: true }))).toBe(true)
35 | expect(fn(new KeyboardEvent("keydown", { key: "x", altKey: false }))).toBe(false)
36 | })
37 |
38 | test(`{@event} keyboard() handles "space" alias`, () => {
39 | const fn = keyboard("space")
40 | expect(fn(new KeyboardEvent("keydown", { key: " " }))).toBe(true)
41 | expect(fn(new KeyboardEvent("keydown", { key: "a" }))).toBe(false)
42 | expect(fn(new KeyboardEvent("keydown", { key: "" }))).toBe(false)
43 | })
44 |
45 | test(`{@event} keyboard() handles multiple combinations`, () => {
46 | const fn = keyboard("a, b, ctrl+c")
47 | expect(fn(new KeyboardEvent("keydown", { key: "a" }))).toBe(true)
48 | expect(fn(new KeyboardEvent("keydown", { key: "b" }))).toBe(true)
49 | expect(fn(new KeyboardEvent("keydown", { key: "c" }))).toBe(false)
50 | expect(fn(new KeyboardEvent("keydown", { key: "c", ctrlKey: true }))).toBe(true)
51 | })
52 |
53 | test(`{@event} keyboard() does not react on non KeyboardEvent`, () => {
54 | const fn = keyboard("dead")
55 | expect(fn(new MouseEvent("click") as testing)).toBe(false)
56 | })
57 |
--------------------------------------------------------------------------------
/@mizu/event/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { expect, fn, test, type testing } from "@libs/testing"
2 | import { test as _test } from "@mizu/internal/testing"
3 | import { Window } from "@mizu/internal/vdom"
4 | import { type Directive, Phase, Renderer } from "@mizu/internal/engine"
5 | import directive from "./mod.ts"
6 |
7 | _test(import.meta)
8 |
9 | test("[@event] supports `_event` internal api", async () => {
10 | await using window = new Window()
11 | const tested = {
12 | name: "~tested",
13 | phase: Phase.TESTING,
14 | init: directive.init,
15 | execute: (renderer, element, options) => directive.execute(renderer, element, { ...options, _event: "testing" } as testing),
16 | } as Directive
17 | const renderer = await new Renderer(window, { directives: [tested] }).ready
18 | const element = Object.assign(renderer.createElement("div", { attributes: { [`${tested.name}`]: "" } }), { addEventListener: fn() })
19 | await renderer.render(element)
20 | expect(element.addEventListener).toHaveBeenCalledWith("testing", expect.any(Function), expect.any(Object))
21 | })
22 |
23 | test("[@event] supports `_callback` internal api", async () => {
24 | await using window = new Window()
25 | const callback = fn()
26 | const tested = {
27 | name: "~tested",
28 | phase: Phase.TESTING,
29 | init: directive.init,
30 | execute: (renderer, element, options) => directive.execute(renderer, element, { ...options, _event: "testing", _callback: callback } as testing),
31 | } as Directive
32 | const renderer = await new Renderer(window, { directives: [tested] }).ready
33 | const element = renderer.createElement("div", { attributes: { [`${tested.name}`]: "" } })
34 | await renderer.render(element)
35 | element.dispatchEvent(new renderer.window.Event("testing"))
36 | expect(callback).toHaveBeenCalled()
37 | })
38 |
--------------------------------------------------------------------------------
/@mizu/for/README.md:
--------------------------------------------------------------------------------
1 | # `*for="expression"`
2 |
3 | | Version | Phase |
4 | | ------------------------------------ | ------------- |
5 | |  | 21 — `EXPAND` |
6 |
7 | Render an element for each iteration performed.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!CAUTION]
18 | > The expression can be:
19 | >
20 | > - Any syntax supported inside [`for`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/for), [`for...in`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/for...in) and
21 | > [`for...of`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/for...of) loops.
22 | > - Any iterable object that implements [`Symbol.iterator`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol/iterator).
23 | > - The iterated key is exposed as `$key`.
24 | > - The iterated value is exposed as `$value`.
25 | > - A finite `number`.
26 | > - The directive is applied the specified number of times.
27 |
28 | > [!NOTE]
29 | > There is currently no distinction between [`let`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/let), [`const`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/const) and
30 | > [`var`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/var) declarations inside `for` loops, but future versions may introduce specific behavior for these.
31 |
32 | > [!NOTE]
33 | > There is currently no special handling for [``](https://developer.mozilla.org/docs/Web/HTML/Element/template) elements, but future versions may introduce specific behavior for these elements.
34 |
35 | ## Variables
36 |
37 | ### `$id: string`
38 |
39 | The evaluated value of the [`*id`](#id) directive if present, or the auto-generated identifier.
40 |
41 | ### `$iterations: number`
42 |
43 | The total number of iterations.
44 |
45 | ### `$i: number`
46 |
47 | The current iteration index _(0-based)_.
48 |
49 | ### `$I: number`
50 |
51 | The current iteration index _(1-based, same as `$i + 1`)_.
52 |
53 | ### `$first: number`
54 |
55 | Whether this is the first iteration _(same as `$i === 0`)_.
56 |
57 | ### `$last: number`
58 |
59 | Whether this is the last iteration _(same as `$i === ($iterations - 1)`)_.
60 |
61 | # `*id="expression"`
62 |
63 | | Version | Phase |
64 | | ------------------------------------ | ---------- |
65 | |  | 0 — `META` |
66 |
67 | Hint for [`*for`](#for) directive to differentiate generated elements.
68 |
69 | ```html
70 |
71 |
72 |
73 | ```
74 |
75 | ## Notes
76 |
77 | > [!CAUTION]
78 | > Must be used on an element with a [`*for`](#for) directive.
79 |
80 | > [!NOTE]
81 | > Identifiers must be unique within the loop, any duplicates will be replaced by the last occurrence.
82 |
--------------------------------------------------------------------------------
/@mizu/for/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/for",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts",
6 | "./empty": "./empty/mod.ts"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/@mizu/for/empty/README.md:
--------------------------------------------------------------------------------
1 | # `*empty`
2 |
3 | | Version | Phase |
4 | | ------------------------------------------ | ------------- |
5 | |  | 23 — `TOGGLE` |
6 |
7 | Conditionally render an element after a [`*for`](#for) directive.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!CAUTION]
18 | > Must be placed immediately after an element with a [`*for`](#for) or another [`*empty`](#empty) directive. Elements generated by the [`*for`](#for) directive do not apply to this restriction.
19 |
20 | > [!NOTE]
21 | > There is currently no special handling for [``](https://developer.mozilla.org/docs/Web/HTML/Element/template) elements, but future versions may introduce specific behavior for these elements.
22 |
23 | ## Variables
24 |
25 | ### `$generated: number`
26 |
27 | The number of elements generated by the preceding [`*for`](#for) directive. This value may differ from the actual number of iterations processed if conditional directives were applied.
28 |
29 | ## Modifiers
30 |
31 | ### `.not[boolean]`
32 |
33 | Inverts the condition, rendering the element when at least one element is generated.
34 |
--------------------------------------------------------------------------------
/@mizu/for/empty/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *empty
3 |
4 | Conditionally render an element after a *for
directive.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Must be placed immediately after an element with a *for
or another *empty
directive. Elements generated by the *for
13 | directive do not apply to this restriction.
14 |
15 |
16 | There is currently no special handling for <template>
elements, but future versions may introduce specific behavior for these elements.
17 |
18 |
19 | number
20 | The number of elements generated by the preceding *for
directive. This value may differ from the actual number of iterations processed if conditional directives were applied.
21 |
22 |
23 | Inverts the condition, rendering the element when at least one element is generated.
24 |
25 |
26 |
--------------------------------------------------------------------------------
/@mizu/for/empty/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Cache, type Directive, Phase } from "@mizu/internal/engine"
3 | import { _for, _id } from "@mizu/for"
4 | import { _if } from "@mizu/if"
5 | export type * from "@mizu/internal/engine"
6 |
7 | /** Typings. */
8 | export const typings = {
9 | modifiers: {
10 | not: { type: Boolean },
11 | },
12 | } as const
13 |
14 | /** `*empty` directive. */
15 | export const _empty = {
16 | name: "*empty",
17 | phase: Phase.TOGGLE,
18 | typings,
19 | execute(renderer, element, { attributes: [attribute] }) {
20 | const parsed = renderer.parseAttribute(attribute, this.typings, { modifiers: true })
21 | const cache = renderer.cache>(_for.name)
22 | const seen = [] as HTMLElement[]
23 | let previous = element.previousSibling as HTMLElement
24 | while (previous) {
25 | // Break on non-empty text nodes
26 | if ((previous.nodeType === renderer.window.Node.TEXT_NODE) && (previous.textContent?.trim())) {
27 | break
28 | }
29 |
30 | // Break on element node that was not created by a for directive or without an empty directive
31 | if ((renderer.isHtmlElement(previous)) && (!renderer.getAttributes(previous, [_empty.name, _id.name], { first: true }))) {
32 | seen.push(previous)
33 | }
34 |
35 | // Execute directive on first for loop found
36 | if ((renderer.isComment(previous)) && (cache?.has(previous))) {
37 | const items = [...cache.get(previous)!.items.values()]
38 | const $generated = items.length
39 | if (seen.some((item) => !items.includes(item))) {
40 | break
41 | }
42 | return {
43 | ..._if.execute(renderer, element, { ...arguments[2], _directive: { directive: this.name, expression: attribute.value, value: `!${parsed.modifiers.not ? "!" : ""}${$generated}` } }),
44 | state: { $generated },
45 | }
46 | }
47 | previous = (previous as HTMLElement).previousSibling as HTMLElement
48 | }
49 | renderer.warn(`[${this.name}] must be immediately preceded by a [${_for.name}] or [${_empty.name}] directive, ignoring`, element)
50 | return { state: { $generated: NaN } }
51 | },
52 | } as Directive & { name: string }
53 |
54 | /** Default exports. */
55 | export default [_for, _empty]
56 |
--------------------------------------------------------------------------------
/@mizu/for/empty/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/for/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/for/parse_test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@libs/testing"
2 | import { Expression } from "./parse.ts"
3 |
4 | for (
5 | const [expression, expected] of [
6 | // Basic syntax
7 | ["invalid", SyntaxError],
8 | [`let a of []`, ["a"]],
9 | [`var a of []`, ["a"]],
10 | [`const a of []`, ["a"]],
11 | [`const a, b, c of []`, SyntaxError],
12 |
13 | // Initialization, condition and afterthought
14 | [`let i = 0; i < x; i++`, ["i"]],
15 | [`let i = 0, j = 0;;`, ["i", "j"]],
16 | [`let i = {}, j = [], k = "";;`, ["i", "j", "k"]],
17 | [`let i = () => {}, j = async function() {}, k = i+1;;`, ["i", "j", "k"]],
18 | [`let i = 0;;`, ["i"]],
19 | [`let i`, SyntaxError],
20 | [`let i = 0;`, SyntaxError],
21 | [`;;`, []],
22 |
23 | // Array
24 | // Destructuring
25 | [`const [a, b, c] of []`, ["a", "b", "c"]],
26 | [`const [a, [b, [c]]] of []`, ["a", "b", "c"]],
27 | [`const [a, b, c of []`, SyntaxError],
28 | [`const [a, [b, c] of []`, SyntaxError],
29 | // Spreading
30 | [`const [a, b, ...c] of []`, ["a", "b", "c"]],
31 | [`const [a, ...b, c] of []`, SyntaxError],
32 | // Default values
33 | [`const [a = 1, b = [], c = {x:1}, d] of []`, ["a", "b", "c", "d"]],
34 | [`const [a = Math.random() > .5 ? 1 : 0, b] of []`, ["a", "b"]],
35 | [`const [a = (async () => await ([]))(), b] of []`, ["a", "b"]],
36 | [`const [a = "string", b = c] of []`, ["a", "b"]],
37 |
38 | // Object
39 | // Destructuring
40 | [`const {a, b, c} of []`, ["a", "b", "c"]],
41 | [`const {a, x: {b, y:{c}}} of []`, ["a", "b", "c"]],
42 | [`const {a, b, c of []`, SyntaxError],
43 | [`const {a, {b, c} of []`, SyntaxError],
44 | // Spreading syntax
45 | [`const {a, b, ...c} of []`, ["a", "b", "c"]],
46 | [`const {a, ...b, c} of []`, SyntaxError],
47 | // Default values
48 | [`const {a = 1, b = [], c = {x:1}, d} of []`, ["a", "b", "c", "d"]],
49 | [`const {a = Math.random() > .5 ? 1 : 0, b} of []`, ["a", "b"]],
50 | [`const {a = (async () => await ([]))(), b} of []`, ["a", "b"]],
51 | [`const {a = "string", b = c} of []`, ["a", "b"]],
52 | [`const {a = () => ({}), b} of []`, ["a", "b"]],
53 | [`const {a = async () => ({}), b} of []`, ["a", "b"]],
54 | [`const {a = async function*(){}, b} of []`, ["a", "b"]],
55 |
56 | // Alias syntax
57 | [`const {a, b:c} of []`, ["a", "c"]],
58 | [`const {a, b:c = 1} of []`, ["a", "c"]],
59 | [`const {a, [b]:c = 1} of []`, ["a", "c"]],
60 |
61 | // Complex syntax
62 | [`const {a = ([{a:1}, [{a:(true)}]]), b} of []`, ["a", "b"]],
63 | [`const {a = ({a = 1} = {}) => [a + 1], b = Math.random(), [Symbol.for("@")]:{d = 1}} of []`, ["a", "b", "d"]],
64 | ] as const
65 | ) {
66 | test(`\`Expression.parse()\` parses \`${expression} => ${expected === SyntaxError ? "new SyntaxError()" : expected}\``, () => {
67 | if (expected === SyntaxError) {
68 | expect(() => Expression.parse(expression)).toThrow()
69 | } else {
70 | expect(Expression.parse(expression)).toEqual(expected)
71 | }
72 | })
73 | }
74 |
--------------------------------------------------------------------------------
/@mizu/html/README.md:
--------------------------------------------------------------------------------
1 | # `*html="content"`
2 |
3 | | Version | Phase |
4 | | ------------------------------------- | -------------- |
5 | |  | 41 — `CONTENT` |
6 |
7 | Set element's [`innerHTML`](https://developer.mozilla.org/docs/Web/API/Element/innerHTML).
8 |
9 | ```html
10 | ...
'">
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!WARNING]
18 | > Raw HTML can introduce [XSS vulnerabilities](https://developer.mozilla.org/docs/Glossary/Cross-site_scripting). Exercise caution when using expressions from untrusted sources.
19 |
--------------------------------------------------------------------------------
/@mizu/html/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/html",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/html/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *html ="content"
3 |
4 | Set element's innerHTML
.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Raw HTML can introduce XSS vulnerabilities . Exercise caution when using expressions from untrusted sources.
13 |
14 |
15 |
--------------------------------------------------------------------------------
/@mizu/html/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Directive, Phase } from "@mizu/internal/engine"
3 | export type * from "@mizu/internal/engine"
4 |
5 | /** `*html` directive. */
6 | export const _html = {
7 | name: "*html",
8 | phase: Phase.CONTENT,
9 | async execute(renderer, element, { attributes: [attribute], ...options }) {
10 | if (!renderer.isHtmlElement(element)) {
11 | return
12 | }
13 | element.innerHTML = `${await renderer.evaluate(element, attribute.value, options)}`
14 | },
15 | } as Directive & { default: NonNullable }
16 |
17 | /** Default exports. */
18 | export default _html
19 |
--------------------------------------------------------------------------------
/@mizu/html/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 | foo
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/@mizu/html/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/http/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/http",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts",
6 | "./event": "./event/mod.ts"
7 | },
8 | "imports": {
9 | "@libs/xml/parse": "jsr:@libs/xml@6/parse",
10 | "@libs/xml/stringify": "jsr:@libs/xml@6/stringify"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/@mizu/http/event/README.md:
--------------------------------------------------------------------------------
1 | # `%@event="listener"`
2 |
3 | | Version | Phase | Default | Multiple |
4 | | ------------------------------------------- | ------------------------- | ------- | -------- |
5 | |  | 35 — `HTTP_INTERACTIVITY` | `null` | Yes |
6 |
7 | Listen for a dispatched [`Event`](https://developer.mozilla.org/docs/web/api/event) and re-evaluates [`%http`](#http) directive before reacting to its [`Response`](https://developer.mozilla.org/docs/Web/API/Response).
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!CAUTION]
18 | > Must be defined on an element that also possess a [`%http`](#http) directive.
19 |
20 | > [!NOTE]
21 | > This is essentially a combination of [`%response`](#response) and [`@event`](#event) directives.
22 |
23 | > [!NOTE]
24 | > Target URL is still set by [`%http`](#http) directive. As it is re-evaluated, you can however use the `$event` value to dynamically compute the target URL _(e.g. `%http="$event ? '/foo' : '/bar'"`)_. All modifiers from [`%http`](#http) directive are inherited, along with the
25 | > [`RequestInit`](https://developer.mozilla.org/docs/Web/API/RequestInit) prepared by [`%header`](#header) and [`%body`](#body) directives.
26 |
27 | ## Variables
28 |
29 | ### `$event: Event`
30 |
31 | _(in `listener` only)_ The dispatched [`Event`](https://developer.mozilla.org/docs/web/api/event).
32 |
33 | ### `$response: Response`
34 |
35 | A [`Response`](https://developer.mozilla.org/docs/Web/API/Response) object that contains the fetched data.
36 |
37 | ### `$content: unknown`
38 |
39 | A variable that contains the [`response.body`](https://developer.mozilla.org/docs/Web/API/Response/body) _(typing depends on which modifier is used)_.
40 |
41 | ## Modifiers
42 |
43 | ### `...`
44 |
45 | Inherited from [`@event`](#event) and [`%response`](#response) directives. See their respective documentation for more information.
46 |
--------------------------------------------------------------------------------
/@mizu/http/event/mod.html:
--------------------------------------------------------------------------------
1 |
2 | %@ event ="listener"
3 |
4 | Listen for a dispatched Event
5 | and re-evaluates %http
directive before reacting to its Response
.
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Must be defined on an element that also possess a %http
directive.
14 |
15 |
16 | This is essentially a combination of %response
and @event
directives.
17 |
18 |
19 | Target URL is still set by %http
directive. As it is re-evaluated, you can however use the $event
value to dynamically compute the target URL (e.g.%http="$event ? '/foo' : '/bar'"
) . All
20 | modifiers from %http
directive are inherited, along with the RequestInit
prepared by %header
and %body
directives.
22 |
23 |
24 | Event
25 | (in listener
only) The dispatched Event
.
26 |
27 |
28 | Response
29 | A Response
object that contains the fetched data.
30 |
31 |
32 | unknown
33 | A variable that contains the response.body
34 | (typing depends on which modifier is used) .
35 |
36 |
37 | Inherited from @event
and %response
directives. See their respective documentation for more information.
38 |
39 |
40 |
--------------------------------------------------------------------------------
/@mizu/http/event/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Cache, type callback, type Directive, Phase } from "@mizu/internal/engine"
3 | import { _event } from "@mizu/event"
4 | import { _body, _header, _http, _response, _response_typings } from "@mizu/http"
5 | export type * from "@mizu/internal/engine"
6 | export type { _event, typings as _event_typings } from "@mizu/event"
7 |
8 | /** `%@event` directive. */
9 | export const _http_event = {
10 | name: /^%@(?)/,
11 | prefix: "%@",
12 | phase: Phase.HTTP_INTERACTIVITY,
13 | default: "null",
14 | multiple: true,
15 | typings: {
16 | modifiers: {
17 | ..._event.typings.modifiers,
18 | ..._response_typings.modifiers,
19 | },
20 | },
21 | init(renderer) {
22 | renderer.cache(this.name, new WeakMap())
23 | renderer.cache(`#${this.name}`, new WeakMap())
24 | },
25 | async execute(renderer, element) {
26 | if (renderer.isComment(element)) {
27 | return
28 | }
29 | const cached = renderer.cache>(`#${this.name}`)!
30 | if (!cached.has(element)) {
31 | const callback = async ($event: Event, { attribute, expression }: { attribute: Attr; expression: callback }) => {
32 | const http = await _http.execute.call(this, renderer, element, { ...arguments[2], attributes: renderer.getAttributes(element, _http.name), _return_callback: true }) as Awaited
33 | const $response = await http($event)
34 | await _response.execute.call(this, renderer, element, { ...arguments[2], attributes: [attribute], state: { ...arguments[2].state, $event, $response }, _expression: { value: expression, args: [$event] } })
35 | }
36 | cached.set(element, callback)
37 | }
38 | await _event.execute.call(this, renderer, element, { ...arguments[2], _callback: cached.get(element)! })
39 | },
40 | } as Directive>
41 |
42 | /** Default exports. */
43 | export default [_header, _body, _http, _response, _http_event]
44 |
--------------------------------------------------------------------------------
/@mizu/http/event/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/http/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/if/README.md:
--------------------------------------------------------------------------------
1 | # `*if="expression"`
2 |
3 | | Version | Phase |
4 | | ----------------------------------- | ------------- |
5 | |  | 23 — `TOGGLE` |
6 |
7 | Conditionally render an element.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!NOTE]
18 | > There is currently no special handling for [``](https://developer.mozilla.org/docs/Web/HTML/Element/template) elements, but future versions may introduce specific behavior for these elements.
19 |
--------------------------------------------------------------------------------
/@mizu/if/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/if",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts",
6 | "./else": "./else/mod.ts"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/@mizu/if/else/README.md:
--------------------------------------------------------------------------------
1 | # `*else="expression"`
2 |
3 | | Version | Phase | Default |
4 | | ---------------------------------------- | ------------- | ------- |
5 | |  | 23 — `TOGGLE` | `true` |
6 |
7 | Conditionally render an element placed after another [`*if`](#if) or [`*else`](#else) directive.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!CAUTION]
18 | > Must be placed immediately after an element with an [`*if`](#if) or [`*else`](#else) directive.
19 |
20 | > [!NOTE]
21 | > There is currently no special handling for [``](https://developer.mozilla.org/docs/Web/HTML/Element/template) elements, but future versions may introduce specific behavior for these elements.
22 |
--------------------------------------------------------------------------------
/@mizu/if/else/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *else ="expression"
3 |
4 | Conditionally render an element placed after another *if
or *else
directive.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Must be placed immediately after an element with an *if
or *else
directive.
13 |
14 |
15 | There is currently no special handling for <template>
elements, but future versions may introduce specific behavior for these elements.
16 |
17 |
18 |
--------------------------------------------------------------------------------
/@mizu/if/else/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Directive, Phase } from "@mizu/internal/engine"
3 | import { _if } from "@mizu/if"
4 | export type * from "@mizu/internal/engine"
5 |
6 | /** `*else` directive. */
7 | export const _else = {
8 | name: "*else",
9 | phase: Phase.TOGGLE,
10 | default: "true",
11 | execute(renderer, element, { attributes: [attribute] }) {
12 | let previous = element.previousSibling as HTMLElement
13 | while (previous) {
14 | // Break on non-empty text nodes
15 | if ((previous.nodeType === renderer.window.Node.TEXT_NODE) && (previous.textContent?.trim())) {
16 | break
17 | }
18 |
19 | // Force directive to `false` when a previous operand is truthy
20 | if ((renderer.isHtmlElement(previous))) {
21 | if (renderer.getAttributes(previous, [_if.name, _else.name] as string[], { first: true })) {
22 | return _if.execute(renderer, element, { ...arguments[2], _directive: { directive: this.name, expression: attribute.value, value: "false" } })
23 | }
24 | break
25 | }
26 |
27 | // Execute directive with given expression when first operand is found and is falsy (meaning all previous operand were falsy too)
28 | if ((renderer.isComment(previous)) && (renderer.getAttributes(renderer.cache("*").get(previous), _if.name, { first: true }))) {
29 | return _if.execute(renderer, element, { ...arguments[2], _directive: { directive: this.name, expression: attribute.value, value: attribute.value || this.default } })
30 | }
31 | previous = (previous as HTMLElement).previousSibling as HTMLElement
32 | }
33 | renderer.warn(`[${this.name}] must be immediately preceded by another [${_if.name}] or [${_else.name}], ignoring`, element)
34 | },
35 | } as Directive
36 |
37 | /** Default exports. */
38 | export default [_if, _else]
39 |
--------------------------------------------------------------------------------
/@mizu/if/else/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/if/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *if ="expression"
3 |
4 | Conditionally render an element.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | There is currently no special handling for <template>
elements, but future versions may introduce specific behavior for these elements.
13 |
14 |
15 |
--------------------------------------------------------------------------------
/@mizu/if/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Directive, Phase } from "@mizu/internal/engine"
3 | export type * from "@mizu/internal/engine"
4 |
5 | /**
6 | * `*if` directive.
7 | *
8 | * @internal `_directive.value` Force the directive to use specified value rather than the attribute value.
9 | * @internal `_directive.expression` Set the original expression for the directive if element is commented out.
10 | * @internal `_directive.directive` Set the original directive name for the directive if element is commented out.
11 | */
12 | export const _if = {
13 | name: "*if",
14 | phase: Phase.TOGGLE,
15 | async execute(renderer, element, { attributes: [attribute], ...options }) {
16 | const result = Boolean(await renderer.evaluate(element, arguments[2]._directive?.value ?? attribute.value, options))
17 | switch (true) {
18 | // Switch comment to element if truthy
19 | case result && (renderer.isComment(element)) && (renderer.cache("*").has(element)): {
20 | const original = renderer.uncomment(element)
21 | return { element: original }
22 | }
23 | // Switch element to comment if falsy
24 | case (!result) && (renderer.isHtmlElement(element)): {
25 | const comment = renderer.comment(element, { expression: attribute.value, directive: _if.name, ...arguments[2]._directive })
26 | return { element: comment, final: true }
27 | }
28 | }
29 | },
30 | } as Directive & { execute: NonNullable }
31 |
32 | /** Default exports. */
33 | export default _if
34 |
--------------------------------------------------------------------------------
/@mizu/if/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | foo
6 |
7 |
8 | foo
9 |
10 |
11 |
12 |
13 |
14 | foo
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
25 |
26 | foo
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 | foo
37 |
38 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/@mizu/if/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/internal/README.md:
--------------------------------------------------------------------------------
1 | # 🌊 mizu.js
2 |
3 | [](https://mizu.sh)
4 |
5 |
6 |
--------------------------------------------------------------------------------
/@mizu/internal/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/internal",
3 | "version": "0.8.2",
4 | "exports": {
5 | "./engine": "./engine/mod.ts",
6 | "./testing": "./testing/mod.ts",
7 | "./vdom": "./vdom/mod.ts",
8 | "./extras/evaluate": "./extras/evaluate.ts"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/@mizu/internal/engine/directive_test.ts:
--------------------------------------------------------------------------------
1 | import "./directive.ts"
2 |
--------------------------------------------------------------------------------
/@mizu/internal/engine/mod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Mizu engine.
3 | * @module
4 | */
5 | export * from "./phase.ts"
6 | export * from "./directive.ts"
7 | export * from "./renderer.ts"
8 |
--------------------------------------------------------------------------------
/@mizu/internal/engine/mod_test.ts:
--------------------------------------------------------------------------------
1 | import "./mod_test.ts"
2 |
--------------------------------------------------------------------------------
/@mizu/internal/engine/phase_test.ts:
--------------------------------------------------------------------------------
1 | import "./phase.ts"
2 |
--------------------------------------------------------------------------------
/@mizu/internal/extras/evaluate.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import type { Nullable, State } from "../engine/mod.ts"
3 | import { Renderer } from "../engine/mod.ts"
4 |
5 | /** Default renderer instance. */
6 | // deno-lint-ignore no-explicit-any
7 | const renderer = await new Renderer(null as any, { directives: [] }).ready
8 |
9 | /**
10 | * Evaluate an expression with given variables and arguments.
11 | *
12 | * This is a convenience function built upon Mizu {@linkcode Renderer.evaluate} and does not require a DOM or an already instantiated renderer.
13 | *
14 | * Both `variables` and `imports` are exposed through {@linkcode https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/with | with} statements,
15 | * meaning that their properties can be accessed directly in the expression without prefixing them.
16 | *
17 | * It is possible to specify the `context` (either `"expression"` or `"function"`).
18 | *
19 | * If set to `"expression"` (default), the expression is evaluated as a regular JavaScript expression.
20 | * ```ts
21 | * console.assert(await evaluate("1 + 1") === 2)
22 | * ```
23 | *
24 | * If set to `"function"`, the expression is automatically wrapped in an async function, effectively treating the expression as a function body.
25 | * It also means that no implicit value is returned, requiring an explicit `return` statement to return a value.
26 | *
27 | * ```ts
28 | * console.assert(await evaluate("return 1 + 1", null, { context: "function" }) === 2)
29 | * ```
30 | *
31 | * > [!IMPORTANT]
32 | * > Note that because evaluation is not performed within a ESM module, it is not possible to use {@linkcode https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/import | import} statement directly (and other ESM-only features).
33 | * > Instead, use the `imports` option which will dynamically import the specified modules and expose them in the evaluation context.
34 |
35 | * Both of these examples are equivalent:
36 | * ```ts
37 | * console.assert(await evaluate("foo.default", null, { imports: { foo: "data:application/javascript,export default true" }}) === true)
38 | * ```
39 | * ```ts
40 | * const foo = await import("data:application/javascript,export default true")
41 | * console.assert(await evaluate("foo.default", { foo }))
42 | * ```
43 | *
44 | * > [!NOTE]
45 | * > The root {@linkcode Renderer.internal} prefix is used internally to manage evaluation state, and thus cannot be used as a variable name.
46 | */
47 | export async function evaluate(expression: string, variables = null as Nullable>, { imports = {} as Record, context = "expression" as "expression" | "function" } = {}): Promise {
48 | if (context === "function") {
49 | expression = `async () =>{${expression}}`
50 | }
51 | variables ??= {}
52 | return await renderer.evaluate(null, expression, {
53 | state: { ...Object.fromEntries(await Promise.all(Object.entries(imports).map(async ([name, value]) => [name, await import(value)]))), ...variables } as State,
54 | args: context === "function" ? [] : undefined,
55 | }) as T
56 | }
57 |
--------------------------------------------------------------------------------
/@mizu/internal/extras/evaluate_test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@libs/testing"
2 | import { evaluate } from "./evaluate.ts"
3 |
4 | test("`evaluates()` evaluates expressions", async () => {
5 | await expect(evaluate("1 + 1")).resolves.toBe(2)
6 | await expect(evaluate("foo + bar", { foo: 1, bar: 2 })).resolves.toBe(3)
7 | await expect(evaluate("function foo() {}")).resolves.toBeInstanceOf(Function)
8 | })
9 |
10 | test("`evaluates()` evaluates expressions in function context", async () => {
11 | await expect(evaluate("1 + 1", null, { context: "function" })).resolves.toBeUndefined()
12 | await expect(evaluate("return 1 + 1", null, { context: "function" })).resolves.toBe(2)
13 | await expect(evaluate("return foo + bar", { foo: 1, bar: 2 }, { context: "function" })).resolves.toBe(3)
14 | await expect(evaluate("return function foo() {}", null, { context: "function" })).resolves.toBeInstanceOf(Function)
15 | })
16 |
17 | test("`evaluates()` supports `imports` options", async () => {
18 | await expect(evaluate("foo.default", null, { imports: { foo: "data:application/javascript,export default 'bar'" } })).resolves.toBe("bar")
19 | })
20 |
--------------------------------------------------------------------------------
/@mizu/internal/testing/fixtures/markup_fmt-v0.13.1.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lowlighter/mizu/cdb3d73afddfa3d3450c6b4a3915464a0caa682b/@mizu/internal/testing/fixtures/markup_fmt-v0.13.1.wasm
--------------------------------------------------------------------------------
/@mizu/internal/testing/format.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { createFromBuffer } from "@dprint/formatter"
3 | import { fromFileUrl } from "@std/path"
4 | // deno-lint-ignore no-external-import
5 | import { readFileSync } from "node:fs"
6 |
7 | /** HTML formatter. */
8 | const formatter = createFromBuffer(readFileSync(fromFileUrl(import.meta.resolve("./fixtures/markup_fmt-v0.13.1.wasm"))))
9 | formatter.setConfig({}, { printWidth: 120, closingBracketSameLine: true, closingTagLineBreakForEmpty: "never", preferAttrsSingleLine: true, whitespaceSensitivity: "ignore" })
10 |
11 | /** Format HTML. */
12 | export function format(html: string): string {
13 | const options = { filePath: "test.html", fileText: html }
14 | return formatter.formatText({ ...options, fileText: formatter.formatText({ ...options, overrideConfig: { printWidth: 0 } }) })
15 | .split("\n")
16 | .filter((line) => line)
17 | .join("\n")
18 | }
19 |
--------------------------------------------------------------------------------
/@mizu/internal/testing/format_test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@libs/testing"
2 | import { format } from "./format.ts"
3 |
4 | test("`format()` formats html and tries to put at most one node per line", () => {
5 | expect(format(`
6 | foo
7 | `.trim())).toBe(`
8 |
9 |
10 | foo
11 |
12 |
13 |
14 | `.trim())
15 | })
16 |
17 | test("`format()` formats html and tries to put empty nodes on the same line", () => {
18 | expect(format(`
19 |
20 |
21 | `.trim())).toBe(`
22 |
23 | `.trim())
24 | })
25 |
--------------------------------------------------------------------------------
/@mizu/internal/testing/mod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Testing utilities.
3 | *
4 | * @module
5 | */
6 | export * from "./format.ts"
7 | export * from "./filter.ts"
8 | export * from "./test.ts"
9 |
--------------------------------------------------------------------------------
/@mizu/internal/testing/mod_test.ts:
--------------------------------------------------------------------------------
1 | import "./mod.ts"
2 |
--------------------------------------------------------------------------------
/@mizu/internal/testing/test_test.ts:
--------------------------------------------------------------------------------
1 | import type { testing } from "@libs/testing"
2 | import { expect, fn, test } from "@libs/testing"
3 | import { test as runner } from "./test.ts"
4 |
5 | test("` ` generates `Deno.test` cases", () => {
6 | const callback = () => () => {}
7 | const test = Object.assign(fn(callback), { skip: fn(callback), only: fn(callback) })
8 | runner(` `, test as testing)
9 | expect(test).toBeCalledTimes(1)
10 | expect(test.skip).not.toBeCalled()
11 | expect(test.only).not.toBeCalled()
12 | })
13 |
14 | test("` ` generates `Deno.test.only()` cases", () => {
15 | const callback = () => () => {}
16 | const test = Object.assign(fn(callback), { skip: fn(callback), only: fn(callback) })
17 | runner(` `, test as testing)
18 | expect(test).not.toBeCalled()
19 | expect(test.skip).not.toBeCalled()
20 | expect(test.only).toBeCalledTimes(1)
21 | })
22 |
23 | test("` ` generates `Deno.test.ignore()` cases", () => {
24 | const callback = () => () => {}
25 | const test = Object.assign(fn(callback), { skip: fn(callback), only: fn(callback) })
26 | runner(` `, test as testing)
27 | expect(test).not.toBeCalled()
28 | expect(test.skip).toBeCalledTimes(1)
29 | expect(test.only).not.toBeCalled()
30 | })
31 |
32 | runner(import.meta.resolve("./fixtures/mod_test.html"))
33 |
--------------------------------------------------------------------------------
/@mizu/internal/vdom/jsdom.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import type { VirtualWindow } from "../engine/mod.ts"
3 | import { JSDOM } from "jsdom"
4 | export type { VirtualWindow }
5 |
6 | /**
7 | * Virtual {@linkcode https://developer.mozilla.org/docs/Web/API/Window | Window} implementation based on {@link https://github.com/jsdom/jsdom | JSDOM}.
8 | *
9 | * ```ts
10 | * await using window = new Window("")
11 | * console.assert(window.document.documentElement.tagName === "HTML")
12 | * ```
13 | */
14 | const Window = (function (content: string) {
15 | const { window } = new JSDOM(content, { url: globalThis.location?.href, contentType: "text/html" })
16 | window[Symbol.asyncDispose] = async () => {
17 | await window.close()
18 | }
19 | return window
20 | }) as unknown as (new (content?: string) => VirtualWindow)
21 | export { Window }
22 |
--------------------------------------------------------------------------------
/@mizu/internal/vdom/jsdom_test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@libs/testing"
2 | import { Window } from "./jsdom.ts"
3 |
4 | test("`Window.constructor()` instantiates a `new Window()`", async () => {
5 | await using window = new Window()
6 | expect(window.document).toBeDefined()
7 | expect(window.location.href).toBe(globalThis.location?.href ?? "about:blank")
8 | })
9 |
--------------------------------------------------------------------------------
/@mizu/internal/vdom/mod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Virtual {@linkcode https://developer.mozilla.org/docs/Web/API/Window | Window} implementations.
3 | *
4 | * They are used to render HTML content in server-side environments.
5 | *
6 | * > [!IMPORTANT]
7 | * > All implementations must closely follow the {@link https://html.spec.whatwg.org/ | HTML specification} to ensure compatibility with different environments.
8 | *
9 | * @module
10 | */
11 | export * from "./jsdom.ts"
12 |
--------------------------------------------------------------------------------
/@mizu/internal/vdom/mod_test.ts:
--------------------------------------------------------------------------------
1 | import "./mod.ts"
2 |
--------------------------------------------------------------------------------
/@mizu/is/README.md:
--------------------------------------------------------------------------------
1 | # `*is="tagname"`
2 |
3 | | Version | Phase |
4 | | ----------------------------------- | --------------- |
5 | |  | 22 — `MORPHING` |
6 |
7 | Set an [element tagname](https://developer.mozilla.org/docs/Web/API/Element/tagName).
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!WARNING]
18 | > If the tagname changes, the reference will also change. Equality checks with elements using this directive may not work as expected. Some directives may be incompatible with this directive.
19 |
--------------------------------------------------------------------------------
/@mizu/is/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/is",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/is/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *is ="tagname"
3 |
4 | Set an element tagname .
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | If the tagname changes, the reference will also change. Equality checks with elements using this directive may not work as expected. Some directives may be incompatible with this directive.
13 |
14 |
15 |
--------------------------------------------------------------------------------
/@mizu/is/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Directive, Phase } from "@mizu/internal/engine"
3 | export type * from "@mizu/internal/engine"
4 |
5 | /** `*is` directive. */
6 | export const _is = {
7 | name: "*is",
8 | phase: Phase.MORPHING,
9 | async execute(renderer, element, { attributes: [attribute], ...options }) {
10 | if (!renderer.isHtmlElement(element)) {
11 | return
12 | }
13 |
14 | // Check whether element needs to be morphed
15 | const tagname = `${await renderer.evaluate(element, attribute.value, options)}`.toLocaleUpperCase()
16 | if (tagname === element.tagName) {
17 | return
18 | }
19 | const original = renderer.cache("*").get(element) ?? element
20 | let morphed = original
21 |
22 | // Create morphed element if tagname doesn't match and keep track of original element
23 | if (tagname !== original.tagName) {
24 | morphed = renderer.createElement(tagname)
25 | renderer.cache("*").set(morphed, original)
26 | }
27 |
28 | // Replace element with morphed element while keeping same children and cloning attributes
29 | if (element !== morphed) {
30 | for (const child of Array.from(element.childNodes) as HTMLElement[]) {
31 | morphed.appendChild(child)
32 | }
33 | Array.from(element.attributes).forEach((attribute) => morphed.attributes.setNamedItem(attribute.cloneNode(true) as Attr))
34 | element.replaceWith(morphed)
35 | }
36 |
37 | // Clean up previous element if it isn't original element
38 | if (element !== original) {
39 | renderer.cache("*").delete(element)
40 | }
41 |
42 | return { element: morphed }
43 | },
44 | } as Directive
45 |
46 | /** Default exports. */
47 | export default _is
48 |
--------------------------------------------------------------------------------
/@mizu/is/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | foo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | foo
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
25 |
26 | foo
27 |
28 |
29 |
30 |
31 |
34 |
35 |
36 | foo
37 |
38 |
39 |
40 |
41 |
46 |
47 |
50 |
51 |
55 |
56 |
57 |
60 |
61 |
65 |
66 |
67 |
68 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
80 |
81 |
82 |
83 |
84 |
85 |
--------------------------------------------------------------------------------
/@mizu/is/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/markdown/README.md:
--------------------------------------------------------------------------------
1 | # `*markdown="content"`
2 |
3 | | Version | Phase | Default |
4 | | ----------------------------------------- | -------------- | ------------------ |
5 | |  | 41 — `CONTENT` | `this.textContent` |
6 |
7 | Set element's [`innerHTML`](https://developer.mozilla.org/docs/Web/API/Element/innerHTML) after performing [markdown](https://github.github.com/gfm/) rendering.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!IMPORTANT]
18 | > This directive dynamically imports [`@libs/markdown`](https://jsr.io/@libs/markdown).
19 |
20 | ## Modifiers
21 |
22 | ### `[string]`
23 |
24 | Load additional Markdown plugins by specifying a comma-separated list (e.g., `*markdown[emojis,highlighting,sanitize]`). See the full list of supported plugins at [`@libs/markdown/plugins`](https://jsr.io/@libs/markdown/doc/plugins/~#Variables). Unsupported plugins will be
25 | silently ignored.
26 |
--------------------------------------------------------------------------------
/@mizu/markdown/build.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { Vendor } from "@tools/vendor_imports.ts"
3 | import { _markdown } from "./mod.ts"
4 | import { fromFileUrl } from "@std/path"
5 | import { command } from "@libs/run/command"
6 | import * as JSONC from "@std/jsonc"
7 |
8 | // Get dependency
9 | const config = fromFileUrl(import.meta.resolve("./deno.jsonc"))
10 | const { imports: { "@libs/markdown": dependency } } = JSONC.parse(await Deno.readTextFile(config)) as { imports: Record }
11 |
12 | // Fetch markdown plugins
13 | const plugins = [] as string[]
14 | const vendor = await new Vendor({ directive: _markdown.name, meta: import.meta, name: "markdown" })
15 | .github({
16 | repository: "lowlighter/libs",
17 | branch: "main",
18 | path: "markdown/plugins",
19 | globs: ["*.ts", "!(*_test.ts)", "!(mod.ts)"],
20 | callback(name) {
21 | plugins.push(name)
22 | },
23 | })
24 |
25 | // Update deno.jsonc
26 | const imports = {
27 | "@libs/markdown": dependency,
28 | "@libs/markdown/plugins/____": `${dependency}/plugins`,
29 | ...Object.fromEntries(plugins.map((name) => [`@libs/markdown/plugins/${name}`, `${dependency}/plugins/${name}`])),
30 | }
31 | await Deno.writeTextFile(config, Deno.readTextFileSync(config).replace(/"imports": {[^}]+}/, `"imports": ${JSON.stringify(imports)}`))
32 | await command("deno", ["fmt", config], { stdout: null, stderr: null })
33 | vendor.log.ok(`updated ${config}`)
34 |
--------------------------------------------------------------------------------
/@mizu/markdown/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/markdown",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | },
7 | "tasks": {
8 | "build": {
9 | "description": "Build dynamic imports and the list of mapped languages",
10 | "command": "deno run --allow-read --allow-net=api.github.com --allow-write=deno.jsonc --allow-run=deno build.ts"
11 | }
12 | },
13 | "imports": {
14 | "@libs/markdown": "jsr:@libs/markdown@1",
15 | "@libs/markdown/plugins/____": "jsr:@libs/markdown@1/plugins",
16 | "@libs/markdown/plugins/anchors": "jsr:@libs/markdown@1/plugins/anchors",
17 | "@libs/markdown/plugins/directives": "jsr:@libs/markdown@1/plugins/directives",
18 | "@libs/markdown/plugins/emojis": "jsr:@libs/markdown@1/plugins/emojis",
19 | "@libs/markdown/plugins/frontmatter": "jsr:@libs/markdown@1/plugins/frontmatter",
20 | "@libs/markdown/plugins/gfm": "jsr:@libs/markdown@1/plugins/gfm",
21 | "@libs/markdown/plugins/highlighting": "jsr:@libs/markdown@1/plugins/highlighting",
22 | "@libs/markdown/plugins/linebreaks": "jsr:@libs/markdown@1/plugins/linebreaks",
23 | "@libs/markdown/plugins/markers": "jsr:@libs/markdown@1/plugins/markers",
24 | "@libs/markdown/plugins/math": "jsr:@libs/markdown@1/plugins/math",
25 | "@libs/markdown/plugins/mermaid": "jsr:@libs/markdown@1/plugins/mermaid",
26 | "@libs/markdown/plugins/ruby": "jsr:@libs/markdown@1/plugins/ruby",
27 | "@libs/markdown/plugins/sanitize": "jsr:@libs/markdown@1/plugins/sanitize",
28 | "@libs/markdown/plugins/uncomments": "jsr:@libs/markdown@1/plugins/uncomments",
29 | "@libs/markdown/plugins/wikilinks": "jsr:@libs/markdown@1/plugins/wikilinks"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/@mizu/markdown/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *markdown ="content"
3 |
4 | Set element's innerHTML
after performing markdown rendering.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | This directive dynamically imports @libs/markdown .
13 |
14 |
15 | Load additional Markdown plugins by specifying a comma-separated list (e.g., *markdown[emojis,highlighting,sanitize]
). See the full list of supported plugins at @libs/markdown/plugins .
16 | Unsupported plugins will be silently ignored.
17 |
18 |
19 |
--------------------------------------------------------------------------------
/@mizu/markdown/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Directive, Phase } from "@mizu/internal/engine"
3 | export type * from "@mizu/internal/engine"
4 |
5 | /** Typings. */
6 | export const typings = {
7 | modifiers: {
8 | trim: { type: Boolean, enforce: true },
9 | },
10 | } as const
11 |
12 | /** `*markdown` directive. */
13 | export const _markdown = {
14 | name: "*markdown",
15 | phase: Phase.CONTENT,
16 | default: "this.textContent",
17 | typings,
18 | async execute(renderer, element, { attributes: [attribute], ...options }) {
19 | if (!renderer.isHtmlElement(element)) {
20 | return
21 | }
22 | const parsed = renderer.parseAttribute(attribute, typings, { modifiers: true })
23 | const { Renderer } = await import("@libs/markdown")
24 | let markdown = Renderer
25 | if (parsed.tag) {
26 | const plugins = await Promise.all(parsed.tag.split(",").map(async (name) => (await import(`@libs/markdown/plugins/${name}`)).default))
27 | markdown = await Renderer.with({ plugins }) as unknown as typeof markdown
28 | }
29 | let content = `${await renderer.evaluate(element, attribute.value || this.default, options)}`
30 | if (parsed.modifiers.trim) {
31 | const trim = content.match(/^[ \t]*\S/m)?.[0].match(/\S/)?.index ?? 0
32 | content = content.replaceAll(new RegExp(`^[ \\t]{${trim}}`, "gm"), "")
33 | }
34 | element.innerHTML = await markdown.render(content)
35 | },
36 | } as Directive & { default: NonNullable }
37 |
38 | /** Default exports. */
39 | export default _markdown
40 |
--------------------------------------------------------------------------------
/@mizu/markdown/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
20 |
27 |
28 |
29 |
30 |
31 | foo
32 |
33 |
34 |
35 |
36 |
37 |
38 | foo
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | :ocean:
52 | :ocean:
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | **foo**
63 |
64 |
65 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
--------------------------------------------------------------------------------
/@mizu/markdown/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/mizu/README.md:
--------------------------------------------------------------------------------
1 | # `*mizu`
2 |
3 | | Version | Phase |
4 | | ------------------------------------- | ----------------- |
5 | |  | 1 — `ELIGIBILITY` |
6 |
7 | Enable _**mizu.js**_ rendering for the element and its children.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!CAUTION]
18 | > For performance reasons, this directive must not have any [`[tag]`](/#concept-directive-tag) or [`.modifiers`](/#concept-directive-modifier). If it does, the directive will be ignored.
19 |
20 | > [!NOTE]
21 | > You can choose whether to require this directive for _**mizu.js**_ rendering with the [`implicit`](https://jsr.io/@mizu/render@0.5.0/doc/engine/~/RendererRenderOptions.implicit) option when using the [user API](/#api-user). By default, rendering is explicit in Client-Side APIs
22 | > and implicit in Server-Side APIs.
23 |
24 | ## Variables
25 |
26 | ### `$root: HTMLElement`
27 |
28 | The closest element that declares a [`*mizu`](#mizu) directive.
29 |
--------------------------------------------------------------------------------
/@mizu/mizu/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/mizu",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/mizu/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *mizu
3 |
4 | Enable mizu.js rendering for the element and its children.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | For performance reasons, this directive must not have any [tag]
or .modifiers
. If it does, the directive will be ignored.
13 |
14 |
15 | You can choose whether to require this directive for mizu.js rendering with the implicit
option when using the user API . By default,
16 | rendering is explicit in Client-Side APIs and implicit in Server-Side APIs.
17 |
18 |
19 | HTMLElement
20 | The closest element that declares a *mizu
directive.
21 |
22 |
23 |
--------------------------------------------------------------------------------
/@mizu/mizu/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Directive, Phase } from "@mizu/internal/engine"
3 | export type * from "@mizu/internal/engine"
4 |
5 | /** `*mizu` directive. */
6 | export const _mizu = {
7 | name: "*mizu",
8 | phase: Phase.ELIGIBILITY,
9 | execute(_, element) {
10 | return { state: { $root: element } }
11 | },
12 | } as Directive & { name: string }
13 |
14 | /** Default exports. */
15 | export default _mizu
16 |
--------------------------------------------------------------------------------
/@mizu/mizu/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
13 |
14 |
15 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
36 |
37 |
38 |
42 | MAIN
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/@mizu/mizu/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/model/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/model",
3 | "version": "0.8.3",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/model/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/mustache/README.md:
--------------------------------------------------------------------------------
1 | # `*mustache`
2 |
3 | | Version | Phase | Multiple |
4 | | ----------------------------------------- | ---------------------------- | -------- |
5 | |  | 42 — `CONTENT_INTERPOLATION` | Yes |
6 |
7 | Enable content interpolation within « mustaches » ( `{{` and `}}`) from [`Text`](https://developer.mozilla.org/docs/Web/API/Text) child nodes.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!NOTE]
18 | > Interpolation occurs only within [`Text`](https://developer.mozilla.org/docs/Web/API/Text) nodes, not the entire element.
19 |
20 | > [!NOTE]
21 | > HTML content is automatically escaped.
22 |
23 | > [!NOTE]
24 | > There is currently no distinction between double mustaches ( `{{` and `}}`) and triple mustaches ( `{{{` and `}}}`), but future versions may introduce specific behavior for these.
25 |
--------------------------------------------------------------------------------
/@mizu/mustache/capture.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import type { Nullable } from "@libs/typing"
3 |
4 | /** Tokens. */
5 | const tokens = {
6 | "(": ")",
7 | "[": "]",
8 | "{": "}",
9 | '"': '"',
10 | "'": "'",
11 | "`": "`",
12 | } as Record
13 |
14 | /**
15 | * Capture content between delimiters.
16 | *
17 | * ```ts
18 | * const { captured, match } = capture("foo {{ bar }} baz")!
19 | * console.assert(captured === "bar")
20 | * console.assert(match === "{{ bar }}")
21 | * ```
22 | *
23 | * @author Simon Lecoq (lowlighter)
24 | * @license MIT
25 | */
26 | export function capture(string: string, offset = 0): Nullable<{ a: number; b: number; match: string; captured: string; triple: boolean }> {
27 | const stack = []
28 | let a = NaN
29 | let d = 2
30 | let quoted = false
31 | for (let i = offset; i < string.length; i++) {
32 | // Start capturing upon meeting mustache opening
33 | if (Number.isNaN(a)) {
34 | if ((string[i] === "{") && (string[i + 1] === "{")) {
35 | if (string[i + 2] === "{") {
36 | d = 3
37 | }
38 | a = i
39 | i += d - 1
40 | }
41 | continue
42 | }
43 | // Close capturing on mustache closing (stack must be empty)
44 | if ((!stack.length) && (string[i] === "}") && (string[i + 1] === "}") && ((d === 2) || (string[i + 2] === "}"))) {
45 | return {
46 | a,
47 | b: i + d,
48 | match: string.slice(a, i + d),
49 | captured: string.slice(a + d, i).trim(),
50 | triple: d === 3,
51 | }
52 | }
53 | // Close group on closing token
54 | if (string[i] === tokens[stack.at(-1)!]) {
55 | stack.pop()
56 | continue
57 | }
58 | // Open group on opening token
59 | if ((!quoted) && (string[i] in tokens)) {
60 | stack.push(string[i])
61 | quoted = ["'", '"', "`"].includes(string[i])
62 | continue
63 | }
64 | }
65 | // Throw on mustache unclosed
66 | if (!Number.isNaN(a)) {
67 | throw new SyntaxError(`Unclosed expression, unterminated expression at: ${a}\n${string.slice(a)}`)
68 | }
69 | return null
70 | }
71 |
--------------------------------------------------------------------------------
/@mizu/mustache/capture_test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@libs/testing"
2 | import { capture } from "./capture.ts"
3 |
4 | test("`capture()` captures content between double delimiters", () => {
5 | expect(capture("foo {{ bar }} baz")).toMatchObject({
6 | captured: "bar",
7 | match: "{{ bar }}",
8 | a: "foo ".length,
9 | b: "foo {{ bar }}".length,
10 | triple: false,
11 | })
12 | })
13 |
14 | test("`capture()` captures content between triple delimiters", () => {
15 | expect(capture("foo {{{ bar }}} baz")).toMatchObject({
16 | captured: "bar",
17 | match: "{{{ bar }}}",
18 | a: "foo ".length,
19 | b: "foo {{{ bar }}}".length,
20 | triple: true,
21 | })
22 | })
23 |
24 | test("`capture()` handles nested delimiters", () => {
25 | for (const d of ["{{", "{{{"]) {
26 | const b = { "{{": "}}", "{{{": "}}}" }[d]
27 | for (const q of ["'", '"', "`"]) {
28 | expect(capture(`foo ${d} ${q}${d} bar ${b}${q} ${b} baz`)).toMatchObject({ captured: `${q}${d} bar ${b}${q}` })
29 | expect(capture(`foo ${d} ${q}bar ${b}${q} ${b} baz`)).toMatchObject({ captured: `${q}bar ${b}${q}` })
30 | expect(capture(`foo ${d} ${q}${d} bar${q} ${b} baz`)).toMatchObject({ captured: `${q}${d} bar${q}` })
31 | }
32 | expect(capture(`${d} [{}, {}] ${b}`)).toMatchObject({ captured: "[{}, {}]" })
33 | expect(capture(`${d} ({}, {}) ${b}`)).toMatchObject({ captured: "({}, {})" })
34 | expect(capture(`${d} {foo:{}}[bar] ${b}`)).toMatchObject({ captured: "{foo:{}}[bar]" })
35 | expect(capture(`${d} (() => {{ 1 }}) ${b}`)).toMatchObject({ captured: "(() => {{ 1 }})" })
36 | }
37 | })
38 |
39 | test("`capture()` throws `new SyntaxError()` on unclosed expressions", () => {
40 | expect(() => capture("foo {{ bar baz")).toThrow(SyntaxError, "Unclosed expression")
41 | })
42 |
--------------------------------------------------------------------------------
/@mizu/mustache/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/mustache",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/mustache/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *mustache
3 |
4 | Enable content interpolation within mustaches ({{
and }}
) from Text
child nodes.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Interpolation occurs only within Text
nodes, not the entire element.
13 |
14 |
15 | HTML content is automatically escaped.
16 |
17 |
18 | There is currently no distinction between double mustaches ({{
and }}
) and triple mustaches ({{{
and }}}
), but future versions may introduce specific behavior for these.
19 |
20 |
21 |
--------------------------------------------------------------------------------
/@mizu/mustache/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Cache, type Directive, Phase } from "@mizu/internal/engine"
3 | import { capture } from "./capture.ts"
4 | export type * from "@mizu/internal/engine"
5 |
6 | /** `*mustache` directive. */
7 | export const _mustache = {
8 | name: "*mustache",
9 | phase: Phase.CONTENT_INTERPOLATION,
10 | multiple: true,
11 | init(renderer) {
12 | renderer.cache>(this.name, new WeakMap())
13 | },
14 | setup(renderer, _, { state }) {
15 | if (state[renderer.internal("mustaching")]) {
16 | return { execute: true }
17 | }
18 | },
19 | async execute(renderer, element, { cache, ...options }) {
20 | if (!renderer.isHtmlElement(element)) {
21 | return
22 | }
23 | await Promise.allSettled(
24 | Array.from(element.childNodes).map(async (node) => {
25 | if (node.nodeType !== renderer.window.Node.TEXT_NODE) {
26 | return
27 | }
28 | const text = node as Text
29 | if (!cache.has(text)) {
30 | cache.set(text, text.textContent!)
31 | }
32 | try {
33 | const template = cache.get(text)!
34 | let templated = template
35 | let offset = 0
36 | let captured = null as ReturnType
37 | // deno-lint-ignore no-cond-assign
38 | while (captured = capture(template, offset)) {
39 | const { b, match, captured: expression } = captured
40 | templated = templated.replace(match, `${await renderer.evaluate(element, expression, options).catch((error) => (renderer.warn(error, element), null)) ?? ""}`)
41 | offset = b
42 | }
43 | text.textContent = templated
44 | } catch (error) {
45 | renderer.warn(`${error}`.split("\n")[0], element)
46 | }
47 | }),
48 | )
49 | return { state: { [renderer.internal("mustaching")]: true } }
50 | },
51 | } as Directive> & { default: NonNullable }
52 |
53 | /** Default exports. */
54 | export default _mustache
55 |
--------------------------------------------------------------------------------
/@mizu/mustache/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/once/README.md:
--------------------------------------------------------------------------------
1 | # `*once`
2 |
3 | | Version | Phase |
4 | | ------------------------------------- | --------------------- |
5 | |  | 99 — `POSTPROCESSING` |
6 |
7 | Render an element once and skip subsequent updates.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
--------------------------------------------------------------------------------
/@mizu/once/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/once",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/once/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *once
3 |
4 | Render an element once and skip subsequent updates.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/@mizu/once/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Cache, type Directive, Phase } from "@mizu/internal/engine"
3 | export type * from "@mizu/internal/engine"
4 |
5 | /** `*once` directive. */
6 | export const _once = {
7 | name: "*once",
8 | phase: Phase.POSTPROCESSING,
9 | init(renderer) {
10 | renderer.cache>(this.name, new WeakSet())
11 | },
12 | setup(_, element, { cache }) {
13 | if (cache.has(element)) {
14 | return false
15 | }
16 | },
17 | cleanup(renderer, element, { cache }) {
18 | let target = element
19 | if ((renderer.isComment(element)) && (renderer.cache("*").has(element))) {
20 | target = renderer.cache("*").get(element)!
21 | }
22 | const attribute = renderer.getAttributes(target, this.name, { first: true })
23 | if (!attribute) {
24 | return
25 | }
26 | cache.add(element)
27 | },
28 | } as Directive>
29 |
30 | /** Default exports. */
31 | export default _once
32 |
--------------------------------------------------------------------------------
/@mizu/once/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 | foo
12 |
13 |
16 |
17 |
18 | foo
19 |
20 |
23 |
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/@mizu/once/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/ref/README.md:
--------------------------------------------------------------------------------
1 | # `*ref="name"`
2 |
3 | | Version | Phase |
4 | | ------------------------------------ | ---------------- |
5 | |  | 82 — `REFERENCE` |
6 |
7 | Create a reference to an element for later use.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!NOTE]
18 | > Redefining a reference will shadow its previous value within the current subtree, without affecting its value in the parent subtree.
19 |
20 | ## Variables
21 |
22 | ### `$refs: Record`
23 |
24 | A collection of all referenced elements within the current subtree.
25 |
26 | ## Modifiers
27 |
28 | ### `.raw[boolean=true]`
29 |
30 | Skip expression evaluation if set.
31 |
--------------------------------------------------------------------------------
/@mizu/ref/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/ref",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/ref/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *ref ="name"
3 |
4 | Create a reference to an element for later use.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Record<PropertyKey, HTMLElement>
13 | A collection of all referenced elements within the current subtree.
14 |
15 |
16 | Redefining a reference will shadow its previous value within the current subtree, without affecting its value in the parent subtree.
17 |
18 |
19 | Skip expression evaluation if set.
20 |
21 |
22 |
--------------------------------------------------------------------------------
/@mizu/ref/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Directive, Phase } from "@mizu/internal/engine"
3 | export type * from "@mizu/internal/engine"
4 |
5 | /** `*ref` typings. */
6 | export const typings = {
7 | modifiers: {
8 | raw: { type: Boolean, enforce: true },
9 | },
10 | } as const
11 |
12 | /** `*ref` directive. */
13 | export const _ref = {
14 | name: "*ref",
15 | phase: Phase.REFERENCE,
16 | typings,
17 | setup(_, __, { state }) {
18 | if (!state.$refs) {
19 | return { state: { $refs: {} } }
20 | }
21 | },
22 | async execute(renderer, element, { attributes: [attribute], state, ...options }) {
23 | const parsed = renderer.parseAttribute(attribute, this.typings, { modifiers: true })
24 | const name = parsed.modifiers.raw ? attribute.value : await renderer.evaluate(element, attribute.value, { state, ...options })
25 | return { state: { $refs: { ...state.$refs as Record, [`${name}`]: element } } }
26 | },
27 | } as Directive
28 |
29 | /** Default exports. */
30 | export default _ref
31 |
--------------------------------------------------------------------------------
/@mizu/ref/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
13 |
14 |
15 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
40 |
43 |
44 |
45 |
48 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/@mizu/ref/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/refresh/README.md:
--------------------------------------------------------------------------------
1 | # `*refresh="interval"`
2 |
3 | | Version | Phase |
4 | | ---------------------------------------- | --------------------- |
5 | |  | 99 — `POSTPROCESSING` |
6 |
7 | Reprocess an element at a specified interval _(in seconds)_.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!WARNING]
18 | > Ensure proper context management to prevent unexpected errors.
19 |
20 | > [!WARNING]
21 | > Avoid using with iterative directives like [`*for`](#for) as [`*refresh`](#refresh) will be duplicated for each generated element.
22 |
23 | > [!NOTE]
24 | > The target element will be rendered regardless of detected changes. This is useful for updating content that cannot be directly observed, but use sparingly to avoid performance issues.
25 |
26 | > [!NOTE]
27 | > Set the interval to `null` to stop refreshing.
28 |
29 | > [!NOTE]
30 | > If the element is commented out by a directive, the refresh is automatically cleared.
31 |
32 | > [!NOTE]
33 | > Refresh operations are performed using [`setTimeout`](https://developer.mozilla.org/docs/Web/API/Window/setTimeout). New calls are scheduled when the directive is processed again, ensuring a consistent interval.
34 |
35 | ## Variables
36 |
37 | ### `$refresh: boolean`
38 |
39 | Indicates if the element is currently being refreshed.
40 |
--------------------------------------------------------------------------------
/@mizu/refresh/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/refresh",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/refresh/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *refresh ="interval"
3 |
4 | Reprocess an element at a specified interval (in seconds) .
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | Ensure proper context management to prevent unexpected errors.
13 |
14 |
15 | Avoid using with iterative directives like *for
as *refresh
will be duplicated for each generated element.
16 |
17 |
18 | The target element will be rendered regardless of detected changes. This is useful for updating content that cannot be directly observed, but use sparingly to avoid performance issues.
19 |
20 |
21 | Set the interval to null
to stop refreshing.
22 |
23 |
24 | If the element is commented out by a directive, the refresh is automatically cleared.
25 |
26 |
27 | Refresh operations are performed using setTimeout
. New calls are scheduled when the directive is processed again, ensuring a consistent interval.
28 |
29 |
30 | boolean
31 | Indicates if the element is currently being refreshed.
32 |
33 |
34 |
--------------------------------------------------------------------------------
/@mizu/refresh/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Cache, type Directive, Phase } from "@mizu/internal/engine"
3 | export type * from "@mizu/internal/engine"
4 |
5 | /** `*refresh` directive. */
6 | export const _refresh = {
7 | name: "*refresh",
8 | phase: Phase.POSTPROCESSING,
9 | init(renderer) {
10 | renderer.cache>(this.name, new WeakMap())
11 | },
12 | setup(_, __, { state }) {
13 | if (!("$refresh" in state)) {
14 | Object.assign(state, { $refresh: false })
15 | }
16 | return { state }
17 | },
18 | async execute(renderer, element, { attributes: [attribute], cache, ...options }) {
19 | const value = await renderer.evaluate(element, attribute.value, options) as string
20 |
21 | // Clear interval if value is null
22 | if (value === null) {
23 | clearTimeout(cache.get(element)?.id)
24 | cache.delete(element)
25 | return
26 | }
27 |
28 | // Setup interval configuration for later use
29 | const interval = Number.parseInt(`${1000 * Number(value)}`)
30 | if ((Number.isNaN(interval)) || (interval <= 0)) {
31 | renderer.warn(`[${this.name}] expects a finite positive number but got ${value}, ignoring`, element)
32 | return
33 | }
34 | const cached = cache.get(element) ?? cache.set(element, { interval, id: NaN }).get(element)!
35 | if (((cached.interval !== interval) && (!Number.isNaN(cached.id))) || (options.state[renderer.internal("refreshing")])) {
36 | clearTimeout(cached.id)
37 | cached.id = NaN
38 | }
39 | },
40 | cleanup(renderer, element, { cache, ...options }) {
41 | // Cleanup interval from commented out elements
42 | if ((renderer.isComment(element)) && (cache.has(renderer.cache("*").get(element)!))) {
43 | element = renderer.cache("*").get(element)!
44 | clearTimeout(cache.get(element)?.id)
45 | cache.delete(element)
46 | return
47 | }
48 |
49 | // Setup interval if needed
50 | if (!Number.isNaN(cache.get(element)?.id)) {
51 | return
52 | }
53 | cache.get(element)!.id = setTimeout(() => {
54 | if (element.isConnected) {
55 | renderer.render(element as HTMLElement, { ...options, state: { ...options.state, $refresh: true, [renderer.internal("refreshing")]: true } })
56 | }
57 | }, cache.get(element)!.interval)
58 | },
59 | } as Directive>
60 |
61 | /** Default exports. */
62 | export default _refresh
63 |
--------------------------------------------------------------------------------
/@mizu/refresh/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/render/client/client_test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@libs/testing"
2 | import { Window } from "@mizu/internal/vdom"
3 | import { Client } from "./client.ts"
4 |
5 | test("`Client.render()` renders dom content", async () => {
6 | await using window = new Window(` `)
7 | Client.defaults.directives.push("@mizu/test")
8 | const mizu = new Client({ context: { foo: "" }, window })
9 | await mizu.render()
10 | expect(window.document.querySelector("a")?.textContent).toBe("")
11 | expect(window.document.querySelector("b")?.textContent).toBe("")
12 | expect(window.document.querySelector("c")?.textContent).toBe("client")
13 |
14 | await mizu.render(undefined, { context: { foo: "bar" }, state: { $renderer: "custom" } })
15 | expect(window.document.querySelector("a")?.textContent).toBe("bar")
16 | expect(window.document.querySelector("b")?.textContent).toBe("")
17 | expect(window.document.querySelector("c")?.textContent).toBe("custom")
18 |
19 | mizu.context.foo = "baz"
20 | await mizu.render()
21 | expect(window.document.querySelector("a")?.textContent).toBe("baz")
22 | expect(window.document.querySelector("b")?.textContent).toBe("")
23 | expect(window.document.querySelector("c")?.textContent).toBe("client")
24 |
25 | await mizu.flush()
26 | })
27 |
--------------------------------------------------------------------------------
/@mizu/render/client/defaults.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import type { Directive } from "@mizu/internal/engine"
3 | import _mizu from "@mizu/mizu"
4 | import _bind from "@mizu/bind"
5 | import _code from "@mizu/code"
6 | import _custom_element from "@mizu/custom-element"
7 | import _eval from "@mizu/eval"
8 | import _event from "@mizu/event"
9 | import _for from "@mizu/for/empty"
10 | import _html from "@mizu/html"
11 | import _http from "@mizu/http/event"
12 | import _if from "@mizu/if/else"
13 | import _model from "@mizu/model"
14 | import _mustache from "@mizu/mustache"
15 | import _once from "@mizu/once"
16 | import _ref from "@mizu/ref"
17 | import _refresh from "@mizu/refresh"
18 | import _set from "@mizu/set"
19 | import _show from "@mizu/show"
20 | import _skip from "@mizu/skip"
21 | import _text from "@mizu/text"
22 |
23 | /** Defaults directives. */
24 | export default [
25 | _mizu,
26 | _bind,
27 | _code,
28 | _custom_element,
29 | _eval,
30 | _event,
31 | _for,
32 | _html,
33 | _http,
34 | _if,
35 | _model,
36 | _mustache,
37 | _once,
38 | _ref,
39 | _refresh,
40 | _set,
41 | _show,
42 | _skip,
43 | _text,
44 | ].flat(Infinity) as Directive[]
45 |
--------------------------------------------------------------------------------
/@mizu/render/client/defaults_test.ts:
--------------------------------------------------------------------------------
1 | import "./defaults.ts"
2 |
--------------------------------------------------------------------------------
/@mizu/render/client/mod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Mizu client-side renderer.
3 | * @module
4 | */
5 | import { Client } from "./client.ts"
6 | export { Client as Mizu } from "./client.ts"
7 | export type * from "./client.ts"
8 |
9 | /** Default Mizu {@linkcode Client} instance. */
10 | export default Client.default as Client
11 |
12 | // Start the client-side renderer if this module is the main entry point
13 | // @ts-expect-error: iife handling
14 | if ((globalThis.MIZU_IIFE) && (globalThis.window?.document)) {
15 | Client.default.render()
16 | }
17 |
--------------------------------------------------------------------------------
/@mizu/render/client/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { expect, runtime, test, type testing } from "@libs/testing"
2 | import { Window } from "@mizu/internal/vdom"
3 |
4 | if (runtime === "deno") {
5 | test("`Client.render()` is automatically called in iife mode", async () => {
6 | try {
7 | await using window = new Window(`
`)
8 | Object.assign(globalThis, { MIZU_IIFE: true, window })
9 | await import("./mod.ts")
10 | expect(window.document.querySelector("p")?.textContent).toBe("foo")
11 | } finally {
12 | delete (globalThis as testing).MIZU_IIFE
13 | delete (globalThis as testing).window
14 | }
15 | })
16 | }
17 |
--------------------------------------------------------------------------------
/@mizu/render/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/render",
3 | "version": "0.8.5",
4 | "exports": {
5 | "./client": "./client/mod.ts",
6 | "./client/defaults": "./client/defaults.ts",
7 | "./server": "./server/mod.ts",
8 | "./server/defaults": "./server/defaults.ts"
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/@mizu/render/server/defaults.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import type { Directive } from "@mizu/internal/engine"
3 | import _mizu from "@mizu/mizu"
4 | import _bind from "@mizu/bind"
5 | import _clean from "@mizu/clean"
6 | import _code from "@mizu/code"
7 | import _custom_element from "@mizu/custom-element"
8 | import _eval from "@mizu/eval"
9 | import _for from "@mizu/for/empty"
10 | import _html from "@mizu/html"
11 | import _http from "@mizu/http"
12 | import _if from "@mizu/if/else"
13 | import _is from "@mizu/is"
14 | import _markdown from "@mizu/markdown"
15 | import _mustache from "@mizu/mustache"
16 | import _once from "@mizu/once"
17 | import _ref from "@mizu/ref"
18 | import _set from "@mizu/set"
19 | import _show from "@mizu/show"
20 | import _skip from "@mizu/skip"
21 | import _text from "@mizu/text"
22 | import _toc from "@mizu/toc"
23 |
24 | /** Defaults directives. */
25 | export default [
26 | _mizu,
27 | _bind,
28 | _clean,
29 | _code,
30 | _custom_element,
31 | _eval,
32 | _for,
33 | _html,
34 | _http,
35 | _if,
36 | _is,
37 | _markdown,
38 | _mustache,
39 | _once,
40 | _ref,
41 | _set,
42 | _show,
43 | _skip,
44 | _text,
45 | _toc,
46 | ].flat(Infinity) as Directive[]
47 |
--------------------------------------------------------------------------------
/@mizu/render/server/defaults_test.ts:
--------------------------------------------------------------------------------
1 | import "./defaults.ts"
2 |
--------------------------------------------------------------------------------
/@mizu/render/server/mod.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Mizu server-side renderer.
3 | * @module
4 | */
5 | import { Server } from "./server.ts"
6 | export { Server as Mizu } from "./server.ts"
7 | export type * from "./server.ts"
8 |
9 | /** Default Mizu {@linkcode Server} instance. */
10 | export default Server.default as Server
11 |
--------------------------------------------------------------------------------
/@mizu/render/server/mod_test.ts:
--------------------------------------------------------------------------------
1 | import "./mod.ts"
2 |
--------------------------------------------------------------------------------
/@mizu/render/server/server_test.ts:
--------------------------------------------------------------------------------
1 | import { expect, test } from "@libs/testing"
2 | import { Window } from "@mizu/internal/vdom"
3 | import { Server } from "./server.ts"
4 |
5 | test("`Server.render()` renders content", async () => {
6 | const html = ` `
7 | const mizu = new Server({ directives: ["@mizu/test"], context: { foo: "" } })
8 | await expect(mizu.render(html, { select: "body" })).resolves.toBe(`server `)
9 | await expect(mizu.render(html, { select: "body", context: { foo: "bar" }, state: { $renderer: "custom" } })).resolves.toBe(`bar bar custom `)
10 | })
11 |
12 | test("`Server.render()` renders virtual nodes", async () => {
13 | await using window = new Window(` `)
14 | const mizu = new Server({ directives: ["@mizu/test"], context: { foo: "bar" } })
15 | await expect(mizu.render(window.document.documentElement, { select: "body" })).resolves.toBe(`bar `)
16 | })
17 |
18 | test("`Server.render()` returns doctype when no selector is passed", async () => {
19 | const html = ``
20 | const mizu = new Server()
21 | await expect(mizu.render(html)).resolves.toMatch(/^/)
22 | })
23 |
24 | test("`Server.context` can be edited", async () => {
25 | await using window = new Window(` `)
26 | const mizu = new Server({ directives: ["@mizu/test"] })
27 | expect(mizu.context).toEqual({})
28 | mizu.context = { foo: "bar" }
29 | expect(mizu.context).toEqual({ foo: "bar" })
30 | await expect(mizu.render(window.document.documentElement, { select: "body" })).resolves.toBe(`bar `)
31 | })
32 |
--------------------------------------------------------------------------------
/@mizu/set/README.md:
--------------------------------------------------------------------------------
1 | # `*set="context"`
2 |
3 | | Version | Phase |
4 | | ------------------------------------ | -------------- |
5 | |  | 11 — `CONTEXT` |
6 |
7 | Set context values for an element and its children.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!CAUTION]
18 | > The context must resolve to a [JavaScript `Object`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object).
19 |
20 | > [!NOTE]
21 | > The context is initialized once and persists across renderings, but it can still be updated by other directives.
22 |
--------------------------------------------------------------------------------
/@mizu/set/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/set",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/set/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *set ="context"
3 |
4 | Set context values for an element and its children.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | The context must resolve to a JavaScript Object
.
13 |
14 |
15 | The context is initialized once and persists across renderings, but it can still be updated by other directives.
16 |
17 |
18 |
--------------------------------------------------------------------------------
/@mizu/set/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Cache, type Context, type Directive, type Nullable, Phase } from "@mizu/internal/engine"
3 | export type * from "@mizu/internal/engine"
4 |
5 | /** `*set` directive. */
6 | export const _set = {
7 | name: "*set",
8 | phase: Phase.CONTEXT,
9 | init(renderer) {
10 | renderer.cache>(this.name, new WeakMap())
11 | },
12 | async execute(renderer, element, { attributes: [attribute], cache, ...options }) {
13 | if (!cache.has(element)) {
14 | const context = await renderer.evaluate(element, attribute.value, options)
15 | if (typeof context !== "object") {
16 | renderer.warn(`[${this.name}] expects an object but got ${typeof context}, ignoring`, element)
17 | return
18 | }
19 | cache.set(element, context ? options.context.with(context as Record) : null)
20 | }
21 | return { context: cache.get(element) }
22 | },
23 | } as Directive>>
24 |
25 | /** Default exports. */
26 | export default _set
27 |
--------------------------------------------------------------------------------
/@mizu/set/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 | bar
13 | foo
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | foobar
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | foo
40 | bar
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | 1970-01-01T00:00:00.000Z
51 |
52 |
53 |
54 |
55 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
67 |
68 |
69 | true
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | baz
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/@mizu/set/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/show/README.md:
--------------------------------------------------------------------------------
1 | # `*show="expression"`
2 |
3 | | Version | Phase | Default |
4 | | ------------------------------------- | -------------- | ------- |
5 | |  | 71 — `DISPLAY` | `true` |
6 |
7 | Conditionally display an element.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!NOTE]
18 | > When hidden, the element's [CSS `display` property](https://developer.mozilla.org/docs/Web/CSS/display) is set to `none !important`.
19 |
20 | > [!NOTE]
21 | > When shown and if initially hidden by a CSS stylesheet ( `display: none`), the element's display property is reset to `initial !important`.
22 |
23 | > [!NOTE]
24 | > Unlike [`*if`](#if) and [`*else`](#else) directives, the element remains in the DOM when hidden.
25 |
26 | > [!NOTE]
27 | > You can take advantage of the default value being `true` to hide elements before _**mizu.js**_ loads (e.g. ``).
28 |
--------------------------------------------------------------------------------
/@mizu/show/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/show",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/show/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *show ="expression"
3 |
4 | Conditionally display an element.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | When hidden, the element's CSS display
property is set to none !important
.
13 |
14 |
15 | When shown and if initially hidden by a CSS stylesheet (display: none
), the element's display property is reset to initial !important
.
16 |
17 |
18 | Unlike *if
and *else
directives, the element remains in the DOM when hidden.
19 |
20 |
21 | You can take advantage of the default value being true
to hide elements before mizu.js loads (e.g. <style>[\*show]{display:none}</style>
).
22 |
23 |
24 |
--------------------------------------------------------------------------------
/@mizu/show/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Directive, Phase } from "@mizu/internal/engine"
3 | export type * from "@mizu/internal/engine"
4 |
5 | /** `*show` directive. */
6 | export const _show = {
7 | name: "*show",
8 | phase: Phase.DISPLAY,
9 | default: "true",
10 | async execute(renderer, element, { attributes: [attribute], ...options }) {
11 | if (!renderer.isHtmlElement(element)) {
12 | return
13 | }
14 | const result = Boolean(await renderer.evaluate(element, attribute.value || this.default, options))
15 | if (result) {
16 | element.style.removeProperty("display")
17 | if (!element.style.length) {
18 | element.removeAttribute("style")
19 | }
20 | if (renderer.window.getComputedStyle(element).display === "none") {
21 | element.style.setProperty("display", "initial", "important")
22 | }
23 | } else {
24 | element.style.setProperty("display", "none", "important")
25 | }
26 | },
27 | } as Directive & { default: NonNullable }
28 |
29 | /** Default exports. */
30 | export default _show
31 |
--------------------------------------------------------------------------------
/@mizu/show/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | foo
6 |
7 |
8 | foo
9 |
10 |
11 |
12 |
13 |
14 | foo
15 |
16 |
17 | foo
18 |
19 |
20 |
21 |
22 |
23 |
28 | foo
29 |
30 |
31 | foo
32 |
33 |
34 |
35 |
36 |
39 |
40 | foo
41 |
42 |
43 | foo
44 |
45 |
48 |
49 |
50 | foo
51 |
52 |
55 |
56 |
57 | foo
58 |
59 |
60 |
61 |
62 |
63 | foo
64 |
65 |
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/@mizu/show/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/skip/README.md:
--------------------------------------------------------------------------------
1 | # `*skip`
2 |
3 | | Version | Phase |
4 | | ------------------------------------- | ------------------- |
5 | |  | 2 — `PREPROCESSING` |
6 |
7 | Prevent an element from being processed.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
--------------------------------------------------------------------------------
/@mizu/skip/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/skip",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/skip/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *skip
3 |
4 | Prevent an element from being processed.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/@mizu/skip/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Directive, Phase } from "@mizu/internal/engine"
3 | export type * from "@mizu/internal/engine"
4 |
5 | /** `*skip` directive. */
6 | export const _skip = {
7 | name: "*skip",
8 | phase: Phase.PREPROCESSING,
9 | setup(renderer, element) {
10 | if ((renderer.isHtmlElement(element)) && (element.hasAttribute(this.name))) {
11 | return false
12 | }
13 | },
14 | } as Directive & { name: string }
15 |
16 | /** Default exports. */
17 | export default _skip
18 |
--------------------------------------------------------------------------------
/@mizu/skip/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | bar
6 |
7 |
8 | foo
9 |
10 |
11 | bar
12 |
13 |
14 | bar
15 |
16 |
17 |
--------------------------------------------------------------------------------
/@mizu/skip/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/test/README.md:
--------------------------------------------------------------------------------
1 | # `~test="expression"`
2 |
3 | | Version | Phase | Multiple |
4 | | ------------------------------------- | -------------- | -------- |
5 | |  | 10 — `TESTING` | Yes |
6 |
7 | Special directive for testing purposes.
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!WARNING]
18 | > For testing only. Use this directive to isolate and test custom directives without relying on others.
19 |
20 | > [!WARNING]
21 | > The modifiers may not be compatible with each other.
22 |
23 | ## Modifiers
24 |
25 | ### `[string]`
26 |
27 | Specify any existing [Phase](#concept-rendering-phase) name (e.g., `~test [testing]`, defaults to `Phase.TESTING`). The directive will execute during the specified phase before any other directive in that phase, allowing you to simulate specific scenarios.
28 |
29 | ### `.text[boolean]`
30 |
31 | Set the element's [`textContent`](https://developer.mozilla.org/docs/Web/API/Node/textContent) with the expression result.
32 |
33 | ### `.eval[boolean]`
34 |
35 | Evaluate a JavaScript expression within the element's context.
36 |
37 | ### `.comment[boolean]`
38 |
39 | Convert the element to a [`Comment`](https://developer.mozilla.org/docs/Web/API/Comment) if the expression is truthy, and revert it otherwise.
40 |
41 | ### `.throw[boolean]`
42 |
43 | Throw an [`EvalError`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/EvalError) if the expression is truthy.
44 |
--------------------------------------------------------------------------------
/@mizu/test/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/test",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/test/mod.html:
--------------------------------------------------------------------------------
1 |
2 | ~test ="expression"
3 |
4 | Special directive for testing purposes.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | For testing only. Use this directive to isolate and test custom directives without relying on others.
13 |
14 |
15 | The modifiers may not be compatible with each other.
16 |
17 |
18 | string
19 | Specify any existing Phase name (e.g., ~test[testing]
, defaults to Phase.TESTING
). The directive will execute during the specified phase before any
20 | other directive in that phase, allowing you to simulate specific scenarios.
21 |
22 |
23 | Set the element's textContent
with the expression result.
24 |
25 |
26 | Evaluate a JavaScript expression within the element's context.
27 |
28 |
29 | Convert the element to a Comment
if the expression is truthy, and revert it otherwise.
30 |
31 |
32 | Throw an EvalError
if the expression is truthy.
33 |
34 |
35 |
--------------------------------------------------------------------------------
/@mizu/test/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | foo
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
64 |
65 |
66 |
67 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
82 |
85 |
86 |
87 |
88 |
92 |
93 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 | foo
104 |
105 |
106 |
--------------------------------------------------------------------------------
/@mizu/test/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/text/README.md:
--------------------------------------------------------------------------------
1 | # `*text="content"`
2 |
3 | | Version | Phase | Default |
4 | | ------------------------------------- | -------------- | ---------------- |
5 | |  | 41 — `CONTENT` | `this.innerHTML` |
6 |
7 | Set element's [`textContent`](https://developer.mozilla.org/docs/Web/API/Node/textContent).
8 |
9 | ```html
10 |
11 |
12 |
13 | ```
14 |
15 | ## Notes
16 |
17 | > [!NOTE]
18 | > HTML content is automatically escaped.
19 |
20 | > [!NOTE]
21 | > Without an attribute value, this directive escapes the element's [`innerHTML`](https://developer.mozilla.org/docs/Web/API/Element/innerHTML) _(e.g., ` ` becomes `<b></b> `)_.
22 |
--------------------------------------------------------------------------------
/@mizu/text/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mizu/text",
3 | "version": "0.8.2",
4 | "exports": {
5 | ".": "./mod.ts"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/@mizu/text/mod.html:
--------------------------------------------------------------------------------
1 |
2 | *text ="content"
3 |
4 | Set element's textContent
.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | HTML content is automatically escaped.
13 |
14 |
15 | Without an attribute value, this directive escapes the element's innerHTML
16 | (e.g., <a *text><b></b></a>
becomes <a *text><b></b></a>
) .
17 |
18 |
19 |
--------------------------------------------------------------------------------
/@mizu/text/mod.ts:
--------------------------------------------------------------------------------
1 | // Imports
2 | import { type Directive, Phase } from "@mizu/internal/engine"
3 | export type * from "@mizu/internal/engine"
4 |
5 | /** `*text` directive. */
6 | export const _text = {
7 | name: "*text",
8 | phase: Phase.CONTENT,
9 | default: "this.innerHTML",
10 | async execute(renderer, element, { attributes: [attribute], ...options }) {
11 | if (!renderer.isHtmlElement(element)) {
12 | return
13 | }
14 | element.textContent = `${await renderer.evaluate(element, attribute.value || this.default, options)}`
15 | },
16 | } as Directive & { default: NonNullable }
17 |
18 | /** Default exports. */
19 | export default _text
20 |
--------------------------------------------------------------------------------
/@mizu/text/mod_test.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | foo
9 |
10 |
11 |
12 |
13 |
16 |
17 |
18 |
19 |
20 | <b>foo</b>
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | <p>foo</p>
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/@mizu/text/mod_test.ts:
--------------------------------------------------------------------------------
1 | import { test } from "@mizu/internal/testing"
2 | test(import.meta)
3 |
--------------------------------------------------------------------------------
/@mizu/toc/README.md:
--------------------------------------------------------------------------------
1 | # `*toc="selector"`
2 |
3 | | Version | Phase | Default |
4 | | ------------------------------------ | -------------- | -------- |
5 | |  | 41 — `CONTENT` | `'main'` |
6 |
7 | Create a table of contents from [`