UNPKG

@jinntec/fore

Version:

Fore - declarative user interfaces in plain HTML

182 lines (144 loc) 5.16 kB
import ForeElementMixin from '../ForeElementMixin.js'; import registerFunction from './registerFunction.js'; const LOCAL_FUNCTIONS_NS = 'http://www.w3.org/2005/xquery-local-functions'; // Global per-page cache to prevent registering the same library multiple times. // Keyed by resolved URL + prefix + mode. const _functionLibLoadCache = new Map(); function looksLikeModuleSrc(src) { return /\.m?js($|\?)/i.test(src); } function applyPrefixToSignature(signature, prefix) { if (!signature || !prefix) return signature; const s = signature.trim(); const paren = s.indexOf('('); if (paren < 0) return s; const namePart = s.slice(0, paren).trim(); const rest = s.slice(paren); const localName = namePart.includes(':') ? namePart.split(':').pop().trim() : namePart; if (!localName) return s; return `${prefix}:${localName}${rest}`; } function normalizeModuleExportToList(mod, src) { const lib = mod.functions ?? mod.fxFunctions; if (!lib) { console.error( `fx-functionlib: Module ${src} must export a named \`functions\` (or \`fxFunctions\`).`, ); return []; } if (Array.isArray(lib)) return lib; if (typeof lib === 'object') return Object.values(lib); return []; } export class FxFunctionlib extends ForeElementMixin { constructor() { super(); /** * @type {Function} */ this._resolve = null; /** * @type {Promise<undefined>} */ this.readyPromise = new Promise(resolve => (this._resolveLoading = resolve)); } async connectedCallback() { this.style.display = 'none'; const src = this.getAttribute('src'); if (!src) { console.error('fx-functionlib: Missing required @src.'); this._resolveLoading(undefined); return; } const prefix = (this.getAttribute('prefix') || '').trim(); const typeAttr = (this.getAttribute('type') || '').trim().toLowerCase(); const isModule = typeAttr === 'module' || (!typeAttr && looksLikeModuleSrc(src)); const resolvedUrl = new URL(src, this.baseURI).href; if (prefix) this._ensurePrefixDeclared(prefix); const mode = isModule ? 'module' : 'html'; const cacheKey = `${mode}|${resolvedUrl}|${prefix}`; const existing = _functionLibLoadCache.get(cacheKey); if (existing) { try { await existing; } finally { this._resolveLoading(undefined); } return; } const loadPromise = (async () => { if (isModule) { await this._loadModuleLibrary(resolvedUrl, src, prefix); } else { await this._loadHtmlLibrary(resolvedUrl, src, prefix); } })(); _functionLibLoadCache.set(cacheKey, loadPromise); try { await loadPromise; } catch (e) { _functionLibLoadCache.delete(cacheKey); console.error(`fx-functionlib: Loading function library at ${src} failed.`, e); } finally { this._resolveLoading(undefined); } } _ensurePrefixDeclared(prefix) { const ownerForm = (typeof this.getOwnerForm === 'function' && this.getOwnerForm()) || this.closest('fx-fore'); if (!ownerForm) return; const attrName = `xmlns:${prefix}`; if (!ownerForm.getAttribute(attrName)) { ownerForm.setAttribute(attrName, LOCAL_FUNCTIONS_NS); } } _register(functionObject, prefix) { if (!functionObject || typeof functionObject.signature !== 'string') return; // If prefix is given: register ONLY the prefixed signature (no unprefixed alias). const sig = prefix ? applyPrefixToSignature(functionObject.signature, prefix) : functionObject.signature; registerFunction({ ...functionObject, signature: sig }, this); } async _loadModuleLibrary(resolvedUrl, src, prefix) { const mod = await import(/* @vite-ignore */ resolvedUrl); const items = normalizeModuleExportToList(mod, src); for (const item of items) { if (typeof item === 'function') { const { signature } = item; if (typeof signature !== 'string' || !signature.trim()) continue; this._register( { type: 'text/javascript', signature: signature.trim(), implementation: item, }, prefix, ); } else if (item && typeof item === 'object' && typeof item.signature === 'string') { this._register(item, prefix); } } } async _loadHtmlLibrary(resolvedUrl, src, prefix) { const result = await fetch(resolvedUrl); if (!result.ok) { console.error(`Loading function library at ${src} failed.`); return; } const body = await result.text(); const document = new DOMParser().parseFromString(body, 'text/html'); const functions = Array.from(document.querySelectorAll('fx-function')); for (const func of functions) { const functionObject = { type: func.getAttribute('type'), signature: func.getAttribute('signature'), functionBody: func.innerText, }; this._register(functionObject, prefix); } } } if (!customElements.get('fx-functionlib')) { customElements.define('fx-functionlib', FxFunctionlib); }