@webqit/oohtml 2.1.52 → 2.1.53

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.
Files changed (45) hide show
  1. package/README.md +14 -18
  2. package/dist/bindings-api.js +1 -1
  3. package/dist/bindings-api.js.map +3 -3
  4. package/dist/context-api.js +1 -1
  5. package/dist/context-api.js.map +3 -3
  6. package/dist/html-bracelets.js +2 -0
  7. package/dist/html-bracelets.js.map +7 -0
  8. package/dist/html-imports.js +1 -1
  9. package/dist/html-imports.js.map +3 -3
  10. package/dist/html-namespaces.js +2 -0
  11. package/dist/html-namespaces.js.map +7 -0
  12. package/dist/main.js +16 -24
  13. package/dist/main.js.map +3 -3
  14. package/dist/scoped-css.js +2 -2
  15. package/dist/scoped-css.js.map +3 -3
  16. package/dist/scoped-js.js +15 -23
  17. package/dist/scoped-js.js.map +3 -3
  18. package/package.json +7 -7
  19. package/src/bindings-api/_HTMLBindingsProvider.js +38 -0
  20. package/src/bindings-api/index.js +49 -26
  21. package/src/context-api/ContextReturnValue.js +22 -0
  22. package/src/context-api/HTMLContext.js +14 -6
  23. package/src/context-api/HTMLContextProvider.js +27 -10
  24. package/src/context-api/_ContextRequestEvent.js +2 -2
  25. package/src/context-api/index.js +26 -10
  26. package/src/html-bracelets/AttrBracelet.js +109 -0
  27. package/src/html-bracelets/Bracelet.js +78 -0
  28. package/src/html-bracelets/HTMLBracelets.js +67 -0
  29. package/src/html-bracelets/TextBracelet.js +69 -0
  30. package/src/html-bracelets/index.js +71 -0
  31. package/src/html-bracelets/targets.browser.js +10 -0
  32. package/src/html-imports/_HTMLExportsManager.js +2 -2
  33. package/src/html-imports/_HTMLImportsProvider.js +3 -9
  34. package/src/html-imports/index.js +8 -8
  35. package/src/{namespace-api → html-namespaces}/index.js +25 -25
  36. package/src/index.js +14 -12
  37. package/src/scoped-js/Hash.js +26 -0
  38. package/src/scoped-js/index.js +63 -63
  39. package/test/index.js +1 -1
  40. package/test/modules.test.js +1 -1
  41. package/test/scoped-js.test.js +1 -1
  42. package/dist/namespace-api.js +0 -2
  43. package/dist/namespace-api.js.map +0 -7
  44. package/src/scoped-js/Compiler.js +0 -299
  45. /package/src/{namespace-api → html-namespaces}/targets.browser.js +0 -0
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "wicg-proposal"
15
15
  ],
16
16
  "homepage": "https://webqit.io/tooling/oohtml",
17
- "version": "2.1.52",
17
+ "version": "2.1.53",
18
18
  "license": "MIT",
