@webqit/oohtml 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -19,7 +19,6 @@ export default function init( $config = {} ) {
19
19
  } );
20
20
  config.lidSelector = `[${ window.CSS.escape( config.attr.lid ) }]`;
21
21
  config.namespaceSelector = `[${ window.CSS.escape( config.attr.namespace ) }]`;
22
- config.documentNamespaceUUID = getNamespaceUUID( getOwnNamespaceObject.call( window, window.document ) );
23
22
  window.webqit.DOMNamingContext = DOMNamingContext;
24
23
  exposeAPIs.call( window, config );
25
24
  realtime.call( window, config );
@@ -40,7 +39,7 @@ function lidUtil( config ) {
40
39
  lidrefSeparator( escapeMode = 0 ) { return escapeMode ? this.escape( lidrefSeparator, escapeMode ) : lidrefSeparator; },
41
40
  isUuid( str, escapeMode = 0 ) { return str.startsWith( this.lidrefPrefix( escapeMode ) ) && str.includes( this.lidrefSeparator( escapeMode ) ); },
42
41
  isLidref( str, escapeMode = 0 ) { return str.startsWith( this.lidrefPrefix( escapeMode ) ) && !str.includes( this.lidrefSeparator( escapeMode ) ); },
43
- toUuid( hash, lid, escapeMode = 0 ) { return hash === config.documentNamespaceUUID ? lid : `${ this.lidrefPrefix( escapeMode ) }${ hash }${ this.lidrefSeparator( escapeMode ) }${ lid }`; },
42
+ toUuid( hash, lid, escapeMode = 0 ) { return hash.endsWith( '-root' ) ? lid : `${ this.lidrefPrefix( escapeMode ) }${ hash }${ this.lidrefSeparator( escapeMode ) }${ lid }`; },
44
43
  uuidToId( str, escapeMode = 0 ) { return this.isUuid( str ) ? str.split( this.lidrefSeparator( escapeMode ) )[ 1 ] : str; },
45
44
  uuidToLidref( str, escapeMode = 0 ) { return this.isUuid( str ) ? `${ this.lidrefPrefix( escapeMode ) }${ str.split( this.lidrefSeparator( escapeMode ) )[ 1 ] }` : str; },
46
45
  }
