1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 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 60 61 62 63 64 65 66 67 68 69 70 71 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 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 |
x41 x41 |
// Imports
import type { DeepReadonly, Promisable } from "@libs/typing/types"
import type { AttrTypings, Context, InferAttrTypings, InitialContextState, Renderer, State } from "./renderer.ts"
import { Phase } from "./phase.ts"
export { Phase }
export type { DeepReadonly, Promisable }
/**
* A directive implements a custom behaviour for a matching {@link https://developer.mozilla.org/docs/Web/HTML/Attributes | HTML attribute}.
*
* For more information, see the {@link https://mizu.sh/#concept-directive | mizu.sh documentation}.
*/
// deno-lint-ignore no-explicit-any
export interface Directive<Cache = any, Typings extends AttrTypings = any> {
/**
* Directive name.
*
* The {@linkcode Renderer.render()} method uses this value to determine whether {@linkcode Directive.execute()} should be called for the processed node.
*
* The name should be prefixed to avoid conflicts with regular attribute names and must be unique among other {@linkcode Renderer.directives}.
* {@linkcode Renderer.load()} will use this value to check whether the directive is already loaded.
*
* If the directive name is dynamic, a `RegExp` may be used instead of a `string`.
* In this case, {@linkcode Directive.prefix} should be specified.
*
* ```ts
* const foo = {
* name: "*foo",
* phase: Phase.UNKNOWN,
* } as Directive & { name: string }
* ```
*/
readonly name: string | RegExp
/**
* Directive prefix.
*
* It is used as a hint for {@linkcode Renderer.parseAttribute()} to strip prefix from {@linkcode https://developer.mozilla.org/docs/Web/API/Attr/name | Attr.name} when parsing the directive.
*
* If {@linkcode Directive.name} is a `RegExp`, this property shoud be specified.
*
* ```ts
* const foo = {
* name: /^~(?<bar>)/,
* prefix: "~",
* phase: Phase.UNKNOWN,
* } as Directive & { name: RegExp, prefix: string }
* ```
*/
readonly prefix?: string
/**
* Directive phase.
*
* Directives are executed in ascending order based on their {@linkcode Phase}.
*
* > [!IMPORTANT]
* > Directives with {@linkcode Phase.UNKNOWN} and {@linkcode Phase.META} are ignored by {@linkcode Renderer.load()}.
* >
* > {@linkcode Phase.TESTING} is intended for testing purposes only.
*
* For more information, see the {@link https://mizu.sh/#concept-renderering-phase | mizu.sh documentation}.
*
* ```ts
* const foo = {
* name: "*foo",
* phase: Phase.CONTENT,
* } as Directive & { name: string }
* ```
*/
readonly phase: Phase
/**
* Indicates whether the directive can be specified multiple times on the same node.
*
* If set to `false`, a warning will be issued to users attempting to apply it more than once.
*
* ```ts
* const foo = {
* name: /^\/(?<value>)/,
* prefix: "/",
* phase: Phase.UNKNOWN,
* multiple: true
* } as Directive & { name: RegExp; prefix: string }
* ```
*/
readonly multiple?: boolean
/**
* Typings for directive parsing.
*
* For more information, see {@linkcode Renderer.parseAttribute()}.
*
* ```ts
* const typings = {
* type: Boolean,
* modifiers: {
* foo: { type: Boolean, default: false },
* }
* }
*
* const foo = {
* name: "*foo",
* phase: Phase.UNKNOWN,
* typings,
* async execute(renderer, element, { attributes: [ attribute ], ...options }) {
* console.log(renderer.parseAttribute(attribute, this.typings, { modifiers: true }))
* }
* } as Directive<null, typeof typings> & { name: string }
* ```
*/
readonly typings?: Typings
/**
* Default value.
*
* This value should be used by directive callbacks when the {@linkcode https://developer.mozilla.org/docs/Web/API/Attr/value | Attr.value} is empty.
*
* ```ts
* const foo = {
* name: "*foo",
* phase: Phase.UNKNOWN,
* default: "bar",
* async execute(renderer, element, { attributes: [ attribute ], ...options }) {
* console.log(attribute.value || this.default)
* }
* } as Directive & { name: string; default: string }
* ```
*/
readonly default?: string
/**
* Directive initialization callback.
*
* This callback is executed once during when {@linkcode Renderer.load()} loads the directive.
* It should be used to set up dependencies, instantiate directive-specific caches (via {@linkcode Renderer.cache()}), and perform other initialization tasks.
*
* If a cache is instantiated, it is recommended to use the `Directive<Cache>` generic type to ensure type safety when accessing it in {@linkcode Directive.setup()}, {@linkcode Directive.execute()}, and {@linkcode Directive.cleanup()}.
*
* ```ts
* const foo = {
* name: "*foo",
* phase: Phase.UNKNOWN,
* async init(renderer) {
* renderer.cache(this.name, new WeakSet())
* },
* } as Directive<WeakSet<HTMLElement | Comment>> & { name: string }
* ```
*/
readonly init?: (renderer: Renderer) => Promisable<void>
/**
* Directive setup callback.
*
* This callback is executed during {@linkcode Renderer.render()} before any {@linkcode Directive.execute()} calls.
*
* A partial object can be returned to update the rendering {@linkcode State}, and the eligibility.
*
* If `false` is returned, the entire rendering process for this node is halted.
*
* > [!IMPORTANT]
* > This method is executed regardless of the directive's presence on the node.
*
* ```ts
* const foo = {
* name: "*foo",
* phase: Phase.UNKNOWN,
* async setup(renderer, element, { cache, context, state }) {
* if ((!renderer.isHtmlElement(element)) || (element.hasAttribute("no-render"))) {
* return false
* }
* },
* } as Directive & { name: string }
* ```
*/
readonly setup?: (renderer: Renderer, element: HTMLElement | Comment, _: { cache: Cache; context: Context; state: DeepReadonly<State>; root: InitialContextState }) => Promisable<void | Partial<{ state: State; execute: boolean } | false>>
/**
* Directive execution callback.
*
* This callback is executed during {@linkcode Renderer.render()} if the rendered node has been marked as eligible.
*
* A node is considered eligible if at least one of the following conditions is met:
* - {@linkcode Directive.setup()} returned `{ execute: true }`.
* - {@linkcode Directive.setup()} did not return an `execute` value and the element has at least one attribute matching the directive name.
*
* A partial object can be returned to update the rendering {@linkcode Context}, {@linkcode State}, and the rendered {@linkcode https://developer.mozilla.org/docs/Web/API/HTMLElement | HTMLElement} (or {@linkcode https://developer.mozilla.org/docs/Web/API/Comment | Comment}).
*
* If `final: true` is returned, the rendering process for this node is stopped (all {@linkcode Directive.cleanup()} will still be called).
*
* ```ts
* const foo = {
* name: "*foo",
* phase: Phase.UNKNOWN,
* async execute(renderer, element, { attributes: [ attribute ], ...options }) {
* console.log(`${await renderer.evaluate(element, attribute.value || "''", options)}`)
* return { state: { $foo: true } }
* },
* } as Directive & { name: string }
* ```
*/
readonly execute?: (
renderer: Renderer,
element: HTMLElement | Comment,
_: { cache: Cache; context: Context; state: DeepReadonly<State>; attributes: Readonly<Attr[]>; root: InitialContextState },
) => Promisable<void | Partial<{ element: HTMLElement | Comment; context: Context; state: State; final: boolean }>>
/**
* Directive cleanup callback.
*
* This callback is executed during {@linkcode Renderer.render()} after all {@linkcode Directive.execute()} have been applied and all {@linkcode https://developer.mozilla.org/docs/Web/API/Node/childNodes | Element.childNodes} have been processed.
*
* > [!IMPORTANT]
* > This method is executed regardless of the directive's presence on the node, and regardless of whether a {@linkcode Directive.execute()} returned with `final: true`.
*
* ```ts
* const foo = {
* name: "*foo",
* phase: Phase.UNKNOWN,
* async cleanup(renderer, element, { cache, context, state }) {
* console.log("Cleaning up")
* }
* } as Directive & { name: string }
* ```
*/
readonly cleanup?: (renderer: Renderer, element: HTMLElement | Comment, _: { cache: Cache; context: Context; state: DeepReadonly<State>; root: InitialContextState }) => Promisable<void>
}
/** Extracts the cache type from a {@linkcode Directive}. */
export type Cache<T> = T extends Directive<infer U> ? U : never
/** Extracts the typings values from a {@linkcode Directive}. */
// deno-lint-ignore no-explicit-any
export type Modifiers<T> = T extends Directive<any, infer U> ? InferAttrTypings<U>["modifiers"] : never
|