@webqit/oohtml 3.1.13 → 3.2.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.
@@ -2,24 +2,51 @@
2
2
  /**
3
3
  * @imports
4
4
  */
5
- import { _init } from '../util.js';
5
+ import { rewriteSelector } from '../namespaced-html/index.js';
6
+ import { _init, _toHash, _splitOuter } from '../util.js';
6
7
 
7
8
  /**
8
9
  * @init
9
10
  *
10
11
  * @param Object $config
11
12
  */
12
- export default function init( { advanced = {}, ...$config } ) {
13
+ export default function init({ advanced = {}, ...$config }) {
13
14
  const { config, window } = _init.call( this, 'scoped-css', $config, {
15
+ api: { styleSheets: 'styleSheets' },
14
16
  style: { retention: 'retain', mimeType: '', strategy: null },
15
17
  } );
16
- config.styleSelector = ( Array.isArray( config.style.mimeType ) ? config.style.mimeType : [ config.style.mimeType ] ).reduce( ( selector, mm ) => {
18
+ config.styleSelector = (Array.isArray( config.style.mimeType ) ? config.style.mimeType : [ config.style.mimeType ] ).reduce( ( selector, mm ) => {
17
19
  const qualifier = mm ? `[type=${ window.CSS.escape( mm ) }]` : '';
18
20
  return selector.concat( `style${ qualifier }[scoped]` );
19
21
  }, [] ).join( ',' );
22
+ window.webqit.oohtml.Style = {
23
+ compileCache: new Map,
24
+ };
25
+ exposeAPIs.call( window, config );
20
26
  realtime.call( window, config );
21
27
  }
22
28
 
29
+ /**
30
+ * Exposes Bindings with native APIs.
31
+ *
32
+ * @param Object config
33
+ *
34
+ * @return Void
35
+ */
36
+ function exposeAPIs( config ) {
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
+ }, } );
43
+ Object.defineProperty( window.HTMLStyleElement.prototype, 'scoped', {
44
+ configurable: true,
45
+ get() { return this.hasAttribute( 'scoped' ); },
46
+ set( value ) { this.toggleAttribute( 'scoped', value ); },
47
+ } );
48
+ }
49
+
23
50
  /**
24
51
  * Performs realtime capture of elements and builds their relationships.
25
52
  *
@@ -28,22 +55,87 @@ export default function init( { advanced = {}, ...$config } ) {
28
55
  * @return Void
29
56
  */
