mancha
Version:
Javscript HTML rendering engine
214 lines • 10.4 kB
JavaScript
import { dirname, nodeToString, setProperty, traverse } from "./dome.js";
import { Iterator } from "./iterator.js";
import { RendererPlugins } from "./plugins.js";
import { setupQueryParamBindings } from "./query.js";
import { SignalStore } from "./store.js";
/**
* Represents an abstract class for rendering and manipulating HTML content.
* Extends the `ReactiveProxyStore` class.
*
* @template T - The type of the store state. Defaults to `StoreState`.
*/
export class IRenderer extends SignalStore {
debugging = false;
dirpath = "";
_skipNodes = new Set();
_customElements = new Map();
/**
* Sets the debugging flag for the current instance.
*
* @param flag - The flag indicating whether debugging is enabled or disabled.
* @returns The current instance of the class.
*/
debug(flag) {
this.debugging = flag;
return this;
}
/**
* Fetches the remote file at the specified path and returns its content as a string.
* @param fpath - The path of the remote file to fetch.
* @param params - Optional parameters for the fetch operation.
* @returns A promise that resolves to the content of the remote file as a string.
*/
async fetchRemote(fpath, params) {
return fetch(fpath, { cache: params?.cache ?? "default" }).then((res) => res.text());
}
/**
* Fetches a local path and returns its content as a string.
*
* @param fpath - The file path of the resource.
* @param params - Optional render parameters.
* @returns A promise that resolves to the fetched resource as a string.
*/
async fetchLocal(fpath, params) {
return this.fetchRemote(fpath, params);
}
/**
* Preprocesses a string content with optional rendering and parsing parameters.
*
* @param content - The string content to preprocess.
* @param params - Optional rendering and parsing parameters.
* @returns A promise that resolves to a DocumentFragment representing the preprocessed content.
*/
async preprocessString(content, params) {
this.log("Preprocessing string content with params:\n", params);
const fragment = this.parseHTML(content, params);
await this.preprocessNode(fragment, params);
return fragment;
}
/**
* Preprocesses a remote file by fetching its content and applying preprocessing steps.
* @param fpath - The path to the remote file.
* @param params - Optional parameters for rendering and parsing.
* @returns A Promise that resolves to a DocumentFragment representing the preprocessed content.
*/
async preprocessRemote(fpath, params) {
const fetchOptions = {};
if (params?.cache)
fetchOptions.cache = params.cache;
const content = await fetch(fpath, fetchOptions).then((res) => res.text());
return this.preprocessString(content, {
...params,
dirpath: dirname(fpath),
rootDocument: params?.rootDocument ?? !fpath.endsWith(".tpl.html"),
});
}
/**
* Preprocesses a local file by fetching its content and applying preprocessing steps.
* @param fpath - The path to the local file.
* @param params - Optional parameters for rendering and parsing.
* @returns A promise that resolves to the preprocessed document fragment.
*/
async preprocessLocal(fpath, params) {
const content = await this.fetchLocal(fpath, params);
return this.preprocessString(content, {
...params,
dirpath: dirname(fpath),
rootDocument: params?.rootDocument ?? !fpath.endsWith(".tpl.html"),
});
}
/**
* Creates a subrenderer from the current renderer instance.
* @returns A new instance of the renderer with the same state as the original.
*/
subrenderer() {
const instance = new this.constructor().debug(this.debugging);
// NOTE: Using the store object directly to avoid modifying ancestor values.
// Attach ourselves as the parent of the new instance.
instance._store.set("$parent", this);
// Add a reference to the root renderer, or assume that we are the root renderer.
instance._store.set("$rootRenderer", this.get("$rootRenderer") ?? this);
// Custom elements are shared across all instances.
instance._customElements =
this._customElements;
return instance;
}
/**
* Logs the provided arguments if debugging is enabled.
* @param args - The arguments to be logged.
*/
log(...args) {
if (this.debugging)
console.debug(...args);
}
/**
* Preprocesses a node by applying all the registered preprocessing plugins.
*
* @template T - The type of the input node.
* @param {T} root - The root node to preprocess.
* @param {RenderParams} [params] - Optional parameters for preprocessing.
* @returns {Promise<T>} - A promise that resolves to the preprocessed node.
*/
async preprocessNode(root, params) {
params = { dirpath: this.dirpath, maxdepth: 10, ...params };
const promises = new Iterator(traverse(root, this._skipNodes)).map(async (node) => {
this.log("Preprocessing node:\n", nodeToString(node, 128));
// Resolve all the includes in the node.
await RendererPlugins.resolveIncludes.call(this, node, params);
// Resolve all the relative paths in the node (including :render).
await RendererPlugins.rebaseRelativePaths.call(this, node, params);
// Register all the custom elements in the node.
await RendererPlugins.registerCustomElements.call(this, node, params);
// Resolve all the custom elements in the node.
await RendererPlugins.resolveCustomElements.call(this, node, params);
});
// Wait for all the rendering operations to complete.
await Promise.all(promises.generator());
// Return the input node, which should now be fully preprocessed.
return root;
}
/**
* Renders the node and applies all the registered rendering plugins.
*
* @template T - The type of the root node (Document, DocumentFragment, or Node).
* @param {T} root - The root node to render.
* @param {RenderParams} [params] - Optional parameters for rendering.
* @returns {Promise<T>} - A promise that resolves to the fully rendered root node.
*/
async renderNode(root, params) {
// Iterate over all the nodes and apply appropriate handlers.
// Do these steps one at a time to avoid any potential race conditions.
for (const node of traverse(root, this._skipNodes)) {
this.log("Rendering node:\n", nodeToString(node, 128));
// Resolve :for first - creates copies before other plugins modify the template.
await RendererPlugins.resolveForAttribute.call(this, node, params);
// Resolve :render - creates subrenderer, mounts, then runs init after descendants.
await RendererPlugins.resolveRenderAttribute.call(this, node, params);
// Resolve the :data attribute in the node.
await RendererPlugins.resolveDataAttribute.call(this, node, params);
// Resolve the :text attribute in the node.
await RendererPlugins.resolveTextAttributes.call(this, node, params);
// Resolve the :html attribute in the node.
await RendererPlugins.resolveHtmlAttribute.call(this, node, params);
// Resolve the :if attribute in the node.
await RendererPlugins.resolveIfAttribute.call(this, node, params);
// Resolve the :show attribute in the node.
await RendererPlugins.resolveShowAttribute.call(this, node, params);
// Resolve the :class attribute in the node.
await RendererPlugins.resolveClassAttribute.call(this, node, params);
// Resolve the :bind attribute in the node.
await RendererPlugins.resolveBindAttribute.call(this, node, params);
// Resolve all :on:event attributes in the node.
await RendererPlugins.resolveEventAttributes.call(this, node, params);
// Replace all the {{ variables }} in the text.
await RendererPlugins.resolveTextNodeExpressions.call(this, node, params);
// Resolve the :attr:{name} attribute in the node.
await RendererPlugins.resolveCustomAttribute.call(this, node, params);
// Resolve the :prop:{name} attribute in the node.
await RendererPlugins.resolveCustomProperty.call(this, node, params);
// Strip :types and data-types attributes from rendered output.
await RendererPlugins.stripTypes.call(this, node, params);
}
// Return the input node, which should now be fully rendered.
return root;
}
/**
* Mounts the Mancha application to a root element in the DOM.
*
* @param root - The root element to mount the application to.
* @param params - Optional parameters for rendering the application.
* @returns A promise that resolves when the mounting process is complete.
*/
async mount(root, params) {
params = { ...params, rootNode: root };
// Attach ourselves to the HTML node.
setProperty(root, "renderer", this);
// Attach the HTML node to the renderer instance.
// NOTE: Using the store object directly to avoid modifying ancestor values.
this._store.set("$rootNode", root);
// Set ourselves as the root renderer if not already set.
if (!this.has("$rootRenderer")) {
// NOTE: Using the store object directly to avoid modifying ancestor values.
this._store.set("$rootRenderer", this);
}
// Setup query parameter bindings if we are the root renderer.
if (this.get("$rootRenderer") === this) {
await setupQueryParamBindings(this);
}
// Preprocess all the elements recursively first.
await this.preprocessNode(root, params);
// Now that the DOM is complete, render all the nodes.
await this.renderNode(root, params);
}
}
//# sourceMappingURL=renderer.js.map