Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(mizu): add *mizu directive and core engine #1

Merged
merged 1 commit into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
248 changes: 248 additions & 0 deletions @mizu/mizu/core/engine/directive.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
// 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.
*
* @example
* ```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/en-US/docs/Web/API/Attr/name | Attr.name} when parsing the directive.
*
* If {@linkcode Directive.name} is a `RegExp`, this property shoud be specified.
*
* @example
* ```ts
* const foo = {
* name: /^~(?<bar>)/,
* prefix: "~",
* phase: Phase.UNKNOWN,
* } as Directive & { name: RegExp, prefix: string }
* ```
*/
readonly prefix?: string
/**
* Directive import list.
*
* This list contains a record of all libraries that the directive may dynamically {@linkcode https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import | import()}.
*
* @example
* ```ts
* const foo = {
* name: "*foo",
* phase: Phase.UNKNOWN,
* import: {
* testing: import.meta.resolve("@libs/testing")
* }
* } as Directive & { name: string, import: Record<PropertyKey, string> }
* ```
*/
readonly import?: Record<PropertyKey, string>
/**
* Directive phase.
*
* Directives are executed in ascending order based on their {@linkcode Phase}, which serves as a priority level.
*
* > [!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-phase | mizu.sh documentation}.
*
* @example
* ```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.
*
* @example
* ```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()}.
*
* @example
* ```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.
*
* @example
* ```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()}.
*
* @example
* ```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.
*
* If `false` is returned, the entire rendering process for this node is halted.
*
* A partial object can be returned to update the rendering {@linkcode State}.
*
* > [!IMPORTANT]
* > This method is executed regardless of the directive's presence on the node.
*
* @example
* ```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 | false | Partial<{ state: State }>>
/**
* Directive execution callback.
*
* This callback is executed during {@linkcode Renderer.render()} if the rendered node has been marked as eligible.
*
* If `final: true` is returned, the rendering process for this node is stopped (all {@linkcode Directive.cleanup()} will still be called).
*
* 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}).
*
* @example
* ```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`.
*
* @example
* ```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
1 change: 1 addition & 0 deletions @mizu/mizu/core/engine/directive_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./directive.ts"
7 changes: 7 additions & 0 deletions @mizu/mizu/core/engine/mod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* Mizu engine.
* @module
*/
export * from "./phase.ts"
export * from "./directive.ts"
export * from "./renderer.ts"
1 change: 1 addition & 0 deletions @mizu/mizu/core/engine/mod_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./mod_test.ts"
98 changes: 98 additions & 0 deletions @mizu/mizu/core/engine/phase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Imports
import type { Directive as _Directive } from "./directive.ts"

/**
* Enum representing all possible value that a {@linkcode _Directive | Directive.phase} can have.
*
* For more information, see the {@link https://mizu.sh/#concept-phase | mizu.sh documentation}.
*/
export enum Phase {
/** Placeholder value (intended for internal use only). @internal */
UNKNOWN = NaN,

// 0X - Preprocessing ————————————————————————————————————————————————————————————————————————————————

/** Directives that contain only metadata. */
META = 0,

/** Directives that determine the rendering eligibility of an element. */
ELIGIBILITY = 1,

/** Directives that must be executed first as they influence the rendering process. */
PREPROCESSING = 2,

// 10 - Testing ——————————————————————————————————————————————————————————————————————————————————————

/** Placeholder value (intended for testing use only). @internal */
TESTING = 10,

// 1X - Context ——————————————————————————————————————————————————————————————————————————————————————

/** Directives that alter the rendering context. */
CONTEXT = 11,

// 2X - Transforms ———————————————————————————————————————————————————————————————————————————————————

/** Directives that expand elements. */
EXPAND = 21,
/** Directives that morph elements. */
MORPHING = 22,
/** Directives that toggle elements. */
TOGGLE = 23,

// 3X - HTTP —————————————————————————————————————————————————————————————————————————————————————————

/** HTTP directives that set headers. */
HTTP_HEADER = 31,
/** HTTP directives that set the body. */
HTTP_BODY = 32,
/** HTTP directives that perform requests. */
HTTP_REQUEST = 33,
/** HTTP directives that set content. */
HTTP_CONTENT = 34,
/** HTTP directives that add interactivity. */
HTTP_INTERACTIVITY = 35,

// 4X - Content ——————————————————————————————————————————————————————————————————————————————————————

/** Directives that set content. */
CONTENT = 41,

/** Directives that clean content. */
CONTENT_CLEANING = 49,

// 5X - Attributes ———————————————————————————————————————————————————————————————————————————————————

/** Directives that set attributes. */
ATTRIBUTE = 51,
/** Directives that model value attributes. */
ATTRIBUTE_MODEL_VALUE = 52,

/** Directives that clean attributes. */
ATTRIBUTE_CLEANING = 59,

// 6X - Interactivity ————————————————————————————————————————————————————————————————————————————————

/** Directives that enhance element interactivity. */
INTERACTIVITY = 61,

// 7X - Styling ——————————————————————————————————————————————————————————————————————————————————————

/** Directives that affect display. */
DISPLAY = 71,

// 8X - Others ———————————————————————————————————————————————————————————————————————————————————————

/** Directives that register custom elements. */
CUSTOM_ELEMENT = 81,
/** Directives that register references. */
REFERENCE = 82,

/** Directives that apply custom processing logic. */
CUSTOM_PROCESSING = 89,

// 9X - Postprocessing ———————————————————————————————————————————————————————————————————————————————

/** Directives that must be executed last as they influence the rendering process. */
POSTPROCESSING = 99,
}
1 change: 1 addition & 0 deletions @mizu/mizu/core/engine/phase_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import "./phase.ts"
Loading
Loading