import { A_Feature, A_Inject, A_Scope } from "@adaas/a-concept"; import { A_Frame } from "@adaas/a-frame/core" import { A_ServiceFeatures } from "@adaas/a-utils/a-service"; import { AreEngine, AreSyntaxTokenMatch, AreSyntax, AreSignalsContext } from "@adaas/are"; import { AreHTMLInterpreter } from "@adaas/are-html/interpreter"; import { AreHTMLEngineContext } from "./AreHTML.context"; import { AreInterpolation } from "@adaas/are-html/nodes/AreInterpolation"; import { AreText } from "@adaas/are-html/nodes/AreText"; import { AreComment } from "@adaas/are-html/nodes/AreComment"; import { AreComponentNode } from "@adaas/are-html/nodes/AreComponent"; import { AreHTMLTokenizer } from "@adaas/are-html/tokenizer"; import { AreRootNode } from "@adaas/are-html/nodes/AreRoot"; import { AreHTMLLifecycle } from "@adaas/are-html/lifecycle"; import { AreHTMLTransformer } from "@adaas/are-html/transformer"; import { AreHTMLCompiler } from "./AreHTML.compiler"; import { isVoidElement } from "./AreHTML.constants"; import { AreRootCache } from "../lib/AreRoot/AreRootCache.context"; @A_Frame.Define({ namespace: 'a-are-html', description: 'Concrete HTML rendering engine that assembles the full ARE pipeline for web environments. Bootstraps and wires AreHTMLTokenizer, AreHTMLTransformer, AreHTMLCompiler, AreHTMLInterpreter, and AreHTMLLifecycle; mounts root nodes from inline or fetched templates; and drives reactive re-renders via the AreSignals bus.' }) export class AreHTMLEngine extends AreEngine { get DefaultSyntax() { return new AreSyntax({ trimWhitespace: true, strictMode: true, rules: [ // HTML comments { opening: '', component: AreComment, priority: 10, nested: false, extract: (raw) => ({ content: raw.slice(4, -3).trim() }), }, // interpolations { opening: '{{', closing: '}}', component: AreInterpolation, priority: 9, nested: false, extract: (_, match) => ({ key: match.content }), }, // are-root — matched before generic elements, produces AreRootNode { matcher: this.rootElementMatcher.bind(this), component: AreRootNode, priority: 5, }, // generic HTML elements { matcher: this.htmlElementMatcher.bind(this), component: AreComponentNode, priority: 4, }, // plain text fallback { component: AreText, priority: 0, }, ], }) } /** * Inject AreHTMLSyntax into the container scope before loading * * @param container */ @A_Feature.Extend({ name: A_ServiceFeatures.onBeforeLoad, before: /.*/ }) async init( @A_Inject(A_Scope) scope: A_Scope, @A_Inject(AreSignalsContext) signalContext?: AreSignalsContext, @A_Inject(AreRootCache) rootCache?: AreRootCache ) { this.package(scope, { context: new AreHTMLEngineContext({}), syntax: this.DefaultSyntax, compiler: AreHTMLCompiler, interpreter: AreHTMLInterpreter, tokenizer: AreHTMLTokenizer, lifecycle: AreHTMLLifecycle, transformer: AreHTMLTransformer, }); if(!signalContext) { signalContext = new AreSignalsContext(); scope.register(signalContext); } // Default per-root subtree cache used by AreRoot for fast route-back // re-injection. Apps may register their own (e.g. with a custom limit) // before load to override this. if (!rootCache) { rootCache = new AreRootCache(); scope.register(rootCache); } } protected rootElementMatcher( source: string, from: number, to: number, build: (raw: string, content: string, position: number, closing: string) => AreSyntaxTokenMatch ): AreSyntaxTokenMatch | null { // const rootTag = this.config.rootTag const rootTag = 'are-root'; const tagStart = source.indexOf('<', from) if (tagStart === -1 || tagStart >= to) return null // only match if the tag name is exactly the configured root tag const tagNameMatch = source.slice(tagStart).match(/^<([a-zA-Z][a-zA-Z0-9-]*)/) if (!tagNameMatch || tagNameMatch[1].toLowerCase() !== rootTag) return null // delegate the actual matching to the shared HTML element logic return this.htmlElementMatcher(source, from, to, build) } protected htmlElementMatcher( source: string, from: number, to: number, build: (raw: string, content: string, position: number, closing: string) => AreSyntaxTokenMatch ): AreSyntaxTokenMatch | null { let index = from while (index < to) { const tagStart = source.indexOf('<', index) if (tagStart === -1 || tagStart >= to) return null // skip comments, closing tags, doctype, processing instructions if (source.startsWith('