@thomas-siegfried/tapout 0.0.1 → 0.0.2

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 (47) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +1205 -1205
  3. package/package.json +55 -55
  4. package/src/applyBindings.ts +372 -372
  5. package/src/arrayToDomMapping.ts +300 -300
  6. package/src/attributeInterpolationMarkup.ts +91 -91
  7. package/src/bindingContext.ts +207 -207
  8. package/src/bindingEvent.ts +198 -198
  9. package/src/bindingProvider.ts +260 -260
  10. package/src/bindings.ts +963 -963
  11. package/src/compareArrays.ts +122 -122
  12. package/src/componentBinding.ts +318 -318
  13. package/src/componentDecorator.ts +62 -62
  14. package/src/components.ts +367 -367
  15. package/src/computed.ts +439 -439
  16. package/src/configure.ts +52 -52
  17. package/src/controlFlowBindings.ts +206 -206
  18. package/src/core.ts +60 -60
  19. package/src/decorators.ts +407 -407
  20. package/src/dependencyDetection.ts +76 -76
  21. package/src/disposable.ts +36 -36
  22. package/src/domData.ts +42 -42
  23. package/src/domNodeDisposal.ts +94 -94
  24. package/src/effects.ts +29 -29
  25. package/src/event.ts +173 -173
  26. package/src/expressionRewriting.ts +219 -219
  27. package/src/extenders.ts +102 -102
  28. package/src/filters.ts +91 -91
  29. package/src/index.ts +150 -150
  30. package/src/interpolationMarkup.ts +130 -130
  31. package/src/memoization.ts +71 -71
  32. package/src/namespacedBindings.ts +132 -132
  33. package/src/observable.ts +48 -48
  34. package/src/observableArray.ts +397 -397
  35. package/src/options.ts +25 -25
  36. package/src/selectExtensions.ts +68 -68
  37. package/src/slotBinding.ts +84 -84
  38. package/src/subscribable.ts +266 -266
  39. package/src/tasks.ts +79 -79
  40. package/src/templateEngine.ts +108 -108
  41. package/src/templateRendering.ts +399 -399
  42. package/src/templateRewriting.ts +94 -94
  43. package/src/templateSources.ts +134 -134
  44. package/src/utils.ts +123 -123
  45. package/src/utilsDom.ts +87 -87
  46. package/src/virtualElements.ts +153 -153
  47. package/src/wireParams.ts +49 -49