30
57
  function realtime( config ) {
31
- const window = this, { webqit: { realdom } } = window;
32
- if ( !window.HTMLScriptElement.supports ) { window.HTMLScriptElement.supports = () => false; }
33
- const handled = () => {};
34
- realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( config.styleSelector, record => {
58
+ const window = this, { webqit: { oohtml, realdom } } = window;
59
+ if ( !window.CSS.supports ) { window.CSS.supports = () => false; }
60
+ const handled = new WeakSet;
61
+ realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( config.styleSelector, record => {
35
62
  record.entrants.forEach( style => {
36
- if ( 'scoped' in style ) return handled( style );
37
- if ( config.style.strategy === '@scope' ) {
38
- Object.defineProperty( style, 'scoped', { value: style.hasAttribute( 'scoped' ) } );
39
- if ( style.hasAttribute( 'ref' ) ) return; // Server-rendered
40
- const uuid = `scoped${ uniqId() }`;
41
- style.setAttribute( 'ref', uuid );
42
- style.textContent = `@scope from (:has(> style[ref="${ uuid }"])) {\n${ style.textContent }\n}`;
63
+ if ( handled.has( style ) ) return;
64
+ handled.add( style );
65
+ if ( !style.scoped ) return;
66
+ // Do compilation
67
+ const sourceHash = _toHash( style.textContent );
68
+ let compiledSheet, supportsHAS = CSS.supports( 'selector(:has(a,b))' );
69
+ if ( !( compiledSheet = oohtml.Style.compileCache.get( sourceHash ) ) ) {
70
+ const scopeSelector = supportsHAS ? `:has(> style[rand-${ sourceHash }])` : `[rand-${ sourceHash }]`;
71
+ compiledSheet = createAdoptableStylesheet.call( window, style, scopeSelector );
72
+ //compiledSheet = style.sheet; upgradeSheet( style.sheet, /*!window.CSSScopeRule &&*/ scopeSelector );
73
+ oohtml.Style.compileCache.set( sourceHash, compiledSheet );
43
74
  }
75
+ // Run now!!!
76
+ style.parentNode[ config.api.styleSheets ].push( style );
77
+ Object.defineProperty( style, 'sheet', { value: compiledSheet, configurable: true } );
78
+ ( supportsHAS ? style : style.parentNode ).toggleAttribute( `rand-${ sourceHash }`, true );
79
+ style.textContent = '\n/*[ Shared style sheet ]*/\n';
44
80
  } );
45
- }, { live: true, timing: 'intercept', generation: 'entrants' } );
81
+ }, { live: true, timing: 'intercept', generation: 'entrants' } );
46
82
  // ---
47
83
  }
48
84
 
49
- const uniqId = () => (0|Math.random()*9e6).toString(36);
85
+ function createAdoptableStylesheet( style, scopeSelector ) {
86
+ const window = this, textContent = style.textContent, supportsScope = window.CSSScopeRule && false/* Disabled for buggy behaviour: rewriting selectorText within an @scope block invalidates the scoping */;
87
+ let styleSheet, cssText = supportsScope ? `@scope (${ scopeSelector }) {\n${ textContent.trim() }\n}` : textContent.trim();
88
+ try {
89
+ styleSheet = new window.CSSStyleSheet;
90
+ styleSheet.replaceSync( cssText );
91
+ upgradeSheet( styleSheet, !supportsScope && scopeSelector );
92
+ document.adoptedStyleSheets.push( styleSheet );
93
+ } catch( e ) {
94
+ const style = window.document.createElement( 'style' );
95
+ window.document.body.appendChild( style );
96
+ style.textContent = cssText;
97
+ styleSheet = style.sheet;
98
+ upgradeSheet( styleSheet, !supportsScope && scopeSelector );
99
+ }
100
+ return styleSheet;
101
+ }
102
+
103
+ function upgradeSheet( styleSheet, scopeSelector = null ) {
104
+ const l = styleSheet?.cssRules.length || -1;
105
+ for ( let i = 0; i < l; ++i ) {
106
+ const cssRule = styleSheet.cssRules[ i ];
107
+ if ( cssRule instanceof CSSImportRule ) {
108
+ // Handle imported stylesheets
109
+ //upgradeSheet( cssRule.styleSheet, scopeSelector );
110
+ continue;
111
+ }
112
+ upgradeRule( cssRule, scopeSelector );
113
+ }
114
+ }
115
+
116
+ function upgradeRule( cssRule, scopeSelector = null ) {
117
+ if ( cssRule instanceof CSSStyleRule ) {
118
+ // Resolve relative IDs and scoping (for non-@scope browsers)
119
+ upgradeSelector( cssRule, scopeSelector );
120
+ return;
121
+ }
122
+ if ( [ window.CSSScopeRule, window.CSSMediaRule, window.CSSContainerRule, window.CSSSupportsRule, window.CSSLayerBlockRule ].some( type => type && cssRule instanceof type ) ) {
123
+ // Parse @rule blocks
124
+ const l = cssRule.cssRules.length;
125
+ for ( let i = 0; i < l; ++i ) {
126
+ upgradeRule( cssRule.cssRules[ i ], scopeSelector );
127
+ }
128
+ }
129
+ }
130
+
131
+ function upgradeSelector( cssRule, scopeSelector = null ) {
132
+ const newSelectorText = rewriteSelector( cssRule.selectorText, scopeSelector, true );
133
+ cssRule.selectorText = newSelectorText;
134
+ // Parse nested blocks. (CSS nesting)
135
+ if ( cssRule.cssRules ) {
136
+ const l = cssRule.cssRules.length;
137
+ for ( let i = 0; i < l; ++i ) {
138
+ upgradeSelector( cssRule.cssRules[ i ], /* Nesting has nothing to do with scopeSelector */ );
139
+ }
140
+ }
141
+ }
@@ -3,34 +3,63 @@
3
3
  * @imports
4
4
  */
5
5
  import { resolveParams } from '@webqit/quantum-js/params';
6
- import { _init } from '../util.js';
7
- import Hash from './Hash.js';
6
+ import { _init, _toHash, _fromHash } from '../util.js';
8
7
 
9
8
  /**
10
9
  * @init
11
10
  *
12
11
  * @param Object $config
13
12
  */
14
- export default function init( { advanced = {}, ...$config } ) {
13
+ export default function init({ advanced = {}, ...$config }) {
15
14
  const { config, window } = _init.call( this, 'scoped-js', $config, {
16
15
  script: { retention: 'retain', mimeType: '' },
17
- advanced: resolveParams( advanced ),
16
+ api: { scripts: 'scripts' },
17
+ advanced: resolveParams(advanced),
18
18
  } );
19
- config.scriptSelector = ( Array.isArray( config.script.mimeType ) ? config.script.mimeType : [ config.script.mimeType ] ).reduce( ( selector, mm ) => {
20
- const qualifier = mm ? `[type=${ window.CSS.escape( mm ) }]` : '';
19
+ config.scriptSelector = ( Array.isArray( config.script.mimeType ) ? config.script.mimeType : [ config.script.mimeType ] ).reduce( ( selector, mm ) => {
20
+ const qualifier = mm ? `[type=${ window.CSS.escape( mm ) } ]` : '';
21
21
  return selector.concat( `script${ qualifier }[scoped],script${ qualifier }[quantum]` );
22
22
  }, [] ).join( ',' );
23
23
  window.webqit.oohtml.Script = {
24
24
  compileCache: [ new Map, new Map, ],
25
25
  execute: execute.bind( window, config ),
26
26
  };
27
+ exposeAPIs.call( window, config );
27
28
  realtime.call( window, config );
28
29
  }
29
30
 
31
+ /**
32
+ * Exposes Bindings with native APIs.
33
+ *
34
+ * @param Object config
35
+ *
36
+ * @return Void
37
+ */
38
+ function exposeAPIs( config ) {
39
+ const window = this, scriptsMap = new Map;
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() {
42
+ if ( !scriptsMap.has( this ) ) { scriptsMap.set( this, [] ); }
43
+ return scriptsMap.get( this );
44
+ }, } );
45
+ Object.defineProperties( window.HTMLScriptElement.prototype, {
46
+ scoped: {
47
+ configurable: true,
48
+ get() { return this.hasAttribute( 'scoped' ); },
49
+ set( value ) { this.toggleAttribute( 'scoped', value ); },
50
+ },
51
+ quantum: {
52
+ configurable: true,
53
+ get() { return this.hasAttribute( 'quantum' ); },
54
+ set( value ) { this.toggleAttribute( 'quantum', value ); },
55
+ },
56
+ } );
57
+ }
58
+
30
59
  // Script runner
31
60
  async function execute( config, execHash ) {
32
61
  const window = this, { realdom } = window.webqit;
33
- const exec = Hash.fromHash( execHash );
62
+ const exec = _fromHash( execHash );
34
63
  if ( !exec ) throw new Error( `Argument must be a valid exec hash.` );
35
64
  const { script, compiledScript, thisContext } = exec;
36
65
  // Honour retention flag
@@ -46,7 +75,7 @@ async function execute( config, execHash ) {
46
75
  if ( script.quantum ) { Object.defineProperty( script, 'state', { value: state } ); }
47
76
  realdom.realtime( window.document ).observe( script, () => {
48
77
  if ( script.quantum ) { state.dispose(); }
49
- if ( script.scoped ) { thisContext.scripts.splice( thisContext.scripts.indexOf( script, 1 ) ); }
78
+ if ( script.scoped ) { thisContext[ config.api.scripts ].splice( thisContext[ config.api.scripts ].indexOf( script, 1 ) ); }
50
79
  }, { subtree: true, timing: 'sync', generation: 'exits' } );
51
80
  }
52
81
 
@@ -58,26 +87,23 @@ async function execute( config, execHash ) {
58
87
  * @return Void
59
88
  */
60
89
  function realtime( config ) {
61
- const window = this, { webqit: { oohtml, realdom, QuantumScript, QuantumAsyncScript, QuantumModule } } = window;
90
+ const window = this, { webqit: { oohtml, realdom, QuantumScript, QuantumAsyncScript, QuantumModule } } = window;
62
91
  if ( !window.HTMLScriptElement.supports ) { window.HTMLScriptElement.supports = () => false; }
63
- const potentialManualTypes = [ 'module' ].concat( config.script.mimeType || [] );
64
- realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( config.scriptSelector, record => {
92
+ const potentialManualTypes = [ 'module' ].concat( config.script.mimeType || [] ), handled = new WeakSet;
93
+ realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( config.scriptSelector, record => {
65
94
  record.entrants.forEach( script => {
66
- if ( script.cloned ) return;
67
- if ( 'quantum' in script ) return handled( script );
68
- Object.defineProperty( script, 'quantum', { value: script.hasAttribute( 'quantum' ) } );
69
- if ( 'scoped' in script ) return handled( script );
70
- Object.defineProperty( script, 'scoped', { value: script.hasAttribute( 'scoped' ) } );
95
+ if ( handled.has( script ) ) return;
96
+ handled.add( script );
71
97
  // Do compilation
72
98
  const textContent = ( script._ = script.textContent.trim() ) && script._.startsWith( '/*@oohtml*/if(false){' ) && script._.endsWith( '}/*@oohtml*/' ) ? script._.slice( 21, -12 ) : script.textContent;
73
- const sourceHash = Hash.toHash( textContent );
99
+ const sourceHash = _toHash( textContent );
74
100
  const compileCache = oohtml.Script.compileCache[ script.quantum ? 0 : 1 ];
75
101
  let compiledScript;
76
102
  if ( !( compiledScript = compileCache.get( sourceHash ) ) ) {
77
103
  const { parserParams, compilerParams, runtimeParams } = config.advanced;
78
- compiledScript = new ( script.type === 'module' ? QuantumModule : ( QuantumScript || QuantumAsyncScript ) )( textContent, {
104
+ compiledScript = new ( script.type === 'module' ? QuantumModule : (QuantumScript || QuantumAsyncScript) )( textContent, {
79
105
  exportNamespace: `#${ script.id }`,
80
- fileName:`${ window.document.url?.split( '#' )?.[ 0 ] || '' }#${ script.id }`,
106
+ fileName: `${ window.document.url?.split( '#' )?.[ 0 ] || '' }#${ script.id }`,
81
107
  parserParams,
82
108
  compilerParams: { ...compilerParams, startStatic: !script.quantum },
83
109
  runtimeParams,
@@ -86,16 +112,13 @@ function realtime( config ) {
86
112
  }
87
113
  // Run now!!!
88
114
  const thisContext = script.scoped ? script.parentNode || record.target : ( script.type === 'module' ? undefined : window );
89
- if ( script.scoped ) {
90
- if ( !thisContext.scripts ) { Object.defineProperty( thisContext, 'scripts', { value: [] } ); }
91
- thisContext.scripts.push( script );
92
- }
93
- const execHash = Hash.toHash( { script, compiledScript, thisContext } );
115
+ if ( script.scoped ) { thisContext[ config.api.scripts ].push( script ); }
116
+ const execHash = _toHash( { script, compiledScript, thisContext } );
94
117
  const manualHandling = record.type === 'query' || ( potentialManualTypes.includes( script.type ) && !window.HTMLScriptElement.supports( script.type ) );
95
118
  if ( manualHandling ) { oohtml.Script.execute( execHash ); } else {
96
119
  script.textContent = `webqit.oohtml.Script.execute( '${ execHash }' );`;
97
120
  }
98
121
  } );
99
- }, { live: true, timing: 'intercept', generation: 'entrants', eventDetails: true } );
122
+ }, { live: true, timing: 'intercept', generation: 'entrants', eventDetails: true } );
100
123
  // ---
101
124
  }
package/src/util.js CHANGED
@@ -53,4 +53,42 @@ export function _compare( a, b, depth = 1, objectSizing = false ) {
53
53
  return ( b = b.slice( 0 ).sort() ) && a.slice( 0 ).sort().every( ( valueA, i ) => valueA === b[ i ] );
54
54
  }
55
55
  return a === b;
56
+ }
57
+
58
+ export function _splitOuter( str, delim ) {
59
+ return [ ...str ].reduce( ( [ quote, depth, splits ], x ) => {
60
+ if ( !quote && depth === 0 && ( Array.isArray( delim ) ? delim : [ delim ] ).includes( x ) ) {
61
+ return [ quote, depth, [ '' ].concat( splits ) ];
62
+ }
63
+ if ( !quote && [ '(', '[', '{' ].includes( x ) && !splits[ 0 ].endsWith( '\\' ) ) depth++;
64
+ if ( !quote && [ ')', ']', '}' ].includes( x ) && !splits[ 0 ].endsWith( '\\' ) ) depth--;
65
+ if ( [ '"', "'", '`' ].includes( x ) && !splits[ 0 ].endsWith( '\\' ) ) {
66
+ quote = quote === x ? null : ( quote || x );
67
+ }
68
+ splits[ 0 ] += x;
69
+ return [ quote, depth, splits ]
70
+ }, [ null, 0, [ '' ] ] )[ 2 ].reverse();
71
+ }
72
+
73
+ // Unique ID generator
74
+ export const _uniqId = () => ( 0 | Math.random() * 9e6 ).toString( 36 );
75
+
76
+ // Hash of anything generator
77
+ const hashTable = new Map;
78
+ export function _toHash( val ) {
79
+ let hash;
80
+ if ( !( hash = hashTable.get( val ) ) ) {
81
+ hash = _uniqId();
82
+ hashTable.set( val, hash );
83
+ }
84
+ return hash;
85
+ }
86
+
87
+ // Value of any hash
88
+ export function _fromHash( hash ) {
89
+ let val;
90
+ hashTable.forEach( ( _hash, _val ) => {
91
+ if ( _hash === hash ) val = _val;
92
+ } );
93
+ return val;
56
94
  }
@@ -10,7 +10,7 @@ describe(`Test: Scoped CSS`, function() {
10
10
  describe(`Styles`, function() {
11
11
 
12
12
  it(`Should do basic rewrite`, async function() {
13
- const head = '<meta name="scoped-css" content="style.strategy=@scope">', body = `
13
+ const head = '', body = `
14
14
  <div>
15
15
  <h1>Hello World!</h1>
16
16
  <style scoped>
@@ -24,7 +24,7 @@ describe(`Test: Scoped CSS`, function() {
24
24
  await delay( 160 ); // Takes time to dynamically load Reflex compiler
25
25
  const styleElement = window.document.querySelector( 'style' );
26
26
 
27
- expect( styleElement.textContent.substring( 0, 13 ) ).to.eq( '@scope from (' );
27
+ //expect( styleElement.textContent.substring( 0, 13 ) ).to.eq( '@scope (' );
28
28
  });
29
29
 
30
30
  });
@@ -1,26 +0,0 @@
1
-
2
- export default class Hash {
3
-
4
- // Unique ID generator
5
- static hashTable = new Map;
6
- static uniqId = () => (0|Math.random()*9e6).toString(36);
7
-
8
- // Hash of anything generator
9
- static toHash( val ) {
10
- let hash;
11
- if ( !( hash = this.hashTable.get( val ) ) ) {
12
- hash = this.uniqId();
13
- this.hashTable.set( val, hash );
14
- }
15
- return hash;
16
- }
17
-
18
- // Value of any hash
19
- static fromHash( hash ) {
20
- let val;
21
- this.hashTable.forEach( ( _hash, _val ) => {
22
- if ( _hash === hash ) val = _val;
23
- } );
24
- return val;
25
- }
26
- }