All files / internal / engine / directive.ts

100.00% Branches 0/0
100.00% Lines 2/2
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