Skip to content

Commit a76f1d8

Browse files
committed
Tokenizer + :class directive
1 parent c74cf0c commit a76f1d8

File tree

10 files changed

+685
-161
lines changed

10 files changed

+685
-161
lines changed

index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"signalizejs/directives": "/packages/signalizejs/src/modules/directives.js",
1919
"signalizejs/directives/if": "/packages/signalizejs/src/modules/directives/if.js",
2020
"signalizejs/directives/for": "/packages/signalizejs/src/modules/directives/for.js",
21+
"signalizejs/directives/class": "/packages/signalizejs/src/modules/directives/class.js",
2122
"signalizejs/dialog": "/packages/signalizejs/src/modules/dialog.js",
2223
"signalizejs/dom/ready": "/packages/signalizejs/src/modules/dom/ready.js",
2324
"signalizejs/dom/traverser": "/packages/signalizejs/src/modules/dom/traverser.js",
@@ -33,6 +34,8 @@
3334
"signalizejs/sizes": "/packages/signalizejs/src/modules/sizes.js",
3435
"signalizejs/snippets": "/packages/signalizejs/src/modules/snippets.js",
3536
"signalizejs/spa": "/packages/signalizejs/src/modules/spa.js",
37+
"signalizejs/strings/cases": "/packages/signalizejs/src/modules/strings/cases.js",
38+
"signalizejs/strings/tokenizer": "/packages/signalizejs/src/modules/strings/tokenizer.js",
3639
"signalizejs/task": "/packages/signalizejs/src/modules/task.js",
3740
"signalizejs/viewport": "/packages/signalizejs/src/modules/viewport.js"
3841
}

packages/signalizejs/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
"./directives/if": {
4747
"import": "./src/modules/directives/if.js"
4848
},
49+
"./directives/class": {
50+
"import": "./src/modules/directives/class.js"
51+
},
4952
"./dom/ready": {
5053
"import": "./src/modules/dom/ready.js"
5154
},
@@ -97,6 +100,9 @@
97100
"./strings/cases": {
98101
"import": "./src/modules/strings/cases.js"
99102
},
103+
"./strings/tokenizer": {
104+
"import": "./src/modules/strings/tokenizer.js"
105+
},
100106
"./task": {
101107
"import": "./src/modules/task.js"
102108
},

packages/signalizejs/src/Signalize.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ export class Signalize {
77
'ajax',
88
'bind',
99
'component',
10-
'dialog', 'dom/ready', 'dom/traverser', 'directives', 'directives/for', 'directives/if',
10+
'dialog', 'dom/ready', 'dom/traverser', 'directives', 'directives/for', 'directives/if', 'directives/class',
1111
'evaluator', 'event',
1212
'hyperscript',
1313
'intersection-observer', 'visibility',
1414
'mutation-observer',
1515
'offset',
16-
'scope', 'signal', 'sizes', 'snippets', 'spa', 'strings/cases',
16+
'scope', 'signal', 'sizes', 'snippets', 'spa', 'strings/cases', 'strings/tokenizer',
1717
'task',
1818
'viewport',
1919
];