@@ -72,7 +71,6 @@ export function rewriteSelector( selectorText, namespaceUUID, scopeSelector = nu
72
71
  // Should yield: `"Hello, John. I\\"m your friend."`, `"you're he're"`, `'f\\'"j\\'"f'`
73
72
  quotesMatch = [ ...selector.matchAll( /(["'])(?:(?=(\\?))\2.)*?\1/g ) ];
74
73
  }
75
- if ( quotesMatch[ 0 ] )
76
74
  // Qualify match
77
75
  if ( quotesMatch.some( q => index > q.index && index + match.length < q.index + q[ 0 ].length ) ) return match;
78
76
  // Replace :scope
@@ -87,7 +85,7 @@ export function rewriteSelector( selectorText, namespaceUUID, scopeSelector = nu
87
85
  }
88
86
  // Rewrite relative ID selector
89
87
  if ( isLidref ) {
90
- if ( config.attr.lid === 'id' && namespaceUUID && namespaceUUID !== config.documentNamespaceUUID ) {
88
+ if ( config.attr.lid === 'id' && namespaceUUID && !namespaceUUID.endsWith( '-root' ) ) {
91
89
  return `#${ $lidUtil.toUuid( namespaceUUID, id, 1 ) }`;
92
90
  }
93
91
  // Fallback to attr-based
@@ -99,7 +97,7 @@ export function rewriteSelector( selectorText, namespaceUUID, scopeSelector = nu
99
97
  } else {
100
98
  rewrite = `:is(#${ id },[${ window.CSS.escape( config.attr.lid ) }="${ id }"])`;
101
99
  }
102
- return isLidref ? `:is(${ rewrite }):not(${ scopeSelector ? scopeSelector + ' ' : '' }[${ config.attr.namespace }] *)` : rewrite;
100
+ return isLidref ? `:is(${ rewrite }):not(${ scopeSelector ? scopeSelector + ' ' : '' }${ config.namespaceSelector } *)` : rewrite;
103
101
  } );
104
102
  // Category 2 has :scope and category 1 does not
105
103
  return hadScopeSelector ? [ cat1, cat2.concat( selector ) ] : [ cat1.concat( selector ), cat2 ];
@@ -120,9 +118,14 @@ export function rewriteSelector( selectorText, namespaceUUID, scopeSelector = nu
120
118
  * @return Object
121
119
  */
122
120
  export function getOwnNamespaceObject( node ) {
121
+ const window = this;
123
122
  if ( !_( node ).has( 'namespace' ) ) {
124
123
  const namespaceObj = Object.create( null );
125
124
  _( node ).set( 'namespace', namespaceObj );
125
+ const isDocumentRoot = [ window.Document, window.ShadowRoot ].some( x => node instanceof x );
126
+ Object.defineProperty( namespaceObj, Symbol.toStringTag, { get() {
127
+ return isDocumentRoot ? 'RootNamespaceRegistry' : 'NamespaceRegistry';
128
+ } } );
126
129
  }
127
130
  return _( node ).get( 'namespace' );
128
131
  }
@@ -135,7 +138,8 @@ export function getOwnNamespaceObject( node ) {
135
138
  */
136
139
  export function getOwnerNamespaceObject( node, forID = false ) {
137
140
  const window = this, { webqit: { oohtml: { configs: { NAMESPACED_HTML: config } } } } = window;
138
- return getOwnNamespaceObject( node instanceof window.Document ? node : ( ( forID ? node.parentNode : node )?.closest( `[${ config.attr.namespace }]` ) || node.ownerDocument ) );
141
+ const isDocumentRoot = [ window.Document, window.ShadowRoot ].some( x => node instanceof x );
142
+ return getOwnNamespaceObject.call( window, isDocumentRoot ? node : ( ( forID ? node.parentNode : node )?.closest/*can be documentFragment when Shadow DOM*/?.( config.namespaceSelector ) || node.getRootNode() ) );
139
143
  }
140
144
 
141
145
  /**
@@ -144,7 +148,8 @@ export function getOwnerNamespaceObject( node, forID = false ) {
144
148
  * @return String
145
149
  */
146
150
  export function getNamespaceUUID( namespaceObj ) {
147
- return _fromHash( namespaceObj ) || _toHash( namespaceObj );
151
+ const isDocumentRoot = Object.prototype.toString.call( namespaceObj ) === '[object RootNamespaceRegistry]';
152
+ return ( _fromHash( namespaceObj ) || _toHash( namespaceObj ) ) + ( isDocumentRoot ? '-root' : '' );
148
153
  }
149
154
 
150
155
  /**
@@ -156,16 +161,16 @@ export function getNamespaceUUID( namespaceObj ) {
156
161
  */
157
162
  function exposeAPIs( config ) {
158
163
  const window = this, { webqit: { Observer } } = window;
159
- // Assertions
160
- if ( config.api.namespace in window.document ) { throw new Error( `document already has a "${ config.api.namespace }" property!` ); }
161
- if ( config.api.namespace in window.Element.prototype ) { throw new Error( `The "Element" class already has a "${ config.api.namespace }" property!` ); }
162
- // Definitions
163
- Object.defineProperty( window.document, config.api.namespace, { get: function() {
164
- return Observer.proxy( getOwnNamespaceObject.call( window, window.document ) );
165
- } });
166
- Object.defineProperty( window.Element.prototype, config.api.namespace, { get: function() {
167
- return Observer.proxy( getOwnNamespaceObject.call( window, this ) );
168
- } } );
164
+ // The Namespace API
165
+ [ window.Document.prototype, window.Element.prototype, window.ShadowRoot.prototype ].forEach( prototype => {
166
+ // No-conflict assertions
167
+ const type = prototype === window.Document.prototype ? 'Document' : ( prototype === window.ShadowRoot.prototype ? 'ShadowRoot' : 'Element' );
168
+ if ( config.api.namespace in prototype ) { throw new Error( `The ${ type } prototype already has a "${ config.api.namespace }" API!` ); }
169
+ // Definitions
170
+ Object.defineProperty( prototype, config.api.namespace, { get: function() {
171
+ return Observer.proxy( getOwnNamespaceObject.call( window, this ) );
172
+ } } );
173
+ } );
169
174
  }
170
175
 
171
176
  /**
@@ -262,7 +267,7 @@ function realtime( config ) {
262
267
  const setupBinding = ( entry, attrName, value, newNamespaceObj = null ) => {
263
268
  attrChange( entry, attrName, value, value => {
264
269
  const isLidAttr = attrName === config.attr.lid;
265
- const namespaceObj = newNamespaceObj || getOwnerNamespaceObject( entry, isLidAttr );
270
+ const namespaceObj = newNamespaceObj || getOwnerNamespaceObject.call( window, entry, isLidAttr );
266
271
  const namespaceUUID = getNamespaceUUID( namespaceObj );
267
272
  if ( isLidAttr ) {
268
273
  const id = $lidUtil.uuidToId( value );
@@ -283,7 +288,7 @@ function realtime( config ) {
283
288
  const cleanupBinding = ( entry, attrName, oldValue, prevNamespaceObj = null ) => {
284
289
  attrChange( entry, attrName, oldValue, oldValue => {
285
290
  const isLidAttr = attrName === config.attr.lid;
286
- const namespaceObj = prevNamespaceObj || getOwnerNamespaceObject( entry, isLidAttr );
291
+ const namespaceObj = prevNamespaceObj || getOwnerNamespaceObject.call( window, entry, isLidAttr );
287
292
  if ( isLidAttr ) {
288
293
  const id = $lidUtil.uuidToId( oldValue );
289
294
  if ( Observer.get( namespaceObj, id ) === entry ) {
@@ -300,8 +305,7 @@ function realtime( config ) {
300
305
  // ------------
301
306
  // NAMESPACE
302
307
  // ------------
303
- window.document[ configs.CONTEXT_API.api.contexts ].attach( new DOMNamingContext );
304
- realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( config.namespaceSelector, record => {
308
+ realdom.realtime( window.document ).query( config.namespaceSelector, record => {
305
309
  const reAssociate = ( entry, attrName, oldNamespaceObj, newNamespaceObj ) => {
306
310
  if ( !entry.hasAttribute( attrName ) ) return;
307
311
  const attrValue = () => entry.getAttribute( attrName );
@@ -309,9 +313,9 @@ function realtime( config ) {
309
313
  if ( entry.isConnected ) { setupBinding( entry, attrName, _( entry, 'attrOriginals' ).get( attrName )/* Saved original value */ || attrValue/* Lest it's ID */, newNamespaceObj ); }
310
314
  };
311
315
  record.exits.forEach( entry => {
312
- const namespaceObj = getOwnNamespaceObject( entry );
316
+ const namespaceObj = getOwnNamespaceObject.call( window, entry );
313
317
  // Detach ID and IDREF associations
314
- for ( const node of new Set( [ ...Object.values( namespaceObj ), ..._( namespaceObj ).get( 'idrefs' ) ] ) ) {
318
+ for ( const node of new Set( [ ...Object.values( namespaceObj ), ...( _( namespaceObj ).get( 'idrefs' ) || [] ) ] ) ) {
315
319
  for ( const attrName of attrList ) { reAssociate( node, attrName, namespaceObj ); }
316
320
  }
317
321
  // Detach ID associations
@@ -323,19 +327,19 @@ function realtime( config ) {
323
327
  record.entrants.forEach( entry => {
324
328
  // Claim ID and IDREF associations
325
329
  let newSuperNamespaceObj;
326
- const superNamespaceObj = getOwnerNamespaceObject( entry, true );
330
+ const superNamespaceObj = getOwnerNamespaceObject.call( window, entry, true );
327
331
  for ( const node of new Set( [ ...Object.values( superNamespaceObj ), ...( _( superNamespaceObj ).get( 'idrefs' ) || [] ) ] ) ) {
328
- if ( ( newSuperNamespaceObj = getOwnerNamespaceObject( node, true ) ) === superNamespaceObj ) continue;
332
+ if ( ( newSuperNamespaceObj = getOwnerNamespaceObject.call( window, node, true ) ) === superNamespaceObj ) continue;
329
333
  for ( const attrName of attrList ) { reAssociate( node, attrName, superNamespaceObj, newSuperNamespaceObj ); }
330
334
  }
331
335
  // Attach namespace instance
332
336
  const contextsApi = entry[ configs.CONTEXT_API.api.contexts ];
333
337
  if ( !contextsApi.find( DOMNamingContext.kind ) ) { contextsApi.attach( new DOMNamingContext ); }
334
338
  } );
335
- }, { live: true, timing: 'sync', staticSensitivity: true } );
339
+ }, { live: true, subtree: 'cross-roots', timing: 'sync', staticSensitivity: true, eventDetails: true } );
336
340
 
337
341
  // DOM realtime
338
- realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( `[${ attrList.map( attrName => window.CSS.escape( attrName ) ).join( '],[' ) }]`, record => {
342
+ realdom.realtime( window.document ).query( `[${ attrList.map( attrName => window.CSS.escape( attrName ) ).join( '],[' ) }]`, record => {
339
343
  for ( const attrName of attrList ) {
340
344
  record.exits.forEach( entry => {
341
345
  if ( !entry.hasAttribute( attrName ) ) return;
@@ -346,14 +350,14 @@ function realtime( config ) {
346
350
  setupBinding( entry, attrName, () => entry.getAttribute( attrName )/* Raw value (as-is) that will be saved as original */ );
347
351
  } );
348
352
  }
349
- }, { live: true, timing: 'sync' } );
353
+ }, { live: true, subtree: 'cross-roots', timing: 'sync' } );
350
354
  // Attr realtime
351
355
  realdom.realtime( window.document, 'attr' ).observe( attrList, records => {
352
356
  for ( const record of records ) {
353
357
  if ( record.oldValue ) { cleanupBinding( record.target, record.name, record.oldValue/* Current resolved value as-is */ ); }
354
358
  if ( record.value ) { setupBinding( record.target, record.name, record.value/* Raw value (as-is) that will be saved as original */ ); }
355
359
  }
356
- }, { subtree: true, timing: 'sync', newValue: true, oldValue: true } );
360
+ }, { subtree: 'cross-roots', timing: 'sync', newValue: true, oldValue: true } );
357
361
 
358
362
  // ------------
359
363
  // TARGETS
@@ -35,11 +35,18 @@ export default function init({ advanced = {}, ...$config }) {
35
35
  */
36
36
  function exposeAPIs( config ) {
37
37
  const window = this, styleSheetsMap = new Map;
38
- if ( config.api.styleSheets in window.Element.prototype ) { throw new Error( `The "Element" class already has a "${ config.api.styleSheets }" property!` ); }
39
- Object.defineProperty( window.HTMLElement.prototype, config.api.styleSheets, { get: function() {
40
- if ( !styleSheetsMap.has( this ) ) { styleSheetsMap.set( this, [] ); }
41
- return styleSheetsMap.get( this );
42
- }, } );
38
+ // The "styleSheets" API
39
+ [ window.Element.prototype ].forEach( prototype => {
40
+ // No-conflict assertions
41
+ const type = 'Element';
42
+ if ( config.api.styleSheets in prototype ) { throw new Error( `The ${ type } prototype already has a "${ config.api.styleSheets }" API!` ); }
43
+ // Definitions
44
+ Object.defineProperty( prototype, config.api.styleSheets, { get: function() {
45
+ if ( !styleSheetsMap.has( this ) ) { styleSheetsMap.set( this, [] ); }
46
+ return styleSheetsMap.get( this );
47
+ }, } );
48
+ } );
49
+ // The HTMLStyleElement "scoped" property
43
50
  Object.defineProperty( window.HTMLStyleElement.prototype, 'scoped', {
44
51
  configurable: true,
45
52
  get() { return this.hasAttribute( 'scoped' ); },
@@ -56,9 +63,10 @@ function exposeAPIs( config ) {
56
63
  */
57
64
  function realtime( config ) {
58
65
  const window = this, { webqit: { oohtml, realdom } } = window;
66
+ const inBrowser = Object.getOwnPropertyDescriptor( globalThis, 'window' )?.get?.toString().includes( '[native code]' ) ?? false;
59
67
  if ( !window.CSS.supports ) { window.CSS.supports = () => false; }
60
68
  const handled = new WeakSet;
61
- realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( config.styleSelector, record => {
69
+ realdom.realtime( window.document ).query( config.styleSelector, record => {
62
70
  record.entrants.forEach( style => {
63
71
  if ( handled.has( style ) ) return;
64
72
  handled.add( style );
@@ -67,10 +75,13 @@ function realtime( config ) {
67
75
  const supportsHAS = CSS.supports( 'selector(:has(a,b))' );
68
76
  const scopeSelector = style.scoped && ( supportsHAS ? `:has(> style[rand-${ sourceHash }])` : `[rand-${ sourceHash }]` );
69
77
  const supportsScope = style.scoped && window.CSSScopeRule && false/* Disabled for buggy behaviour: rewriting selectorText within an @scope block invalidates the scoping */;
70
- if ( style.scoped ) {
71
- style.parentNode[ config.api.styleSheets ].push( style );
72
- ( supportsHAS ? style : style.parentNode ).toggleAttribute( `rand-${ sourceHash }`, true );
78
+ const scopeRoot = style.scoped && style.parentNode || style.getRootNode();
79
+ if ( scopeRoot instanceof window.Element ) {
80
+ scopeRoot[ config.api.styleSheets ].push( style );
81
+ if ( !inBrowser ) return;
82
+ ( supportsHAS ? style : scopeRoot ).toggleAttribute( `rand-${ sourceHash }`, true );
73
83
  }
84
+ if ( !inBrowser ) return;
74
85
  if ( style.scoped && style.hasAttribute( 'shared' ) ) {
75
86
  let compiledSheet;
76
87
  if ( !( compiledSheet = oohtml.Style.compileCache.get( sourceHash ) ) ) {
@@ -82,14 +93,14 @@ function realtime( config ) {
82
93
  style.textContent = '\n/*[ Shared style sheet ]*/\n';
83
94
  } else {
84
95
  const transform = () => {
85
- const namespaceUUID = getNamespaceUUID( getOwnerNamespaceObject.call( window, style.scoped ? style : window.document ) );
96
+ const namespaceUUID = getNamespaceUUID( getOwnerNamespaceObject.call( window, scopeRoot ) );
86
97
  upgradeSheet.call( this, style.sheet, namespaceUUID, !supportsScope && scopeSelector );
87
98
  };
88
99
  if ( style.isConnected ) { transform(); }
89
100
  else { setTimeout( () => { transform(); }, 0 ); }
90
101
  }
91
102
  } );
92
- }, { live: true, timing: 'intercept', generation: 'entrants' } );
103
+ }, { live: true, subtree: 'cross-roots', timing: 'intercept', generation: 'entrants' } );
93
104
  // ---
94
105
  }
95
106
 
@@ -100,12 +111,14 @@ function createAdoptableStylesheet( style, namespaceUUID, supportsScope, scopeSe
100
111
  styleSheet = new window.CSSStyleSheet;
101
112
  styleSheet.replaceSync( cssText );
102
113
  upgradeSheet.call( this, styleSheet, namespaceUUID, !supportsScope && scopeSelector );
103
- document.adoptedStyleSheets.push( styleSheet );
114
+ const adopt = () => style.getRootNode().adoptedStyleSheets.push( styleSheet );
115
+ if ( style.isConnected ) { adopt(); }
116
+ else { setTimeout( () => { adopt(); }, 0 ); }
104
117
  } catch( e ) {
105
- const style = window.document.createElement( 'style' );
106
- window.document.body.appendChild( style );
107
- style.textContent = cssText;
108
- styleSheet = style.sheet;
118
+ const styleCopy = window.document.createElement( 'style' );
119
+ style.after( styleCopy );
120
+ styleCopy.textContent = cssText;
121
+ styleSheet = styleCopy.sheet;
109
122
  upgradeSheet.call( this, styleSheet, namespaceUUID, !supportsScope && scopeSelector );
110
123
  }
111
124
  return styleSheet;
@@ -38,7 +38,7 @@ export default function init({ advanced = {}, ...$config }) {
38
38
  function exposeAPIs( config ) {
39
39
  const window = this, scriptsMap = new Map;
40
40
  if ( config.api.scripts in window.Element.prototype ) { throw new Error( `The "Element" class already has a "${ config.api.scripts }" property!` ); }
41
- Object.defineProperty( window.HTMLElement.prototype, config.api.scripts, { get: function() {
41
+ Object.defineProperty( window.Element.prototype, config.api.scripts, { get: function() {
42
42
  if ( !scriptsMap.has( this ) ) { scriptsMap.set( this, [] ); }
43
43
  return scriptsMap.get( this );
44
44
  }, } );
@@ -77,10 +77,10 @@ async function execute( config, execHash ) {
77
77
  }
78
78
  const state = ( await compiledScript.bind( thisContext, _( documentRoot ).get( 'scriptEnv' ) ) ).execute();
79
79
  if ( script.quantum ) { Object.defineProperty( script, 'state', { value: state } ); }
80
- realdom.realtime( documentRoot ).observe( script, () => {
80
+ realdom.realtime( window.document ).observe( script, () => {
81
81
  if ( script.quantum ) { state.dispose(); }
82
- if ( script.scoped ) { thisContext[ config.api.scripts ].splice( thisContext[ config.api.scripts ].indexOf( script, 1 ) ); }
83
- }, { subtree: true, timing: 'sync', generation: 'exits' } );
82
+ if ( thisContext instanceof window.Element ) { thisContext[ config.api.scripts ]?.splice( thisContext[ config.api.scripts ].indexOf( script, 1 ) ); }
83
+ }, { subtree: 'cross-roots', timing: 'sync', generation: 'exits' } );
84
84
  }
85
85
 
86
86
  /**
@@ -94,7 +94,7 @@ function realtime( config ) {
94
94
  const window = this, { webqit: { oohtml, realdom, QuantumScript, AsyncQuantumScript, QuantumModule } } = window;
95
95
  if ( !window.HTMLScriptElement.supports ) { window.HTMLScriptElement.supports = type => [ 'text/javascript', 'application/javascript' ].includes( type ); }
96
96
  const handled = new WeakSet;
97
- realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( config.scriptSelector, record => {
97
+ realdom.realtime( window.document ).query( config.scriptSelector, record => {
98
98
  record.entrants.forEach( script => {
99
99
  if ( handled.has( script ) ) return;
100
100
  const textContent = ( script._ = script.textContent.trim() ) && script._.startsWith( '/*@oohtml*/if(false){' ) && script._.endsWith( '}/*@oohtml*/' ) ? script._.slice( 21, -12 ) : script.textContent;
@@ -124,6 +124,6 @@ function realtime( config ) {
124
124
  script.textContent = `webqit.oohtml.Script.execute( '${ execHash }' );`;
125
125
  }
126
126
  } );
127
- }, { live: true, timing: 'intercept', generation: 'entrants', eventDetails: true } );
127
+ }, { live: true, subtree: 'cross-roots', timing: 'intercept', generation: 'entrants', eventDetails: true } );
128
128
  // ---
129
129
  }