@@ -1,130 +1,130 @@
1
- import { addNodePreprocessor } from './bindingProvider.js';
2
- import type { NodePreprocessFn } from './bindingProvider.js';
3
- import { options } from './options.js';
4
-
5
- // ---- Interpolation Parser ----
6
-
7
- // Parses text containing {{ }} markup into alternating plain-text and expression segments.
8
- // Uses a recursive inside-out approach: finds the outermost {{ and }} pair, then
9
- // recursively parses the inner text to handle nested {{ }} correctly.
10
- export function parseInterpolationMarkup(
11
- textToParse: string,
12
- outerTextCallback: (text: string) => void,
13
- expressionCallback: (expression: string) => void,
14
- ): void {
15
- function innerParse(text: string): void {
16
- const innerMatch = text.match(/^([\s\S]*)}}([\s\S]*?)\{\{([\s\S]*)$/);
17
- if (innerMatch) {
18
- innerParse(innerMatch[1]);
19
- outerTextCallback(innerMatch[2]);
20
- expressionCallback(innerMatch[3]);
21
- } else {
22
- expressionCallback(text);
23
- }
24
- }
25
-
26
- const outerMatch = textToParse.match(/^([\s\S]*?)\{\{([\s\S]*)}}([\s\S]*)$/);
27
- if (outerMatch) {
28
- outerTextCallback(outerMatch[1]);
29
- innerParse(outerMatch[2]);
30
- outerTextCallback(outerMatch[3]);
31
- }
32
- }
33
-
34
- function trim(s: string | null | undefined): string {
35
- return s == null ? '' : s.trim();
36
- }
37
-
38
- // ---- Expression → Comment Nodes ----
39
-
40
- // Converts a parsed expression into comment-based binding nodes.
41
- // {{ expr }} → <!-- tap text: expr --><!-- /tap -->
42
- // {{{ expr }}} → <!-- tap html: expr --><!-- /tap -->
43
- // {{# if cond }} → <!-- tap if: cond -->
44
- // {{# if cond /}} → <!-- tap if: cond --><!-- /tap --> (self-closing)
45
- // {{/ }} → <!-- /tap -->
46
- export function wrapExpression(expressionText: string, node?: Node): Node[] {
47
- const ownerDocument = node?.ownerDocument ?? document;
48
- let closeComment = true;
49
- let binding: string | undefined;
50
- expressionText = trim(expressionText);
51
-
52
- const firstChar = expressionText[0];
53
- const lastChar = expressionText[expressionText.length - 1];
54
- const result: Node[] = [];
55
-
56
- if (firstChar === '#') {
57
- if (lastChar === '/') {
58
- binding = trim(expressionText.slice(1, -1));
59
- } else {
60
- binding = trim(expressionText.slice(1));
61
- closeComment = false;
62
- }
63
- const matches = binding.match(/^([^,"'{}()\/:[\]\s]+)\s+([^\s:].*)/);
64
- if (matches) {
65
- binding = matches[1] + ':' + matches[2];
66
- }
67
- } else if (firstChar === '/') {
68
- // Closing tag — just emit the end comment
69
- } else if (firstChar === '{' && lastChar === '}') {
70
- binding = 'html:' + trim(expressionText.slice(1, -1));
71
- } else {
72
- binding = 'text:' + trim(expressionText);
73
- }
74
-
75
- if (binding) {
76
- result.push(ownerDocument.createComment(' tap ' + binding + ' '));
77
- }
78
- if (closeComment) {
79
- result.push(ownerDocument.createComment(' /tap '));
80
- }
81
-
82
- return result;
83
- }
84
-
85
- // ---- Node Preprocessor ----
86
-
87
- export const interpolationMarkupPreprocessor: NodePreprocessFn = function (node: Node): Node[] | void {
88
- if (
89
- node.nodeType !== 3 ||
90
- !node.nodeValue ||
91
- node.nodeValue.indexOf('{{') === -1 ||
92
- (node.parentNode && (node.parentNode as Element).nodeName === 'TEXTAREA')
93
- ) {
94
- return;
95
- }
96
-
97
- const nodes: Node[] = [];
98
-
99
- function addTextNode(text: string): void {
100
- if (text) {
101
- nodes.push(node.ownerDocument!.createTextNode(text));
102
- }
103
- }
104
-
105
- function addExpressionNodes(expressionText: string): void {
106
- if (expressionText) {
107
- nodes.push(...wrapExpression(expressionText, node));
108
- }
109
- }
110
-
111
- parseInterpolationMarkup(node.nodeValue, addTextNode, addExpressionNodes);
112
-
113
- if (nodes.length) {
114
- if (node.parentNode) {
115
- const parent = node.parentNode;
116
- for (const newNode of nodes) {
117
- parent.insertBefore(newNode, node);
118
- }
119
- parent.removeChild(node);
120
- }
121
- return nodes;
122
- }
123
- };
124
-
125
- // ---- Enable ----
126
-
127
- export function enableInterpolationMarkup(): void {
128
- options.interpolation = true;
129
- addNodePreprocessor(interpolationMarkupPreprocessor);
130
- }
1
+ import { addNodePreprocessor } from './bindingProvider.js';
2
+ import type { NodePreprocessFn } from './bindingProvider.js';
3
+ import { options } from './options.js';
4
+
5
+ // ---- Interpolation Parser ----
6
+
7
+ // Parses text containing {{ }} markup into alternating plain-text and expression segments.
8
+ // Uses a recursive inside-out approach: finds the outermost {{ and }} pair, then
9
+ // recursively parses the inner text to handle nested {{ }} correctly.
10
+ export function parseInterpolationMarkup(
11
+ textToParse: string,
12
+ outerTextCallback: (text: string) => void,
13
+ expressionCallback: (expression: string) => void,
14
+ ): void {
15
+ function innerParse(text: string): void {
16
+ const innerMatch = text.match(/^([\s\S]*)}}([\s\S]*?)\{\{([\s\S]*)$/);
17
+ if (innerMatch) {
18
+ innerParse(innerMatch[1]);
19
+ outerTextCallback(innerMatch[2]);
20
+ expressionCallback(innerMatch[3]);
21
+ } else {
22
+ expressionCallback(text);
23
+ }
24
+ }
25
+
26
+ const outerMatch = textToParse.match(/^([\s\S]*?)\{\{([\s\S]*)}}([\s\S]*)$/);
27
+ if (outerMatch) {
28
+ outerTextCallback(outerMatch[1]);
29
+ innerParse(outerMatch[2]);
30
+ outerTextCallback(outerMatch[3]);
31
+ }
32
+ }
33
+
34
+ function trim(s: string | null | undefined): string {
35
+ return s == null ? '' : s.trim();
36
+ }
37
+
38
+ // ---- Expression → Comment Nodes ----
39
+
40
+ // Converts a parsed expression into comment-based binding nodes.
41
+ // {{ expr }} → <!-- tap text: expr --><!-- /tap -->
42
+ // {{{ expr }}} → <!-- tap html: expr --><!-- /tap -->
43
+ // {{# if cond }} → <!-- tap if: cond -->
44
+ // {{# if cond /}} → <!-- tap if: cond --><!-- /tap --> (self-closing)
45
+ // {{/ }} → <!-- /tap -->
46
+ export function wrapExpression(expressionText: string, node?: Node): Node[] {
47
+ const ownerDocument = node?.ownerDocument ?? document;
48
+ let closeComment = true;
49
+ let binding: string | undefined;
50
+ expressionText = trim(expressionText);
51
+
52
+ const firstChar = expressionText[0];
53
+ const lastChar = expressionText[expressionText.length - 1];
54
+ const result: Node[] = [];
55
+
56
+ if (firstChar === '#') {
57
+ if (lastChar === '/') {
58
+ binding = trim(expressionText.slice(1, -1));
59
+ } else {
60
+ binding = trim(expressionText.slice(1));
61
+ closeComment = false;
62
+ }
63
+ const matches = binding.match(/^([^,"'{}()\/:[\]\s]+)\s+([^\s:].*)/);
64
+ if (matches) {
65
+ binding = matches[1] + ':' + matches[2];
66
+ }
67
+ } else if (firstChar === '/') {
68
+ // Closing tag — just emit the end comment
69
+ } else if (firstChar === '{' && lastChar === '}') {
70
+ binding = 'html:' + trim(expressionText.slice(1, -1));
71
+ } else {
72
+ binding = 'text:' + trim(expressionText);
73
+ }
74
+
75
+ if (binding) {
76
+ result.push(ownerDocument.createComment(' tap ' + binding + ' '));
77
+ }
78
+ if (closeComment) {
79
+ result.push(ownerDocument.createComment(' /tap '));
80
+ }
81
+
82
+ return result;
83
+ }
84
+
85
+ // ---- Node Preprocessor ----
86
+
87
+ export const interpolationMarkupPreprocessor: NodePreprocessFn = function (node: Node): Node[] | void {
88
+ if (
89
+ node.nodeType !== 3 ||
90
+ !node.nodeValue ||
91
+ node.nodeValue.indexOf('{{') === -1 ||
92
+ (node.parentNode && (node.parentNode as Element).nodeName === 'TEXTAREA')
93
+ ) {
94
+ return;
95
+ }
96
+
97
+ const nodes: Node[] = [];
98
+
99
+ function addTextNode(text: string): void {
100
+ if (text) {
101
+ nodes.push(node.ownerDocument!.createTextNode(text));
102
+ }
103
+ }
104
+
105
+ function addExpressionNodes(expressionText: string): void {
106
+ if (expressionText) {
107
+ nodes.push(...wrapExpression(expressionText, node));
108
+ }
109
+ }
110
+
111
+ parseInterpolationMarkup(node.nodeValue, addTextNode, addExpressionNodes);
112
+
113
+ if (nodes.length) {
114
+ if (node.parentNode) {
115
+ const parent = node.parentNode;
116
+ for (const newNode of nodes) {
117
+ parent.insertBefore(newNode, node);
118
+ }
119
+ parent.removeChild(node);
120
+ }
121
+ return nodes;
122
+ }
123
+ };
124
+
125
+ // ---- Enable ----
126
+
127
+ export function enableInterpolationMarkup(): void {
128
+ options.interpolation = true;
129
+ addNodePreprocessor(interpolationMarkupPreprocessor);
130
+ }
@@ -1,71 +1,71 @@
1
- type MemoCallback = (...args: unknown[]) => void;
2
-
3
- const memos = new Map<string, MemoCallback>();
4
-
5
- function randomMax8HexChars(): string {
6
- return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1);
7
- }
8
-
9
- function generateRandomId(): string {
10
- return randomMax8HexChars() + randomMax8HexChars();
11
- }
12
-
13
- function findMemoNodes(
14
- rootNode: Node | null,
15
- appendToArray: { domNode: Comment; memoId: string }[],
16
- ): void {
17
- if (!rootNode) return;
18
- if (rootNode.nodeType === 8) {
19
- const memoId = parseMemoText(rootNode.nodeValue || '');
20
- if (memoId != null)
21
- appendToArray.push({ domNode: rootNode as Comment, memoId });
22
- } else if (rootNode.nodeType === 1) {
23
- const childNodes = rootNode.childNodes;
24
- for (let i = 0; i < childNodes.length; i++)
25
- findMemoNodes(childNodes[i], appendToArray);
26
- }
27
- }
28
-
29
- export function memoize(callback: MemoCallback): string {
30
- if (typeof callback !== 'function')
31
- throw new Error('You can only pass a function to memoize()');
32
- const memoId = generateRandomId();
33
- memos.set(memoId, callback);
34
- return '<!--[tap_memo:' + memoId + ']-->';
35
- }
36
-
37
- export function unmemoize(memoId: string, callbackParams?: unknown[]): boolean {
38
- const callback = memos.get(memoId);
39
- if (callback === undefined)
40
- throw new Error("Couldn't find any memo with ID " + memoId + ". Perhaps it's already been unmemoized.");
41
- try {
42
- callback.apply(null, callbackParams || []);
43
- return true;
44
- } finally {
45
- memos.delete(memoId);
46
- }
47
- }
48
-
49
- export function unmemoizeDomNodeAndDescendants(
50
- domNode: Node,
51
- extraCallbackParamsArray?: unknown[],
52
- ): void {
53
- const found: { domNode: Comment; memoId: string }[] = [];
54
- findMemoNodes(domNode, found);
55
- for (const { domNode: node, memoId } of found) {
56
- const combinedParams: unknown[] = [node];
57
- if (extraCallbackParamsArray)
58
- combinedParams.push(...extraCallbackParamsArray);
59
- unmemoize(memoId, combinedParams);
60
- node.nodeValue = '';
61
- if (node.parentNode)
62
- node.parentNode.removeChild(node);
63
- }
64
- }
65
-
66
- const memoTextRegex = /^\[tap_memo:(.*?)\]$/;
67
-
68
- export function parseMemoText(memoText: string): string | null {
69
- const match = memoText.match(memoTextRegex);
70
- return match ? match[1] : null;
71
- }
1
+ type MemoCallback = (...args: unknown[]) => void;
2
+
3
+ const memos = new Map<string, MemoCallback>();
4
+
5
+ function randomMax8HexChars(): string {
6
+ return (((1 + Math.random()) * 0x100000000) | 0).toString(16).substring(1);
7
+ }
8
+
9
+ function generateRandomId(): string {
10
+ return randomMax8HexChars() + randomMax8HexChars();
11
+ }
12
+
13
+ function findMemoNodes(
14
+ rootNode: Node | null,
15
+ appendToArray: { domNode: Comment; memoId: string }[],
16
+ ): void {
17
+ if (!rootNode) return;
18
+ if (rootNode.nodeType === 8) {
19
+ const memoId = parseMemoText(rootNode.nodeValue || '');
20
+ if (memoId != null)
21
+ appendToArray.push({ domNode: rootNode as Comment, memoId });
22
+ } else if (rootNode.nodeType === 1) {
23
+ const childNodes = rootNode.childNodes;
24
+ for (let i = 0; i < childNodes.length; i++)
25
+ findMemoNodes(childNodes[i], appendToArray);
26
+ }
27
+ }
28
+
29
+ export function memoize(callback: MemoCallback): string {
30
+ if (typeof callback !== 'function')
31
+ throw new Error('You can only pass a function to memoize()');
32
+ const memoId = generateRandomId();
33
+ memos.set(memoId, callback);
34
+ return '<!--[tap_memo:' + memoId + ']-->';
35
+ }
36
+
37
+ export function unmemoize(memoId: string, callbackParams?: unknown[]): boolean {
38
+ const callback = memos.get(memoId);
39
+ if (callback === undefined)
40
+ throw new Error("Couldn't find any memo with ID " + memoId + ". Perhaps it's already been unmemoized.");
41
+ try {
42
+ callback.apply(null, callbackParams || []);
43
+ return true;
44
+ } finally {
45
+ memos.delete(memoId);
46
+ }
47
+ }
48
+
49
+ export function unmemoizeDomNodeAndDescendants(
50
+ domNode: Node,
51
+ extraCallbackParamsArray?: unknown[],
52
+ ): void {
53
+ const found: { domNode: Comment; memoId: string }[] = [];
54
+ findMemoNodes(domNode, found);
55
+ for (const { domNode: node, memoId } of found) {
56
+ const combinedParams: unknown[] = [node];
57
+ if (extraCallbackParamsArray)
58
+ combinedParams.push(...extraCallbackParamsArray);
59
+ unmemoize(memoId, combinedParams);
60
+ node.nodeValue = '';
61
+ if (node.parentNode)
62
+ node.parentNode.removeChild(node);
63
+ }
64
+ }
65
+
66
+ const memoTextRegex = /^\[tap_memo:(.*?)\]$/;
67
+
68
+ export function parseMemoText(memoText: string): string | null {
69
+ const match = memoText.match(memoTextRegex);
70
+ return match ? match[1] : null;
71
+ }
@@ -1,132 +1,132 @@
1
- import {
2
- bindingHandlers,
3
- getBindingHandler,
4
- addBindingHandlerCreator,
5
- addBindingPreprocessor,
6
- } from './bindingProvider.js';
7
- import type { BindingHandler, PreprocessFn } from './bindingProvider.js';
8
- import { allowedVirtualElementBindings } from './virtualElements.js';
9
- import { parseObjectLiteral } from './expressionRewriting.js';
10
- import type { BindingContext } from './bindingContext.js';
11
- import { options } from './options.js';
12
- import type { AllBindingsAccessor } from './expressionRewriting.js';
13
-
14
- const NAMESPACE_DIVIDER = '.';
15
- const NAMESPACED_BINDING_REGEX = /^([^.]+)\.(.+)$/;
16
-
17
- // Translates `namespace.name: value` into `namespace: {name: value}`.
18
- // This works because attr/css/style/event all accept an object map.
19
- export function defaultGetNamespacedHandler(
20
- this: BindingHandler,
21
- name: string,
22
- namespace: string,
23
- namespacedName: string,
24
- ): BindingHandler {
25
- const handler: BindingHandler = Object.assign({}, this);
26
-
27
- function makeSubValueAccessor(valueAccessor: () => unknown): () => Record<string, unknown> {
28
- return () => {
29
- const result: Record<string, unknown> = {};
30
- result[name] = valueAccessor();
31
- return result;
32
- };
33
- }
34
-
35
- if (handler.init) {
36
- handler.init = function (
37
- element: Node,
38
- valueAccessor: () => unknown,
39
- allBindings: AllBindingsAccessor,
40
- viewModel: unknown,
41
- bindingContext: BindingContext,
42
- ) {
43
- return bindingHandlers[namespace].init!.call(
44
- this, element, makeSubValueAccessor(valueAccessor), allBindings, viewModel, bindingContext,
45
- );
46
- };
47
- }
48
-
49
- if (handler.update) {
50
- handler.update = function (
51
- element: Node,
52
- valueAccessor: () => unknown,
53
- allBindings: AllBindingsAccessor,
54
- viewModel: unknown,
55
- bindingContext: BindingContext,
56
- ) {
57
- bindingHandlers[namespace].update!.call(
58
- this, element, makeSubValueAccessor(valueAccessor), allBindings, viewModel, bindingContext,
59
- );
60
- };
61
- }
62
-
63
- handler.preprocess = undefined;
64
-
65
- if (allowedVirtualElementBindings[namespace]) {
66
- allowedVirtualElementBindings[namespacedName] = true;
67
- }
68
-
69
- return handler;
70
- }
71
-
72
- // Register the dynamic handler creator for the `namespace.name` pattern.
73
- // When `getBindingHandler('attr.href')` is called and no explicit handler exists,
74
- // this splits on `.`, finds the `attr` handler, and creates a derived handler
75
- // that wraps the single value into the object map format the namespace handler expects.
76
- export function enableNamespacedBindings(): void {
77
- options.namespacedBindings = true;
78
- addBindingHandlerCreator(NAMESPACED_BINDING_REGEX, (match, bindingKey) => {
79
- const namespace = match[1];
80
- const namespaceHandler = bindingHandlers[namespace];
81
- if (namespaceHandler) {
82
- const name = match[2];
83
- const handlerFn = namespaceHandler.getNamespacedHandler || defaultGetNamespacedHandler;
84
- const handler = handlerFn.call(namespaceHandler, name, namespace, bindingKey);
85
- bindingHandlers[bindingKey] = handler;
86
- return handler;
87
- }
88
- return undefined;
89
- });
90
- }
91
-
92
- // Preprocessor that auto-expands `binding: {key: val, key2: val2}` into
93
- // individual `binding.key: val` and `binding.key2: val2` bindings.
94
- export const autoNamespacedPreprocessor: PreprocessFn = function (
95
- value: string,
96
- binding: string,
97
- addBinding: (key: string, val: string) => void,
98
- ): string | void {
99
- if (value.charAt(0) !== '{') return value;
100
-
101
- const subBindings = parseObjectLiteral(value);
102
- for (const kv of subBindings) {
103
- addBinding(binding + NAMESPACE_DIVIDER + (kv.key || kv.unknown || ''), kv.value || '');
104
- }
105
- };
106
-
107
- // Enable the auto-expand preprocessor for a specific binding, so
108
- // `attr: {href: url, title: tip}` auto-expands to `attr.href: url, attr.title: tip`.
109
- export function enableAutoNamespacedSyntax(bindingKeyOrHandler: string | BindingHandler): void {
110
- addBindingPreprocessor(bindingKeyOrHandler, autoNamespacedPreprocessor);
111
- }
112
-
113
- // Add a preprocessor to all dynamically-generated namespaced handlers under a namespace.
114
- // For example, adding a filter preprocessor to 'attr' would make it apply to
115
- // every `attr.x` binding that gets created.
116
- export function addDefaultNamespacedBindingPreprocessor(
117
- namespace: string,
118
- preprocessFn: PreprocessFn,
119
- ): void {
120
- const handler = getBindingHandler(namespace);
121
- if (handler) {
122
- const previousHandlerFn = handler.getNamespacedHandler || defaultGetNamespacedHandler;
123
- handler.getNamespacedHandler = function (
124
- name: string,
125
- ns: string,
126
- namespacedName: string,
127
- ): BindingHandler {
128
- const created = previousHandlerFn.call(this, name, ns, namespacedName);
129
- return addBindingPreprocessor(created, preprocessFn);
130
- };
131
- }
132
- }
1
+ import {
2
+ bindingHandlers,
3
+ getBindingHandler,
4
+ addBindingHandlerCreator,
5
+ addBindingPreprocessor,
6
+ } from './bindingProvider.js';
7
+ import type { BindingHandler, PreprocessFn } from './bindingProvider.js';
8
+ import { allowedVirtualElementBindings } from './virtualElements.js';
9
+ import { parseObjectLiteral } from './expressionRewriting.js';
10
+ import type { BindingContext } from './bindingContext.js';
11
+ import { options } from './options.js';
12
+ import type { AllBindingsAccessor } from './expressionRewriting.js';
13
+
14
+ const NAMESPACE_DIVIDER = '.';
15
+ const NAMESPACED_BINDING_REGEX = /^([^.]+)\.(.+)$/;
16
+
17
+ // Translates `namespace.name: value` into `namespace: {name: value}`.
18
+ // This works because attr/css/style/event all accept an object map.
19
+ export function defaultGetNamespacedHandler(
20
+ this: BindingHandler,
21
+ name: string,
22
+ namespace: string,
23
+ namespacedName: string,
24
+ ): BindingHandler {
25
+ const handler: BindingHandler = Object.assign({}, this);
26
+
27
+ function makeSubValueAccessor(valueAccessor: () => unknown): () => Record<string, unknown> {
28
+ return () => {
29
+ const result: Record<string, unknown> = {};
30
+ result[name] = valueAccessor();
31
+ return result;
32
+ };
33
+ }
34
+
35
+ if (handler.init) {
36
+ handler.init = function (
37
+ element: Node,
38
+ valueAccessor: () => unknown,
39
+ allBindings: AllBindingsAccessor,
40
+ viewModel: unknown,
41
+ bindingContext: BindingContext,
42
+ ) {
43
+ return bindingHandlers[namespace].init!.call(
44
+ this, element, makeSubValueAccessor(valueAccessor), allBindings, viewModel, bindingContext,
45
+ );
46
+ };
47
+ }
48
+
49
+ if (handler.update) {
50
+ handler.update = function (
51
+ element: Node,
52
+ valueAccessor: () => unknown,
53
+ allBindings: AllBindingsAccessor,
54
+ viewModel: unknown,
55
+ bindingContext: BindingContext,
56
+ ) {
57
+ bindingHandlers[namespace].update!.call(
58
+ this, element, makeSubValueAccessor(valueAccessor), allBindings, viewModel, bindingContext,
59
+ );
60
+ };
61
+ }
62
+
63
+ handler.preprocess = undefined;
64
+
65
+ if (allowedVirtualElementBindings[namespace]) {
66
+ allowedVirtualElementBindings[namespacedName] = true;
67
+ }
68
+
69
+ return handler;
70
+ }
71
+
72
+ // Register the dynamic handler creator for the `namespace.name` pattern.
73
+ // When `getBindingHandler('attr.href')` is called and no explicit handler exists,
74
+ // this splits on `.`, finds the `attr` handler, and creates a derived handler
75
+ // that wraps the single value into the object map format the namespace handler expects.
76
+ export function enableNamespacedBindings(): void {
77
+ options.namespacedBindings = true;
78
+ addBindingHandlerCreator(NAMESPACED_BINDING_REGEX, (match, bindingKey) => {
79
+ const namespace = match[1];
80
+ const namespaceHandler = bindingHandlers[namespace];
81
+ if (namespaceHandler) {
82
+ const name = match[2];
83
+ const handlerFn = namespaceHandler.getNamespacedHandler || defaultGetNamespacedHandler;
84
+ const handler = handlerFn.call(namespaceHandler, name, namespace, bindingKey);
85
+ bindingHandlers[bindingKey] = handler;
86
+ return handler;
87
+ }
88
+ return undefined;
89
+ });
90
+ }
91
+
92
+ // Preprocessor that auto-expands `binding: {key: val, key2: val2}` into
93
+ // individual `binding.key: val` and `binding.key2: val2` bindings.
94
+ export const autoNamespacedPreprocessor: PreprocessFn = function (
95
+ value: string,
96
+ binding: string,
97
+ addBinding: (key: string, val: string) => void,
98
+ ): string | void {
99
+ if (value.charAt(0) !== '{') return value;
100
+
101
+ const subBindings = parseObjectLiteral(value);
102
+ for (const kv of subBindings) {
103
+ addBinding(binding + NAMESPACE_DIVIDER + (kv.key || kv.unknown || ''), kv.value || '');
104
+ }
105
+ };
106
+
107
+ // Enable the auto-expand preprocessor for a specific binding, so
108
+ // `attr: {href: url, title: tip}` auto-expands to `attr.href: url, attr.title: tip`.
109
+ export function enableAutoNamespacedSyntax(bindingKeyOrHandler: string | BindingHandler): void {
110
+ addBindingPreprocessor(bindingKeyOrHandler, autoNamespacedPreprocessor);
111
+ }
112
+
113
+ // Add a preprocessor to all dynamically-generated namespaced handlers under a namespace.
114
+ // For example, adding a filter preprocessor to 'attr' would make it apply to
115
+ // every `attr.x` binding that gets created.
116
+ export function addDefaultNamespacedBindingPreprocessor(
117
+ namespace: string,
118
+ preprocessFn: PreprocessFn,
119
+ ): void {
120
+ const handler = getBindingHandler(namespace);
121
+ if (handler) {
122
+ const previousHandlerFn = handler.getNamespacedHandler || defaultGetNamespacedHandler;
123
+ handler.getNamespacedHandler = function (
124
+ name: string,
125
+ ns: string,
126
+ namespacedName: string,
127
+ ): BindingHandler {
128
+ const created = previousHandlerFn.call(this, name, ns, namespacedName);
129
+ return addBindingPreprocessor(created, preprocessFn);
130
+ };
131
+ }
132
+ }