packages/signalizejs/src/modules/directives.js

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,19 @@ export default async ($, config) => {
5858

5959
const matcherReturn = matcher({ element, attribute });
6060

61+
/** @type {RegExpExecArray|null} */
62+
let matches = null;
63+
6164
if (matcherReturn === undefined) {
6265
continue;
63-
}
64-
65-
const matches = new RegExp(`^${matcherReturn.source}$`).exec(attribute.name);
66-
67-
if (matches === null) {
66+
} else if (typeof matcherReturn === 'boolean' && !matcherReturn) {
6867
continue;
68+
69+
} else if (matcherReturn instanceof RegExp) {
70+
matches = new RegExp(`^${matcherReturn.source}$`).exec(attribute.name);
71+
if (matches === null) {
72+
continue;
73+
}
6974
}
7075

7176
elementScope = scope(element, (node) => {
@@ -225,6 +230,10 @@ export default async ($, config) => {
225230
return;
226231
}
227232

233+
if (attribute.name === ':class') {
234+
return;
235+
}
236+
228237
return new RegExp(`(?::|${attributePrefix}bind${attributeSeparator})([\\S-]+)|(\\{([^{}]+)\\})`);
229238
},
230239
callback: async (data) => {
@@ -336,6 +345,14 @@ export default async ($, config) => {
336345
}
337346
});
338347

348+
directive('class', {
349+
matcher: ({ attribute }) => attribute.name === ':class',
350+
callback: async (data) => {
351+
const { classDirective } = await resolve('directives/class');
352+
await classDirective(data);
353+
}
354+
});
355+
339356
directive('if', {
340357
matcher: ({ element }) => {
341358
if (element.tagName.toLowerCase() !== 'template') {
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/** @type {import('../../../types/Signalize').Module} */
2+
export default async ({ resolve }) => {
3+
const resolved = await resolve('directives', 'evaluator', 'scope', 'signal', 'strings/tokenizer');
4+
const { evaluate, Signal, createTokenizer } = resolved;
5+
6+
const classDirective = async ({ matches, attribute, scope }) => {
7+
const { $el } = scope;
8+
const isShorthand = attribute.name.startsWith('{');
9+
const attributeValue = isShorthand ? matches[3] : attribute.value;
10+
let inString = false;
11+
let openingQuote = '';
12+
let requiredClosingBrackets = 0;
13+
let openingBracket = '';
14+
let chunkTokenQueue = ''
15+
const tokenizer = createTokenizer(attributeValue);
16+
const classRawParts = [];
17+
18+
const openCloseBracketsPair = {
19+
'(': ')',
20+
'[': ']',
21+
'{': '}'
22+
};
23+
while (tokenizer.canConsumeTokens()) {
24+
const token = tokenizer.consumeTokens();
25+
26+
if (tokenizer.isToken('quote', token)) {
27+
if (inString && openingQuote === token) {
28+
openingQuote = '';
29+
inString = false;
30+
} else {
31+
openingQuote = token;
32+
inString = true;
33+
}
34+
}
35+
36+
if (!inString) {
37+
if (tokenizer.isToken('openingBracket', token)) {
38+
if (requiredClosingBrackets === 0) {
39+
openingBracket = token;
40+
}
41+
42+
requiredClosingBrackets++;
43+
} else if (tokenizer.isToken('closingBracket', token) && token === openCloseBracketsPair[openingBracket]) {
44+
requiredClosingBrackets--;
45+
if (requiredClosingBrackets === 0) {
46+
openingBracket = '';
47+
}
48+
}
49+
}
50+
51+
if (!inString && requiredClosingBrackets === 0) {
52+
if (token === ',') {
53+
classRawParts.push(chunkTokenQueue);
54+
chunkTokenQueue = '';
55+
} else {
56+
chunkTokenQueue += token;
57+
}
58+
} else {
59+
chunkTokenQueue += token;
60+
}
61+
}
62+
63+
if (chunkTokenQueue.trim() !== '') {
64+
classRawParts.push(chunkTokenQueue);
65+
}
66+
67+
/** @type {Record<string, string>} */
68+
const evaluatedClassCache = {};
69+
70+
/** @type {string[]} */
71+
let settedClasses = [];
72+
73+
const setClassAttribute = () => {
74+
const newClasses = [];
75+
76+
for (const newClass of Object.values(evaluatedClassCache)) {
77+
if (newClass.trim() !== '') {
78+
newClasses.push(newClass);
79+
$el.classList.add(newClass);
80+
}
81+
}
82+
83+
for (const className of settedClasses) {
84+
if (!newClasses.includes(className)) {
85+
$el.classList.remove(className);
86+
}
87+
}
88+
89+
settedClasses = newClasses
90+
}
91+
92+
/**
93+
* @param {string} classPart
94+
* @param {boolean} setAttribute
95+
* @param {boolean} trackSignals
96+
*/
97+
const evaluateClassPart = (classPart, setAttribute, trackSignals = false) => {
98+
const { result, detectedSignals } = evaluate(
99+
classPart,
100+
{
101+
$el,
102+
...scope
103+
},
104+
trackSignals
105+
);
106+
107+
evaluatedClassCache[classPart] = result instanceof Signal ? result() : result;
108+
109+
if (setAttribute === true) {
110+
setClassAttribute();
111+
}
112+
113+
return detectedSignals;
114+
};
115+
116+
117+
for (const classRawPart of classRawParts) {
118+
const detectedSignals = evaluateClassPart(classRawPart, false, true);
119+
120+
for (const signal of detectedSignals) {
121+
signal.watch(() => evaluateClassPart(classRawPart, true));
122+
}
123+
}
124+
125+
setClassAttribute();
126+
}
127+
128+
129+
130+
return { classDirective }
131+
};

0 commit comments

Comments
 (0)