UNPKG

@webqit/oohtml

Version:

A suite of new DOM features that brings language support for modern UI development paradigms: a component-based architecture, data binding, and reactivity.

144 lines (137 loc) 6.94 kB
/** * @imports */ import { resolveParams } from '@webqit/quantum-js/params'; import { _, _init, _toHash, _fromHash } from '../util.js'; /** * @init * * @param Object $config */ export default function init({ advanced = {}, ...$config }) { const { config, window } = _init.call( this, 'scoped-js', $config, { script: { retention: 'retain', mimeTypes: 'module|text/javascript|application/javascript', timing: 'auto' }, api: { scripts: 'scripts' }, advanced: resolveParams(advanced), } ); const customTypes = Array.isArray( config.script.mimeTypes ) ? config.script.mimeTypes : config.script.mimeTypes.split( '|' ).filter( t => t ); config.scriptSelector = customTypes.map( t => `script[type="${ window.CSS.escape( t ) }"]:not([oohtmlignore])` ).concat(`script:not([type])`).join( ',' ); window.webqit.oohtml.Script = { compileCache: [ new Map, new Map, ], execute: execute.bind( window, config ), }; exposeAPIs.call( window, config ); realtime.call( window, config ); } /** * Exposes Bindings with native APIs. * * @param Object config * * @return Void */ function exposeAPIs( config ) { const window = this, scriptsMap = new Map; if ( config.api.scripts in window.Element.prototype ) { throw new Error( `The "Element" class already has a "${ config.api.scripts }" property!` ); } [ window.ShadowRoot.prototype, window.Element.prototype ].forEach( proto => { Object.defineProperty( proto, config.api.scripts, { get: function() { if ( !scriptsMap.has( this ) ) { scriptsMap.set( this, [] ); } return scriptsMap.get( this ); }, } ); } ); Object.defineProperties( window.HTMLScriptElement.prototype, { scoped: { configurable: true, get() { return this.hasAttribute( 'scoped' ); }, set( value ) { this.toggleAttribute( 'scoped', value ); }, }, quantum: { configurable: true, get() { return this.hasAttribute( 'quantum' ); }, set( value ) { this.toggleAttribute( 'quantum', value ); }, }, } ); } // Script runner async function execute( config, execHash ) { const window = this, { realdom } = window.webqit; const exec = _fromHash( execHash ); if ( !exec ) throw new Error( `Argument must be a valid exec hash.` ); const { script, compiledScript, thisContext } = exec; // Honour retention flag if ( config.script.retention === 'dispose' ) { script.remove(); } else if ( config.script.retention === 'hidden' ) { script.textContent = `"source hidden"`; } else { script.textContent = await compiledScript.toString(); } // Execute and save state const varScope = script.scoped ? thisContext : script.getRootNode(); if ( !_( varScope ).has( 'scriptEnv' ) ) { _( varScope ).set( 'scriptEnv', Object.create( null ) ); } const state = await ( await compiledScript.bind( thisContext, _( varScope ).get( 'scriptEnv' ) ) ).execute(); if ( script.quantum ) { Object.defineProperty( script, 'state', { value: state } ); } realdom.realtime( window.document ).observe( script, () => { if ( script.quantum ) { state.dispose(); } if ( thisContext instanceof window.Element ) { thisContext[ config.api.scripts ]?.splice( thisContext[ config.api.scripts ].indexOf( script, 1 ) ); } }, { id: 'scoped-js:script-exits', subtree: 'cross-roots', timing: 'sync', generation: 'exits' } ); } /** * Performs realtime capture of elements and builds their relationships. * * @param Object config * * @return Void */ function realtime( config ) { const inBrowser = Object.getOwnPropertyDescriptor( globalThis, 'window' )?.get?.toString().includes( '[native code]' ) ?? false; const window = this, { webqit: { oohtml, realdom } } = window; if ( !window.HTMLScriptElement.supports ) { window.HTMLScriptElement.supports = type => [ 'text/javascript', 'application/javascript' ].includes( type ); } const handled = new WeakSet; realdom.realtime( window.document ).query( config.scriptSelector, record => { record.entrants.forEach( script => { if ( handled.has( script ) || (!inBrowser && !script.hasAttribute('ssr')) ) return; // Do compilation const compiledScript = compileScript.call( window, config, script ); if ( !compiledScript ) return; handled.add( script ); // Run now!!! const thisContext = script.scoped ? script.parentNode || record.target : ( script.type === 'module' ? undefined : window ); if ( script.scoped ) { thisContext[ config.api.scripts ].push( script ); } const execHash = _toHash( { script, compiledScript, thisContext } ); const manualHandling = record.type === 'query' || ( script.type && !window.HTMLScriptElement.supports( script.type ) ) || script.getAttribute('data-handling') === 'manual'; if ( manualHandling || config.script.timing === 'manual' ) { oohtml.Script.execute( execHash ); } else { script.textContent = `webqit.oohtml.Script.execute( '${ execHash }' );`; } } ); }, { id: 'scoped-js:script-entries', live: true, subtree: 'cross-roots', timing: 'intercept', generation: 'entrants', eventDetails: true } ); // --- } function compileScript( config, script ) { const window = this, { webqit: { oohtml, QuantumScript, AsyncQuantumScript, QuantumModule } } = window; const textContent = ( script._ = script.textContent.trim() ) && script._.startsWith( '/*@oohtml*/if(false){' ) && script._.endsWith( '}/*@oohtml*/' ) ? script._.slice( 21, -12 ) : script.textContent; if ( !textContent.trim().length ) return; const sourceHash = _toHash( textContent ); const compileCache = oohtml.Script.compileCache[ script.quantum ? 0 : 1 ]; let compiledScript; if ( !( compiledScript = compileCache.get( sourceHash ) ) ) { const { parserParams, compilerParams, runtimeParams } = config.advanced; compiledScript = new ( script.type === 'module' ? QuantumModule : ( QuantumScript || AsyncQuantumScript ) )( textContent, { exportNamespace: `#${ script.id }`, fileName: `${ window.document.url?.split( '#' )?.[ 0 ] || '' }#${ script.id }`, parserParams: { ...parserParams, executionMode: script.quantum && 'QuantumProgram' || 'RegularProgram' }, compilerParams, runtimeParams, } ); compileCache.set( sourceHash, compiledScript ); } return compiledScript; } export function idleCompiler( node ) { const window = this, { webqit: { oohtml: { configs: { SCOPED_JS: config } } } } = window; [ ...( node?.querySelectorAll( config.scriptSelector ) || [] ) ].forEach( script => { compileScript.call( window, config, script ); } ); }