19
19
  "repository": {
20
20
  "type": "git",
@@ -32,19 +32,19 @@
32
32
  "scripts": {
33
33
  "test": "mocha --extension .test.js --exit",
34
34
  "test:coverage": "c8 --reporter=text-lcov npm run test | coveralls",
35
- "build": "esbuild main=src/targets.browser.js bindings-api=src/bindings-api/targets.browser.js context-api=src/context-api/targets.browser.js namespace-api=src/namespace-api/targets.browser.js html-imports=src/html-imports/targets.browser.js scoped-js=src/scoped-js/targets.browser.js scoped-css=src/scoped-css/targets.browser.js --bundle --minify --sourcemap --outdir=dist",
36
- "preversion": "npm run test && npm run build && git add -A dist",
35
+ "build": "esbuild main=src/targets.browser.js context-api=src/context-api/targets.browser.js bindings-api=src/bindings-api/targets.browser.js html-bracelets=src/html-bracelets/targets.browser.js html-namespaces=src/html-namespaces/targets.browser.js html-imports=src/html-imports/targets.browser.js scoped-js=src/scoped-js/targets.browser.js scoped-css=src/scoped-css/targets.browser.js --bundle --minify --sourcemap --outdir=dist",
36
+ "preversion": "npm run build && git add -A dist",
37
37
  "postversion": "npm publish",
38
38
  "postpublish": "git push && git push --tags"
39
39
  },
40
40
  "dependencies": {
41
- "@webqit/observer": "^2.1.5",
42
- "@webqit/realdom": "^2.1.13",
43
- "@webqit/reflex-functions": "^2.1.50",
41
+ "@webqit/observer": "^2.2.9",
42
+ "@webqit/realdom": "^2.1.17-0",
43
+ "@webqit/stateful-js": "^3.0.14-0",
44
44
  "@webqit/util": "^0.8.11"
45
45
  },
46
46
  "devDependencies": {
47
- "@webqit/oohtml-ssr": "^1.2.15",
47
+ "@webqit/oohtml-ssr": "^1.2.16",
48
48
  "chai": "^4.3.4",
49
49
  "coveralls": "^3.1.1",
50
50
  "esbuild": "^0.14.43",
@@ -0,0 +1,38 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import Observer from '@webqit/observer';
6
+ import { HTMLContextProvider } from '../context-api/index.js';
7
+
8
+ export default class _HTMLBindingsProvider extends HTMLContextProvider {
9
+
10
+ static type = 'bindings';
11
+
12
+ /**
13
+ * @matchesRequest
14
+ */
15
+ static matchRequest( id, request ) {
16
+ return super.matchRequest( id, request ) && ( !request.detail || !id.detail || ( Array.isArray( request.detail ) ? request.detail[ 0 ] === id.detail : request.detail === id.detail ) );
17
+ }
18
+
19
+ /**
20
+ * @bindingsObj
21
+ */
22
+ get bindingsObj() {
23
+ return this.host[ this.constructor.config.api.bindings ];
24
+ }
25
+
26
+ /**
27
+ * @handle()
28
+ */
29
+ handle( event ) {
30
+ // Any existing event.request.controller? Abort!
31
+ event.request.controller?.abort();
32
+ if ( !( event.request.detail + '' ).trim() ) return event.respondWith( this.bindingsObj );
33
+ event.request.controller = Observer.reduce( this.bindingsObj, Array.isArray( event.request.detail ) ? event.request.detail : [ event.request.detail ], Observer.get, descriptor => {
34
+ if ( this.disposed ) return; // If already scheduled but aborted as in provider unmounting
35
+ event.respondWith( descriptor.value );
36
+ }, { live: event.request.live, descripted: true } );
37
+ }
38
+ }
@@ -3,8 +3,11 @@
3
3
  * @imports
4
4
  */
5
5
  import Observer from '@webqit/observer';
6
+ import _HTMLBindingsProvider from './_HTMLBindingsProvider.js';
6
7
  import { _, _init } from '../util.js';
7
8
 
9
+ export { Observer }
10
+
8
11
  /**
9
12
  * @init
10
13
  *
@@ -12,14 +15,16 @@ import { _, _init } from '../util.js';
12
15
  */
13
16
  export default function init( $config = {} ) {
14
17
  const { config, window } = _init.call( this, 'bindings-api', $config, {
18
+ context: { attr: { contextname: 'contextname' }, },
15
19
  api: { bind: 'bind', bindings: 'bindings', },
16
20
  } );
21
+ window.webqit.HTMLBindingsProvider = class extends _HTMLBindingsProvider {
22
+ static get config() { return config; }
23
+ };
17
24
  window.webqit.Observer = Observer;
18
25
  exposeAPIs.call( window, config );
19
26
  }
20
27
 
21
- export { Observer }
22
-
23
28
  /**
24
29
  * @Exports
25
30
  *
@@ -30,10 +35,48 @@ function getBindingsObject( node ) {
30
35
  if ( !_( node ).has( 'bindings' ) ) {
31
36
  const bindingsObj = Object.create( null );
32
37
  _( node ).set( 'bindings', bindingsObj );
38
+ Observer.observe( bindingsObj, mutations => {
39
+ for ( const mutation of mutations ) {
40
+ if ( mutation.type === 'delete' ) {
41
+ detachBindingsContext.call( this, node, mutation.key );
42
+ } else { attachBindingsContext.call( this, node, mutation.key ); }
43
+ }
44
+ } );
33
45
  }
34
46
  return _( node ).get( 'bindings' );
35
47
  }
36
48
 
49
+ function attachBindingsContext( host, key ) {
50
+ const window = this, { HTMLBindingsProvider } = window.webqit;
51
+ const contextId = HTMLBindingsProvider.createId( host, { detail: key } );
52
+ HTMLBindingsProvider.attachTo( host, contextId );
53
+ }
54
+
55
+ function detachBindingsContext( host, key ) {
56
+ const window = this, { HTMLBindingsProvider } = window.webqit;
57
+ const contextId = HTMLBindingsProvider.createId( host, { detail: key } );
58
+ HTMLBindingsProvider.detachFrom( host, contextId );
59
+ }
60
+
61
+ /**
62
+ * Exposes Bindings with native APIs.
63
+ *
64
+ * @param document|Element target
65
+ * @param Object bindings
66
+ * @param Object params
67
+ *
68
+ * @return Void
69
+ */
70
+ function applyBindings( target, bindings, { merge, diff, namespace } = {} ) {
71
+ const bindingsObj = getBindingsObject.call( this, target );
72
+ const $params = { diff, namespace };
73
+ const exitingKeys = merge ? [] : Observer.ownKeys( bindingsObj, $params ).filter( key => !( key in bindings ) );
74
+ return Observer.batch( bindingsObj, () => {
75
+ if ( exitingKeys.length ) { Observer.deleteProperties( bindingsObj, exitingKeys, $params ); }
76
+ return Observer.set( bindingsObj, bindings, $params );
77
+ }, $params );
78
+ }
79
+
37
80
  /**
38
81
  * Exposes Bindings with native APIs.
39
82
  *
@@ -50,35 +93,15 @@ function exposeAPIs( config ) {
50
93
  if ( config.api.bindings in window.Element.prototype ) { throw new Error( `The "Element" class already has a "${ config.api.bindings }" property!` ); }
51
94
  // Definitions
52
95
  Object.defineProperty( window.document, config.api.bind, { value: function( bindings, config = {} ) {
53
- return applyBindings( window.document, bindings, config );
96
+ return applyBindings.call( window, window.document, bindings, config );
54
97
  } });
55
98
  Object.defineProperty( window.document, config.api.bindings, { get: function() {
56
- return Observer.proxy( getBindingsObject( window.document ) );
99
+ return Observer.proxy( getBindingsObject.call( window, window.document ) );
57
100
  } });
58
101
  Object.defineProperty( window.Element.prototype, config.api.bind, { value: function( bindings, config = {} ) {
59
- return applyBindings( this, bindings, config );
102
+ return applyBindings.call( window, this, bindings, config );
60
103
  } });
61
104
  Object.defineProperty( window.Element.prototype, config.api.bindings, { get: function() {
62
- return Observer.proxy( getBindingsObject( this ) );
105
+ return Observer.proxy( getBindingsObject.call( window, this ) );
63
106
  } } );
64
107
  }
65
-
66
- /**
67
- * Exposes Bindings with native APIs.
68
- *
69
- * @param document|Element target
70
- * @param Object bindings
71
- * @param Object params
72
- *
73
- * @return Void
74
- */
75
- function applyBindings( target, bindings, { merge, diff, namespace } = {} ) {
76
- const bindingsObj = getBindingsObject( target );
77
- const $params = { diff, namespace };
78
- if ( merge ) return Observer.set( bindingsObj, bindings, $params );;
79
- const exitingKeys = Observer.ownKeys( bindingsObj, $params ).filter( key => !( key in bindings ) );
80
- return Observer.batch( bindingsObj, () => {
81
- if ( exitingKeys.length ) { Observer.deleteProperty( bindingsObj, exitingKeys, $params ); }
82
- return Observer.set( bindingsObj, bindings, $params );
83
- }, $params );
84
- }
@@ -0,0 +1,22 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import Observer from "@webqit/observer";
6
+
7
+ export default class ContextReturnValue {
8
+ constructor( request, hostElement ) {
9
+ this.request = request;
10
+ this.hostElement = hostElement;
11
+ if ( request.live && !request.signal ) {
12
+ Object.defineProperty( this, 'abortController', { value: new AbortController } );
13
+ request.signal = this.abortController.signal;
14
+ }
15
+ }
16
+ callback( response ) { Observer.defineProperty( this, 'value', { value: response, configurable: true, enumerable: true } ); }
17
+ abort() {
18
+ if ( this.abortController ) { return this.abortController.abort(); }
19
+ const window = this.hostElement.ownerDocument?.defaultView || this.hostElement.defaultView;
20
+ if ( this.request.signal ) { return this.request.signal.dispatchEvent( new window.Event( 'abort' ) ); }
21
+ }
22
+ }
@@ -2,6 +2,7 @@
2
2
  /**
3
3
  * @imports
4
4
  */
5
+ import ContextReturnValue from './ContextReturnValue.js';
5
6
  import _ContextRequestEvent from './_ContextRequestEvent.js';
6
7
  import { _ } from '../util.js';
7
8
 
@@ -25,8 +26,7 @@ export default class HTMLContext {
25
26
  const ContextRequestEvent = _ContextRequestEvent.call( host.ownerDocument?.defaultView || host.defaultView );
26
27
  Object.defineProperty( this, 'ContextRequestEvent', { get: () => ContextRequestEvent } );
27
28
  this[ Symbol.iterator ] = function*() {
28
- const it = priv.contexts[ Symbol.iterator ]();
29
- yield it.next().value;
29
+ yield* priv.contexts;
30
30
  }
31
31
  }
32
32
 
@@ -63,10 +63,18 @@ export default class HTMLContext {
63
63
  /**
64
64
  * @request()
65
65
  */
66
- request( request, callback, options = {} ) {
67
- return this[ '#' ].host.dispatchEvent(
68
- new this.ContextRequestEvent( request, callback, { bubbles: true, ...options } )
69
- );
66
+ request( request, callback = null, options = {} ) {
67
+ if ( typeof callback === 'object' ) {
68
+ options = callback;
69
+ callback = null;
70
+ }
71
+ let contextReturnValue;
72
+ if ( !callback ) {
73
+ contextReturnValue = new ContextReturnValue( request, this[ '#' ].host );
74
+ callback = contextReturnValue.callback.bind( contextReturnValue );
75
+ }
76
+ const returnValue = this[ '#' ].host.dispatchEvent( new this.ContextRequestEvent( request, callback, { bubbles: true, ...options } ) );
77
+ return contextReturnValue ?? returnValue;
70
78
  }
71
79
 
72
80
  /**
@@ -7,6 +7,16 @@ import HTMLContext from './HTMLContext.js';
7
7
 
8
8
  export default class HTMLContextProvider {
9
9
 
10
+ /**
11
+ * For reference purposes
12
+ */
13
+ static providers = new Map;
14
+
15
+ /**
16
+ * To be implemented by subclasses
17
+ */
18
+ static type;
19
+
10
20
  /**
11
21
  * @config
12
22
  */
@@ -18,20 +28,21 @@ export default class HTMLContextProvider {
18
28
  * @attachTo
19
29
  */
20
30
  static attachTo( host, Id, multiple = false ) {
31
+ this.providers.set( this.type, this );
21
32
  let provider, contextMgr = HTMLContext.instance( host );
22
- if ( !multiple && ( provider = contextMgr.findProvider( cx => this.matchRequest( cx.id, Id, true ) ) ) ) return provider;
33
+ if ( !multiple && ( provider = contextMgr.findProvider( provider => this.matchId( provider.id, Id ) ) ) ) return provider;
23
34
  return contextMgr.attachProvider( new this( Id ) );
24
35
  }
25
36
 
26
37
  /**
27
38
  * @detachFrom
28
39
  */
29
- static detachFrom( host, Id, multiple = false ) {
40
+ static detachFrom( host, Id, multipleOrFilter = false ) {
30
41
  let provider, contextMgr = HTMLContext.instance( host );
31
42
  for ( provider of contextMgr[ '#' ].contexts ) {
32
- if ( !this.matchRequest( provider.id, Id, true ) || ( typeof multiple === 'function' && !multiple( provider ) ) ) continue;
43
+ if ( !this.matchId( provider.id, Id ) || ( typeof multipleOrFilter === 'function' && !multipleOrFilter( provider ) ) ) continue;
33
44
  contextMgr.detachProvider( provider );
34
- if ( typeof multiple !== 'function' && !multiple ) return provider;
45
+ if ( typeof multiple !== 'function' && !multipleOrFilter ) return provider;
35
46
  }
36
47
  }
37
48
 
@@ -39,7 +50,7 @@ export default class HTMLContextProvider {
39
50
  * @createId
40
51
  */
41
52
  static createId( host, fields = {} ) {
42
- const id = { ...fields };
53
+ const id = { type: this.type, ...fields };
43
54
  if ( id.contextName ) return id;
44
55
  if ( host.getAttribute && !( id.contextName = ( host.getAttribute( this.config.context.attr.contextname ) || '' ).trim() ) ) {
45
56
  delete id.contextName;
@@ -48,20 +59,26 @@ export default class HTMLContextProvider {
48
59
  }
49
60
  return id;
50
61
  }
62
+
63
+ /**
64
+ * @matchId
65
+ */
66
+ static matchId( a, b ) {
67
+ return _compare( a, b, 1, true );
68
+ }
51
69
 
52
70
  /**
53
71
  * @createRequest
54
72
  */
55
73
  static createRequest( fields = {} ) {
56
- return { ...fields };
74
+ return { type: this.type, ...fields };
57
75
  }
58
76
 
59
77
  /**
60
78
  * @matchesRequest
61
79
  */
62
- static matchRequest( id, request, strict = false ) {
63
- if ( strict ) return _compare( id, request, 1, true );
64
- return request.type === id.type && !request.contextName || request.contextName === id.contextName;
80
+ static matchRequest( id, request ) {
81
+ return request.type === id.type && ( !request.contextName || request.contextName === id.contextName );
65
82
  }
66
83
 
67
84
  /**
@@ -108,7 +125,7 @@ export default class HTMLContextProvider {
108
125
  */
109
126
  handleEvent( event ) {
110
127
  if ( this.disposed || ( event.target === this.host && event.request?.superContextOnly )
111
- || !event.request || typeof event.callback !== 'function' || !this.constructor.matchRequest( this.id, event.request ) ) return;
128
+ || !( typeof event.request === 'object' && event.request ) || typeof event.respondWith !== 'function' || !this.constructor.matchRequest( this.id, event.request ) ) return;
112
129
  event.stopPropagation();
113
130
  if ( event.type === 'contextclaim' ) {
114
131
  const claims = new Set;
@@ -17,8 +17,8 @@ export default function() {
17
17
  */
18
18
  respondWith( response, ...rest ) {
19
19
  if ( this.request.diff ) {
20
- if ( 'previousValue' in this && this.previousValue === response ) return;
21
- this.previousValue = response;
20
+ if ( 'prevValue' in this && this.prevValue === response ) return;
21
+ Object.defineProperty( this, 'prevValue', { value: response, configurable: true } );
22
22
  }
23
23
  return this.callback( response, ...rest );
24
24
  }
@@ -6,6 +6,14 @@ import { _init } from '../util.js';
6
6
  import HTMLContext from './HTMLContext.js';
7
7
  import HTMLContextProvider from './HTMLContextProvider.js';
8
8
 
9
+ /**
10
+ * @exports
11
+ */
12
+ export {
13
+ HTMLContextProvider,
14
+ HTMLContext,
15
+ }
16
+
9
17
  /**
10
18
  * Initializes HTML Modules.
11
19
  *
@@ -19,7 +27,7 @@ export default function init( $config = {} ) {
19
27
  } );
20
28
  window.webqit.HTMLContextProvider = HTMLContextProvider;
21
29
  window.webqit.HTMLContext = HTMLContext;
22
- exposeModulesObjects.call( window, config );
30
+ exposeAPIs.call( window, config );
23
31
  }
24
32
 
25
33
  /**
@@ -29,7 +37,7 @@ export default function init( $config = {} ) {
29
37
  *
30
38
  * @return Void
31
39
  */
32
- function exposeModulesObjects( config ) {
40
+ function exposeAPIs( config ) {
33
41
  const window = this;
34
42
  // Assertions
35
43
  if ( config.api.context in window.document ) { throw new Error( `document already has a "${ config.api.context }" property!` ); }
@@ -41,12 +49,20 @@ function exposeModulesObjects( config ) {
41
49
  Object.defineProperty( window.HTMLElement.prototype, config.api.context, { get: function() {
42
50
  return HTMLContext.instance( this );
43
51
  } } );
52
+ const waitlist = new Set;
53
+ window.addEventListener( 'contextrequest', event => {
54
+ if ( !( typeof event.request === 'object' && event.request ) || typeof event.respondWith !== 'function' ) return;
55
+ waitlist.add( event );
56
+ event.respondWith();
57
+ } );
58
+ window.addEventListener( 'contextclaim', event => {
59
+ if ( !( typeof event.request === 'object' && event.request ) || typeof event.respondWith !== 'function' ) return;
60
+ const claims = new Set;
61
+ waitlist.forEach( subscriptionEvent => {
62
+ if ( !HTMLContextProvider.providers.get( event.request.type ).matchRequest( event.request/*provider ID*/, subscriptionEvent.request/*request ID*/ ) ) return;
63
+ waitlist.delete( subscriptionEvent );
64
+ claims.add( subscriptionEvent );
65
+ } );
66
+ event.respondWith( claims );
67
+ } );
44
68
  }
45
-
46
- /**
47
- * @exports
48
- */
49
- export {
50
- HTMLContextProvider,
51
- HTMLContext,
52
- }
@@ -0,0 +1,109 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import HTMLBracelets from './HTMLBracelets.js';
6
+ import Bracelet from './Bracelet.js';
7
+ import { _ } from '../util.js';
8
+
9
+ export default class AttrBracelet extends Bracelet {
10
+ static get query() { return `*[@*[${ this.tokens.contains }]]`; }
11
+
12
+ static parse( ...attrs ) {
13
+ return attrs.reduce( ( attrs, attr ) => {
14
+ return attrs.concat( [ ...attr.nodeValue.matchAll( new RegExp( this.tokens.regex, 'g' ) ) ].reduce( ( bracelets, match ) => {
15
+ const bracelet = new this( attr, match[ 0 ], match.index, match[ 1 ].trim() );
16
+ const prev = bracelets.slice( -1 )[ 0 ];
17
+ if ( prev ) { prev._nextSibling = bracelet; }
18
+ return bracelets.concat( bracelet );
19
+ }, [] ) );
20
+ }, [] );
21
+ }
22
+
23
+ static mount( ...bracelets ) {
24
+ for ( const bracelet of bracelets ) {
25
+ // Add to attr-specific registry
26
+ let attrBraceletsRegistry = _( bracelet.ownerElement ).get( 'attr-bracelets' );
27
+ if ( !attrBraceletsRegistry ) {
28
+ attrBraceletsRegistry = new Map;
29
+ attrBraceletsRegistry.active = [];
30
+ _( bracelet.ownerElement ).set( 'attr-bracelets', attrBraceletsRegistry );
31
+ }
32
+ let attrBracelets = attrBraceletsRegistry.get( bracelet.attr.nodeName );
33
+ if ( !attrBracelets ) {
34
+ attrBracelets = new Set;
35
+ attrBraceletsRegistry.set( bracelet.attr.nodeName, attrBracelets );
36
+ }
37
+ attrBracelets.add( bracelet );
38
+ attrBraceletsRegistry.active[ 0 ]?._nested.add( bracelet );
39
+ // Add to general registry
40
+ HTMLBracelets.instance( bracelet.ownerElement ).add( bracelet );
41
+ }
42
+ }
43
+
44
+ static cleanup( ...attrs ) {
45
+ for ( const attr of attrs ) {
46
+ // Remove from attr-specific registry
47
+ const attrBraceletsRegistry = _( attr.ownerElement ).get( 'attr-bracelets' );
48
+ attrBraceletsRegistry?.get( attr.nodeName )?.forEach( bracelet => {
49
+ bracelet.disconnect();
50
+ // Remove from general registry
51
+ HTMLBracelets.instance( bracelet.ownerElement ).delete( bracelet );
52
+ } );
53
+ attrBraceletsRegistry?.delete( attr.nodeName );
54
+ if ( attrBraceletsRegistry && !attrBraceletsRegistry.size ) { _( attr.ownerElement ).delete( 'attr-bracelets' ); }
55
+ }
56
+ }
57
+
58
+ constructor( attr, _value, startIndex, expr ) {
59
+ super();
60
+ const $refs = [], $expr = this.parseExpr( expr, $refs );
61
+ Object.defineProperties( this, {
62
+ type: { get: () => 'attr' },
63
+ expr: { get: () => $expr },
64
+ refs: { get: () => $refs },
65
+ attr: { get: () => attr },
66
+ ownerElement: { get: () => attr.ownerElement },
67
+ originalValue: { value: _value },
68
+ _value: { value: _value, writable: true },
69
+ _dirty: { value: false, writable: true },
70
+ _startIndex: { value: undefined, writable: true },
71
+ _endIndex: { value: undefined, writable: true },
72
+ _nextSibling: { value: undefined, writable: true },
73
+ _nested: { value: new Set },
74
+ } );
75
+ this.startIndex = startIndex;
76
+ }
77
+
78
+ get startIndex() { return this._startIndex; }
79
+ set startIndex( value ) {
80
+ this._startIndex = value;
81
+ this.endIndex = this._startIndex + this.value.length;
82
+ }
83
+
84
+ get endIndex() { return this._endIndex; }
85
+ set endIndex( value ) {
86
+ if ( value === this._endIndex ) return;
87
+ if ( this.nextSibling ) { this.nextSibling.startIndex += value - this._endIndex; }
88
+ this._endIndex = value;
89
+ }
90
+
91
+ get value() { return this._value; }
92
+ set value( value ) {
93
+ if ( this.disconnected || value === this._value ) return;
94
+ this._value = value;
95
+ this._dirty = true;
96
+ // Set attribute; but first disconnect any "nested"
97
+ this._nested.forEach( p => p.disconnect() );
98
+ const attrBraceletsRegistry = _( this.ownerElement ).get( 'attr-bracelets' );
99
+ attrBraceletsRegistry.active.unshift( this );
100
+ this.ownerElement.setAttribute( this.attr.nodeName, this.attr.nodeValue.substring( 0, this.startIndex ) + value + this.attr.nodeValue.substring( this.endIndex ) );
101
+ attrBraceletsRegistry.active.shift();
102
+ // Reindex
103
+ const newEndIndex = this.startIndex + value.length;
104
+ if ( newEndIndex !== this.endIndex ) { this.endIndex = newEndIndex; }
105
+ }
106
+
107
+ get nextSibling() { return this._nextSibling; }
108
+ get dirty() { return this._dirty; }
109
+ }
@@ -0,0 +1,78 @@
1
+
2
+ export default class Bracelet {
3
+ static tokens = {
4
+ startTag: `{`, endTag: `}`,
5
+ get contains () { return `contains(., "${ this.startTag }") and contains(substring-after(., "${ this.startTag }"), "${ this.endTag }")`; },
6
+ get startsAndEnds() { return `starts-with(., "${ this.startTag }") and substring(., string-length(.) - string-length("${ this.endTag }") + 1) = "${ this.endTag }"`; },
7
+ get regex() {
8
+ const startTag = this.startTag.split( '' ).map( s => '\\' + s );
9
+ const endTag = this.endTag.split( '' ).map( s => '\\' + s );
10
+ return `${ startTag.join( '' ) }([^${ startTag[ 0 ] }]+)${ endTag.join( '' ) }`;
11
+ }
12
+ }
13
+
14
+ parseExpr( expr, refs ) {
15
+ const tokens = expr.match( /[\+\-\*\/&|\?\:\=\<\>\%]+|(['"])[^'"]*\1|\[[^\]]*\]|\([^)]*\)|\S+/g ).map( s => s.trim() );
16
+ return tokens.map( s => {
17
+ let meta = {};
18
+ if ( s[ 0 ] === '!' ) { meta = { negation: true }; s = s.slice( 1 ); }
19
+ if ( /^\(.*\)$/.test( s ) ) return { type: 'expr', value: this.parseExpr( s.slice( 1, -1 ), refs ), ...meta }; // must be before the operator test
20
+ if ( [ '"', "'" ].includes( s[ 0 ] ) ) return { type: 'literal', value: /(['"])(.*?)\1/g.exec( s )[ 2 ], ...meta }; // must be before the operator test
21
+ if ( /[\+\-\*\/&|\?\:\=\<\>\%]/.test( s ) ) return { type: 'operator', value: s, ...meta };
22
+ if ( !isNaN( s ) ) return { type: 'literal', value: parseFloat( s ), ...meta };
23
+ const ref = s.match( /[^\.\[\]]+/g );
24
+ refs.push( ref );
25
+ return { type: 'ref', value: ref, ...meta };
26
+ } );
27
+ }
28
+
29
+ renderExpr( expr, bindings ) {
30
+ return expr.reduce( ( [ prev, operator, state ], token ) => {
31
+ // No further evaluations allowed?
32
+ if ( state === 'end' ) return [ prev, null, 'end' ];
33
+ // Mode has been consequent and we've now hit the alternate block?
34
+ if ( state === 'consequent' && token.type === 'operator' && token.value === ':' ) return [ prev, null, 'end' ];
35
+ // Always return operators at this level
36
+ if ( token.type === 'operator' ) return [ prev, token, state ];
37
+ // Still expecting to hit the alternate block?
38
+ if ( state === 'alternate' && operator?.value !== ':' ) return [ null, null, 'alternate' ];
39
+ // Main...
40
+ let value, render = ( token, val ) => token.negation ? !val : val;
41
+ switch ( token.type ) {
42
+ case 'ref': value = render( token, bindings[ token.value.join( '.' ) ].value ); break;
43
+ case 'expr': value = render( token, this.renderExpr( token.value, bindings ) ); break;
44
+ default: value = render( token, token.value );
45
+ }
46
+ switch ( operator?.value ) {
47
+ case '-': return [ prev - value, null, state ];
48
+ case '+': return [ prev + value, null, state ];
49
+ case '/': return [ prev / value, null, state ];
50
+ case '*': return [ prev * value, null, state ];
51
+ case '%': return [ prev % value, null, state ];
52
+ case '===': return [ prev === value, null, state ];
53
+ case '==': return [ render( operator, prev == value ), null, state ];
54
+ case '>=': return [ prev >= value, null, state ];
55
+ case '<=': return [ prev <= value, null, state ];
56
+ case '>': return [ prev > value, null, state ];
57
+ case '<': return [ prev < value, null, state ];
58
+ case '||': return [ prev || value, null, state ];
59
+ case '&&': return [ prev && value, null, state ];
60
+ case '?': return prev ? [ value, null, 'consequent' ] : [ null, null, 'alternate' ];
61
+ case '??': return [ prev ?? value, null, state ];
62
+ default: return [ value ];
63
+ }
64
+ }, [ null, null ] )[ 0 ];
65
+ }
66
+
67
+ render( bindings ) {
68
+ const value = this.renderExpr( this.expr, bindings );
69
+ if ( typeof value === 'undefined' ) {
70
+ if ( !this.dirty ) return;
71
+ this.value = this.originalValue;
72
+ return;
73
+ }
74
+ this.value = value + '';
75
+ }
76
+
77
+ disconnect() { this.disconnected = true; }
78
+ }