Skip to content

Commit 2c2d0ef

Browse files
committed
Merge branch 'hanoii:master' into master
Close GH-37 Add support for attribute names according html5 specs.
2 parents 556adbc + 009a0e0 commit 2c2d0ef

File tree

5 files changed

+236
-7
lines changed

5 files changed

+236
-7
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
- Package has been renamed `@zackad/prettier-plugin-twig-melody` -> `@zackad/prettier-plugin-twig`
99
- The parser has been renamed from `melody` into `twig`
1010

11+
### Features
12+
- Add support attribute names according to html5 specs
13+
1114
### Internals
1215
- Remove npm script to publish
1316
- Integrate devenv into nix flakes

src/melody/melody-parser/CharStream.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export class CharStream {
5151

5252
lac(offset) {
5353
const index = this.index + offset;
54-
return index < this.length ? this.input.charCodeAt(index) : EOF;
54+
return index < this.length ? this.input.codePointAt(index) : EOF;
5555
}
5656

5757
next() {

src/melody/melody-parser/Lexer.js

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,7 @@ export default class Lexer {
497497
(c === 95 ||
498498
isAlpha(c) ||
499499
isDigit(c) ||
500-
(inElement && (c === 45 || c === 58)))
500+
(inElement && isValidAttributeName(c)))
501501
) {
502502
input.next();
503503
}
@@ -561,17 +561,15 @@ export default class Lexer {
561561
matchAttributeValue(pos) {
562562
const input = this.input;
563563
const start = this.state === State.STRING_SINGLE ? "'" : '"';
564-
let c;
565-
if (input.la(0) === "{") {
564+
let c = input.la(0);
565+
const c2 = input.la(1);
566+
if (c === "{" && (c2 === "{" || c2 === "#" || c2 === "%")) {
566567
return this.matchExpressionToken(pos);
567568
}
568569
while ((c = input.la(0)) !== start && c !== EOF) {
569570
if (c === "\\" && input.la(1) === start) {
570571
input.next();
571572
input.next();
572-
} else if (c === "{") {
573-
// interpolation start
574-
break;
575573
} else if (c === start) {
576574
break;
577575
} else {
@@ -663,3 +661,48 @@ function isAlpha(c) {
663661
function isDigit(c) {
664662
return c >= 48 && c <= 57;
665663
}
664+
665+
// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
666+
function isValidAttributeName(c) {
667+
const is_c0_control = c >= 0 && c <= 31;
668+
const is_control = is_c0_control || (c >= 127 && c <= 159);
669+
const is_invalid =
670+
c === 32 || c === 34 || c === 39 || c === 62 || c === 47 || c === 61;
671+
const is_noncharacter =
672+
(c >= 64976 && c <= 65007) ||
673+
c === 65534 ||
674+
c === 65535 ||
675+
c === 131070 ||
676+
c === 131071 ||
677+
c === 196606 ||
678+
c === 196607 ||
679+
c === 262142 ||
680+
c === 262143 ||
681+
c === 327678 ||
682+
c === 327679 ||
683+
c === 393214 ||
684+
c === 393215 ||
685+
c === 458750 ||
686+
c === 458751 ||
687+
c === 524286 ||
688+
c === 524287 ||
689+
c === 589822 ||
690+
c === 589823 ||
691+
c === 655358 ||
692+
c === 655359 ||
693+
c === 720894 ||
694+
c === 720895 ||
695+
c === 786430 ||
696+
c === 786431 ||
697+
c === 851966 ||
698+
c === 851967 ||
699+
c === 917502 ||
700+
c === 917503 ||
701+
c === 983038 ||
702+
c === 983039 ||
703+
c === 1048574 ||
704+
c === 1048575 ||
705+
c === 1114106 ||
706+
c === 1114107;
707+
return !is_control && !is_invalid && !is_noncharacter;
708+
}

tests/Element/__snapshots__/jsfmt.spec.js.snap

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,128 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3+
exports[`alpinejs.twig - twig-verify > alpinejs.twig 1`] = `
4+
<div x-data="{ open: false }">
5+
...
6+
</div>
7+
8+
<div x-bind:class="! open ? 'hidden' : ''">
9+
...
10+
</div>
11+
12+
<button x-on:click="open = ! open">
13+
Toggle
14+
</button>
15+
16+
<div>
17+
Copyright ©
18+
19+
<span x-text="new Date().getFullYear()"></span>
20+
</div>
21+
22+
<div x-html="(await axios.get('/some/html/partial')).data">
23+
...
24+
</div>
25+
26+
<div x-data="{ search: '' }">
27+
<input type="text" x-model="search">
28+
29+
Searching for: <span x-text="search"></span>
30+
</div>
31+
32+
<div x-show="open">
33+
...
34+
</div>
35+
36+
<div x-show="open" x-transition>
37+
...
38+
</div>
39+
40+
<template x-for="post in posts">
41+
<h2 x-text="post.title"></h2>
42+
</template>
43+
44+
<template x-if="open">
45+
<div>...</div>
46+
</template>
47+
48+
<div x-init="date = new Date()"></div>
49+
50+
<div x-effect="console.log('Count is '+count)"></div>
51+
52+
<input type="text" x-ref="content">
53+
<button x-on:click="navigator.clipboard.writeText($refs.content.value)">
54+
Copy
55+
</button>
56+
57+
<div x-cloak>
58+
...
59+
</div>
60+
61+
<div x-ignore>
62+
...
63+
</div>
64+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
65+
<div x-data="{ open: false }">
66+
...
67+
</div>
68+
69+
<div x-bind:class="! open ? 'hidden' : ''">
70+
...
71+
</div>
72+
73+
<button x-on:click="open = ! open">Toggle</button>
74+
75+
<div>
76+
Copyright ©
77+
78+
<span x-text="new Date().getFullYear()"></span>
79+
</div>
80+
81+
<div x-html="(await axios.get('/some/html/partial')).data">
82+
...
83+
</div>
84+
85+
<div x-data="{ search: '' }">
86+
<input type="text" x-model="search" />Searching for: <span x-text="search"></span>
87+
</div>
88+
89+
<div x-show="open">
90+
...
91+
</div>
92+
93+
<div x-show="open" x-transition>
94+
...
95+
</div>
96+
97+
<template x-for="post in posts">
98+
<h2 x-text="post.title"></h2>
99+
</template>
100+
101+
<template x-if="open">
102+
<div>
103+
...
104+
</div>
105+
</template>
106+
107+
<div x-init="date = new Date()"></div>
108+
109+
<div x-effect="console.log('Count is '+count)"></div>
110+
111+
<input type="text" x-ref="content" />
112+
<button x-on:click="navigator.clipboard.writeText($refs.content.value)">
113+
Copy
114+
</button>
115+
116+
<div x-cloak>
117+
...
118+
</div>
119+
120+
<div x-ignore>
121+
...
122+
</div>
123+
124+
`;
125+
3126
exports[`attributes.twig - twig-verify > attributes.twig 1`] = `
4127
<a href="#abcd" target="_blank" lang="en" >Link</a>
5128

tests/Element/alpinejs.twig

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<div x-data="{ open: false }">
2+
...
3+
</div>
4+
5+
<div x-bind:class="! open ? 'hidden' : ''">
6+
...
7+
</div>
8+
9+
<button x-on:click="open = ! open">
10+
Toggle
11+
</button>
12+
13+
<div>
14+
Copyright ©
15+
16+
<span x-text="new Date().getFullYear()"></span>
17+
</div>
18+
19+
<div x-html="(await axios.get('/some/html/partial')).data">
20+
...
21+
</div>
22+
23+
<div x-data="{ search: '' }">
24+
<input type="text" x-model="search">
25+
26+
Searching for: <span x-text="search"></span>
27+
</div>
28+
29+
<div x-show="open">
30+
...
31+
</div>
32+
33+
<div x-show="open" x-transition>
34+
...
35+
</div>
36+
37+
<template x-for="post in posts">
38+
<h2 x-text="post.title"></h2>
39+
</template>
40+
41+
<template x-if="open">
42+
<div>...</div>
43+
</template>
44+
45+
<div x-init="date = new Date()"></div>
46+
47+
<div x-effect="console.log('Count is '+count)"></div>
48+
49+
<input type="text" x-ref="content">
50+
<button x-on:click="navigator.clipboard.writeText($refs.content.value)">
51+
Copy
52+
</button>
53+
54+
<div x-cloak>
55+
...
56+
</div>
57+
58+
<div x-ignore>
59+
...
60+
</div>

0 commit comments

Comments
 (0)