@matthesketh/utopia-compiler 0.3.1 → 0.5.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.
- package/dist/index.cjs +339 -31
- package/dist/index.d.cts +32 -2
- package/dist/index.d.ts +32 -2
- package/dist/index.js +338 -31
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
SFCParseError: () => SFCParseError,
|
|
24
|
+
checkA11y: () => checkA11y,
|
|
24
25
|
compile: () => compile,
|
|
25
26
|
compileStyle: () => compileStyle,
|
|
26
27
|
compileTemplate: () => compileTemplate,
|
|
@@ -31,16 +32,19 @@ __export(index_exports, {
|
|
|
31
32
|
module.exports = __toCommonJS(index_exports);
|
|
32
33
|
|
|
33
34
|
// src/parser.ts
|
|
35
|
+
var BLOCK_RE = /<(template|script|style|test)([\s][^>]*)?\s*>/g;
|
|
36
|
+
var ATTR_RE = /([a-zA-Z_][\w-]*)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
34
37
|
function parse(source, filename = "anonymous.utopia") {
|
|
35
38
|
const descriptor = {
|
|
36
39
|
template: null,
|
|
37
40
|
script: null,
|
|
38
41
|
style: null,
|
|
42
|
+
test: null,
|
|
39
43
|
filename
|
|
40
44
|
};
|
|
41
|
-
|
|
45
|
+
BLOCK_RE.lastIndex = 0;
|
|
42
46
|
let match;
|
|
43
|
-
while ((match =
|
|
47
|
+
while ((match = BLOCK_RE.exec(source)) !== null) {
|
|
44
48
|
const tagName = match[1];
|
|
45
49
|
const attrString = match[2] || "";
|
|
46
50
|
const openTagStart = match.index;
|
|
@@ -71,11 +75,10 @@ function parse(source, filename = "anonymous.utopia") {
|
|
|
71
75
|
);
|
|
72
76
|
}
|
|
73
77
|
descriptor[tagName] = block;
|
|
74
|
-
|
|
78
|
+
BLOCK_RE.lastIndex = blockEnd;
|
|
75
79
|
}
|
|
76
80
|
return descriptor;
|
|
77
81
|
}
|
|
78
|
-
var ATTR_RE = /([a-zA-Z_][\w-]*)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
79
82
|
function parseAttributes(raw) {
|
|
80
83
|
const attrs = {};
|
|
81
84
|
let m;
|
|
@@ -112,6 +115,16 @@ var SFCParseError = class extends Error {
|
|
|
112
115
|
};
|
|
113
116
|
|
|
114
117
|
// src/template-compiler.ts
|
|
118
|
+
var TAG_NAME_CHAR_RE = /[a-zA-Z0-9\-_]/;
|
|
119
|
+
var ATTR_NAME_CHAR_RE = /[a-zA-Z0-9\-_:@.]/;
|
|
120
|
+
var ATTR_VALUE_END_RE = /[\s/>]/;
|
|
121
|
+
var WHITESPACE_RE = /\s/;
|
|
122
|
+
var TAG_START_CHAR_RE = /[a-zA-Z]/;
|
|
123
|
+
var U_FOR_EXPR_RE = /^\s*(?:\(\s*(\w+)\s*(?:,\s*(\w+)\s*)?\)|(\w+))\s+in\s+(.+)$/;
|
|
124
|
+
var COMPONENT_TAG_RE = /^[A-Z][a-zA-Z0-9_$]*$/;
|
|
125
|
+
var BACKSLASH_RE = /\\/g;
|
|
126
|
+
var SINGLE_QUOTE_RE = /'/g;
|
|
127
|
+
var HTML_ENTITY_RE = /&(?:#(\d+)|#x([0-9a-fA-F]+)|(\w+));/g;
|
|
115
128
|
function compileTemplate(template, options = {}) {
|
|
116
129
|
const ast = parseTemplate(template);
|
|
117
130
|
return generate(ast, options);
|
|
@@ -293,7 +306,7 @@ var TemplateParser = class {
|
|
|
293
306
|
// ---- Low-level helpers --------------------------------------------------
|
|
294
307
|
readTagName() {
|
|
295
308
|
const start = this.pos;
|
|
296
|
-
while (this.pos < this.source.length &&
|
|
309
|
+
while (this.pos < this.source.length && TAG_NAME_CHAR_RE.test(this.source[this.pos])) {
|
|
297
310
|
this.pos++;
|
|
298
311
|
}
|
|
299
312
|
const name = this.source.slice(start, this.pos);
|
|
@@ -302,7 +315,7 @@ var TemplateParser = class {
|
|
|
302
315
|
}
|
|
303
316
|
readAttributeName() {
|
|
304
317
|
const start = this.pos;
|
|
305
|
-
while (this.pos < this.source.length &&
|
|
318
|
+
while (this.pos < this.source.length && ATTR_NAME_CHAR_RE.test(this.source[this.pos])) {
|
|
306
319
|
this.pos++;
|
|
307
320
|
}
|
|
308
321
|
return this.source.slice(start, this.pos);
|
|
@@ -319,13 +332,13 @@ var TemplateParser = class {
|
|
|
319
332
|
return value;
|
|
320
333
|
}
|
|
321
334
|
const start = this.pos;
|
|
322
|
-
while (this.pos < this.source.length &&
|
|
335
|
+
while (this.pos < this.source.length && !ATTR_VALUE_END_RE.test(this.source[this.pos])) {
|
|
323
336
|
this.pos++;
|
|
324
337
|
}
|
|
325
338
|
return this.source.slice(start, this.pos);
|
|
326
339
|
}
|
|
327
340
|
skipWhitespace() {
|
|
328
|
-
while (this.pos < this.source.length &&
|
|
341
|
+
while (this.pos < this.source.length && WHITESPACE_RE.test(this.source[this.pos])) {
|
|
329
342
|
this.pos++;
|
|
330
343
|
}
|
|
331
344
|
}
|
|
@@ -335,7 +348,7 @@ var TemplateParser = class {
|
|
|
335
348
|
/** Returns true when the char after `<` looks like the start of a tag name. */
|
|
336
349
|
peekTagStart() {
|
|
337
350
|
const next = this.source[this.pos + 1];
|
|
338
|
-
return next !== void 0 &&
|
|
351
|
+
return next !== void 0 && TAG_START_CHAR_RE.test(next);
|
|
339
352
|
}
|
|
340
353
|
expect(str) {
|
|
341
354
|
if (!this.lookingAt(str)) {
|
|
@@ -405,7 +418,7 @@ function classifyDirective(name, value) {
|
|
|
405
418
|
return null;
|
|
406
419
|
}
|
|
407
420
|
function isDirectiveKind(s) {
|
|
408
|
-
return s === "on" || s === "bind" || s === "if" || s === "else" || s === "for" || s === "model";
|
|
421
|
+
return s === "on" || s === "bind" || s === "if" || s === "else" || s === "else-if" || s === "for" || s === "model";
|
|
409
422
|
}
|
|
410
423
|
var CodeGenerator = class {
|
|
411
424
|
constructor(options) {
|
|
@@ -496,7 +509,8 @@ ${fnBody}
|
|
|
496
509
|
}
|
|
497
510
|
}
|
|
498
511
|
for (const dir of node.directives) {
|
|
499
|
-
if (dir.kind === "if" || dir.kind === "else" || dir.kind === "for")
|
|
512
|
+
if (dir.kind === "if" || dir.kind === "else" || dir.kind === "else-if" || dir.kind === "for")
|
|
513
|
+
continue;
|
|
500
514
|
this.genDirective(elVar, dir, scope);
|
|
501
515
|
}
|
|
502
516
|
this.genChildren(elVar, node.children, scope);
|
|
@@ -582,7 +596,7 @@ ${fnBody}
|
|
|
582
596
|
this.emit(`addEventListener(${elVar}, 'input', (e) => ${signalRef}.set(e.target.value))`);
|
|
583
597
|
}
|
|
584
598
|
// ---- Structural: u-if ---------------------------------------------------
|
|
585
|
-
genIf(node, dir, scope, elseNode) {
|
|
599
|
+
genIf(node, dir, scope, elseIfChain, elseNode) {
|
|
586
600
|
this.helpers.add("createIf");
|
|
587
601
|
const anchorVar = this.freshVar();
|
|
588
602
|
this.helpers.add("createComment");
|
|
@@ -590,7 +604,7 @@ ${fnBody}
|
|
|
590
604
|
const condition = this.resolveExpression(dir.expression, scope);
|
|
591
605
|
const strippedNode = {
|
|
592
606
|
...node,
|
|
593
|
-
directives: node.directives.filter((d) => d.kind !== "if")
|
|
607
|
+
directives: node.directives.filter((d) => d.kind !== "if" && d.kind !== "else-if")
|
|
594
608
|
};
|
|
595
609
|
const trueFnVar = this.freshVar();
|
|
596
610
|
const savedCode = this.code;
|
|
@@ -605,7 +619,29 @@ ${fnBody}
|
|
|
605
619
|
this.emit(` return ${innerVar}`);
|
|
606
620
|
this.emit(`}`);
|
|
607
621
|
let elseArg = "";
|
|
608
|
-
if (
|
|
622
|
+
if (elseIfChain && elseIfChain.length > 0) {
|
|
623
|
+
const firstElseIf = elseIfChain[0];
|
|
624
|
+
const remainingElseIfs = elseIfChain.slice(1);
|
|
625
|
+
const falseFnVar = this.freshVar();
|
|
626
|
+
const savedCode2 = this.code;
|
|
627
|
+
this.code = [];
|
|
628
|
+
const nestedAnchor = this.genIf(
|
|
629
|
+
firstElseIf.node,
|
|
630
|
+
firstElseIf.dir,
|
|
631
|
+
scope,
|
|
632
|
+
remainingElseIfs.length > 0 ? remainingElseIfs : void 0,
|
|
633
|
+
elseNode
|
|
634
|
+
);
|
|
635
|
+
const nestedLines = [...this.code];
|
|
636
|
+
this.code = savedCode2;
|
|
637
|
+
this.emit(`const ${falseFnVar} = () => {`);
|
|
638
|
+
for (const line of nestedLines) {
|
|
639
|
+
this.emit(` ${line}`);
|
|
640
|
+
}
|
|
641
|
+
this.emit(` return ${nestedAnchor}`);
|
|
642
|
+
this.emit(`}`);
|
|
643
|
+
elseArg = `, ${falseFnVar}`;
|
|
644
|
+
} else if (elseNode) {
|
|
609
645
|
const falseFnVar = this.freshVar();
|
|
610
646
|
const strippedElse = {
|
|
611
647
|
...elseNode,
|
|
@@ -633,9 +669,7 @@ ${fnBody}
|
|
|
633
669
|
const anchorVar = this.freshVar();
|
|
634
670
|
this.helpers.add("createComment");
|
|
635
671
|
this.emit(`const ${anchorVar} = createComment('u-for')`);
|
|
636
|
-
const forMatch = dir.expression.match(
|
|
637
|
-
/^\s*(?:\(\s*(\w+)\s*(?:,\s*(\w+)\s*)?\)|(\w+))\s+in\s+(.+)$/
|
|
638
|
-
);
|
|
672
|
+
const forMatch = dir.expression.match(U_FOR_EXPR_RE);
|
|
639
673
|
if (!forMatch) {
|
|
640
674
|
throw new Error(`Invalid u-for expression: "${dir.expression}"`);
|
|
641
675
|
}
|
|
@@ -671,7 +705,7 @@ ${fnBody}
|
|
|
671
705
|
this.emitOrDefer(`createFor(${anchorVar}, () => ${listExpr}, ${renderFnVar}${keyArg})`);
|
|
672
706
|
return anchorVar;
|
|
673
707
|
}
|
|
674
|
-
// ---- Children processing (handles u-if / u-else pairing)
|
|
708
|
+
// ---- Children processing (handles u-if / u-else-if / u-else pairing) ----
|
|
675
709
|
genChildren(parentVar, children, scope) {
|
|
676
710
|
this.deferredCallsStack.push([]);
|
|
677
711
|
let i = 0;
|
|
@@ -680,24 +714,39 @@ ${fnBody}
|
|
|
680
714
|
if (child.type === 1 /* Element */) {
|
|
681
715
|
const ifDir = child.directives.find((d) => d.kind === "if");
|
|
682
716
|
if (ifDir) {
|
|
717
|
+
const elseIfChain = [];
|
|
683
718
|
let elseNode;
|
|
684
719
|
let skipTo = i + 1;
|
|
685
720
|
for (let j = i + 1; j < children.length; j++) {
|
|
686
721
|
const next = children[j];
|
|
687
722
|
if (next.type === 2 /* Text */ && !next.content.trim()) continue;
|
|
688
|
-
if (next.type === 1 /* Element */
|
|
689
|
-
|
|
690
|
-
|
|
723
|
+
if (next.type === 1 /* Element */) {
|
|
724
|
+
const elseIfDir = next.directives.find((d) => d.kind === "else-if");
|
|
725
|
+
if (elseIfDir) {
|
|
726
|
+
elseIfChain.push({ node: next, dir: elseIfDir });
|
|
727
|
+
skipTo = j + 1;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (next.directives.some((d) => d.kind === "else")) {
|
|
731
|
+
elseNode = next;
|
|
732
|
+
skipTo = j + 1;
|
|
733
|
+
}
|
|
691
734
|
}
|
|
692
735
|
break;
|
|
693
736
|
}
|
|
694
|
-
const childVar2 = this.genIf(
|
|
737
|
+
const childVar2 = this.genIf(
|
|
738
|
+
child,
|
|
739
|
+
ifDir,
|
|
740
|
+
scope,
|
|
741
|
+
elseIfChain.length > 0 ? elseIfChain : void 0,
|
|
742
|
+
elseNode
|
|
743
|
+
);
|
|
695
744
|
this.helpers.add("appendChild");
|
|
696
745
|
this.emit(`appendChild(${parentVar}, ${childVar2})`);
|
|
697
746
|
i = skipTo;
|
|
698
747
|
continue;
|
|
699
748
|
}
|
|
700
|
-
if (child.directives.some((d) => d.kind === "else")) {
|
|
749
|
+
if (child.directives.some((d) => d.kind === "else" || d.kind === "else-if")) {
|
|
701
750
|
i++;
|
|
702
751
|
continue;
|
|
703
752
|
}
|
|
@@ -823,10 +872,10 @@ ${fnBody}
|
|
|
823
872
|
}
|
|
824
873
|
};
|
|
825
874
|
function isComponentTag(tag) {
|
|
826
|
-
return
|
|
875
|
+
return COMPONENT_TAG_RE.test(tag);
|
|
827
876
|
}
|
|
828
877
|
function escapeStr(s) {
|
|
829
|
-
return s.replace(
|
|
878
|
+
return s.replace(BACKSLASH_RE, "\\\\").replace(SINGLE_QUOTE_RE, "\\'");
|
|
830
879
|
}
|
|
831
880
|
var ENTITY_MAP = {
|
|
832
881
|
"&": "&",
|
|
@@ -855,7 +904,8 @@ var ENTITY_MAP = {
|
|
|
855
904
|
"÷": "\xF7"
|
|
856
905
|
};
|
|
857
906
|
function decodeEntities(text) {
|
|
858
|
-
|
|
907
|
+
HTML_ENTITY_RE.lastIndex = 0;
|
|
908
|
+
return text.replace(HTML_ENTITY_RE, (match, dec, hex, named) => {
|
|
859
909
|
if (dec) {
|
|
860
910
|
const code = parseInt(dec, 10);
|
|
861
911
|
if (code >= 0 && code <= 1114111) {
|
|
@@ -888,6 +938,9 @@ function generate(ast, options) {
|
|
|
888
938
|
}
|
|
889
939
|
|
|
890
940
|
// src/style-compiler.ts
|
|
941
|
+
var WHITESPACE_RE2 = /\s/;
|
|
942
|
+
var KEYFRAMES_RE = /^@(?:-\w+-)?keyframes\b/;
|
|
943
|
+
var PSEUDO_SELECTOR_RE = /(?:::?[\w-]+(?:\([^)]*\))?)+$/;
|
|
891
944
|
function compileStyle(options) {
|
|
892
945
|
const { source, filename, scoped, scopeId: overrideScopeId } = options;
|
|
893
946
|
if (!scoped) {
|
|
@@ -908,7 +961,7 @@ function scopeSelectors(css, scopeId) {
|
|
|
908
961
|
const result = [];
|
|
909
962
|
let pos = 0;
|
|
910
963
|
while (pos < css.length) {
|
|
911
|
-
if (
|
|
964
|
+
if (WHITESPACE_RE2.test(css[pos])) {
|
|
912
965
|
result.push(css[pos]);
|
|
913
966
|
pos++;
|
|
914
967
|
continue;
|
|
@@ -957,7 +1010,7 @@ function consumeAtRule(css, pos, scopeId) {
|
|
|
957
1010
|
return { text: css.slice(start), end: css.length };
|
|
958
1011
|
}
|
|
959
1012
|
const header = css.slice(start, headerEnd);
|
|
960
|
-
const isKeyframes =
|
|
1013
|
+
const isKeyframes = KEYFRAMES_RE.test(header.trim());
|
|
961
1014
|
depth = 1;
|
|
962
1015
|
let bodyStart = headerEnd + 1;
|
|
963
1016
|
let bodyEnd = headerEnd + 1;
|
|
@@ -1000,8 +1053,7 @@ function consumeRuleSet(css, pos, scopeId) {
|
|
|
1000
1053
|
function scopeSingleSelector(selector, scopeId) {
|
|
1001
1054
|
if (!selector) return selector;
|
|
1002
1055
|
const attr = `[${scopeId}]`;
|
|
1003
|
-
const
|
|
1004
|
-
const pseudoMatch = selector.match(pseudoRe);
|
|
1056
|
+
const pseudoMatch = selector.match(PSEUDO_SELECTOR_RE);
|
|
1005
1057
|
if (pseudoMatch) {
|
|
1006
1058
|
const beforePseudo = selector.slice(0, pseudoMatch.index);
|
|
1007
1059
|
return `${beforePseudo}${attr}${pseudoMatch[0]}`;
|
|
@@ -1009,6 +1061,256 @@ function scopeSingleSelector(selector, scopeId) {
|
|
|
1009
1061
|
return `${selector}${attr}`;
|
|
1010
1062
|
}
|
|
1011
1063
|
|
|
1064
|
+
// src/a11y.ts
|
|
1065
|
+
var HEADING_LEVEL_RE = /^h([1-6])$/;
|
|
1066
|
+
var VALID_ARIA_ROLES = /* @__PURE__ */ new Set([
|
|
1067
|
+
"alert",
|
|
1068
|
+
"alertdialog",
|
|
1069
|
+
"application",
|
|
1070
|
+
"article",
|
|
1071
|
+
"banner",
|
|
1072
|
+
"button",
|
|
1073
|
+
"cell",
|
|
1074
|
+
"checkbox",
|
|
1075
|
+
"columnheader",
|
|
1076
|
+
"combobox",
|
|
1077
|
+
"complementary",
|
|
1078
|
+
"contentinfo",
|
|
1079
|
+
"definition",
|
|
1080
|
+
"dialog",
|
|
1081
|
+
"directory",
|
|
1082
|
+
"document",
|
|
1083
|
+
"feed",
|
|
1084
|
+
"figure",
|
|
1085
|
+
"form",
|
|
1086
|
+
"grid",
|
|
1087
|
+
"gridcell",
|
|
1088
|
+
"group",
|
|
1089
|
+
"heading",
|
|
1090
|
+
"img",
|
|
1091
|
+
"link",
|
|
1092
|
+
"list",
|
|
1093
|
+
"listbox",
|
|
1094
|
+
"listitem",
|
|
1095
|
+
"log",
|
|
1096
|
+
"main",
|
|
1097
|
+
"marquee",
|
|
1098
|
+
"math",
|
|
1099
|
+
"menu",
|
|
1100
|
+
"menubar",
|
|
1101
|
+
"menuitem",
|
|
1102
|
+
"menuitemcheckbox",
|
|
1103
|
+
"menuitemradio",
|
|
1104
|
+
"meter",
|
|
1105
|
+
"navigation",
|
|
1106
|
+
"none",
|
|
1107
|
+
"note",
|
|
1108
|
+
"option",
|
|
1109
|
+
"presentation",
|
|
1110
|
+
"progressbar",
|
|
1111
|
+
"radio",
|
|
1112
|
+
"radiogroup",
|
|
1113
|
+
"region",
|
|
1114
|
+
"row",
|
|
1115
|
+
"rowgroup",
|
|
1116
|
+
"rowheader",
|
|
1117
|
+
"scrollbar",
|
|
1118
|
+
"search",
|
|
1119
|
+
"searchbox",
|
|
1120
|
+
"separator",
|
|
1121
|
+
"slider",
|
|
1122
|
+
"spinbutton",
|
|
1123
|
+
"status",
|
|
1124
|
+
"switch",
|
|
1125
|
+
"tab",
|
|
1126
|
+
"table",
|
|
1127
|
+
"tablist",
|
|
1128
|
+
"tabpanel",
|
|
1129
|
+
"term",
|
|
1130
|
+
"textbox",
|
|
1131
|
+
"timer",
|
|
1132
|
+
"toolbar",
|
|
1133
|
+
"tooltip",
|
|
1134
|
+
"tree",
|
|
1135
|
+
"treegrid",
|
|
1136
|
+
"treeitem"
|
|
1137
|
+
]);
|
|
1138
|
+
var INTERACTIVE_ELEMENTS = /* @__PURE__ */ new Set([
|
|
1139
|
+
"a",
|
|
1140
|
+
"button",
|
|
1141
|
+
"input",
|
|
1142
|
+
"select",
|
|
1143
|
+
"textarea",
|
|
1144
|
+
"details",
|
|
1145
|
+
"summary"
|
|
1146
|
+
]);
|
|
1147
|
+
function getAttr(node, name) {
|
|
1148
|
+
return node.attrs.find((a) => a.name === name);
|
|
1149
|
+
}
|
|
1150
|
+
function hasAttr(node, name) {
|
|
1151
|
+
return node.attrs.some((a) => a.name === name);
|
|
1152
|
+
}
|
|
1153
|
+
function hasDirective(node, kind, arg) {
|
|
1154
|
+
return node.directives.some((d) => d.kind === kind && (arg === void 0 || d.arg === arg));
|
|
1155
|
+
}
|
|
1156
|
+
function hasBoundAttr(node, name) {
|
|
1157
|
+
return node.directives.some((d) => d.kind === "bind" && d.arg === name);
|
|
1158
|
+
}
|
|
1159
|
+
function hasTextContent(node) {
|
|
1160
|
+
return node.children.some(
|
|
1161
|
+
(c) => c.type === 2 /* Text */ && c.content.trim() !== "" || c.type === 3 /* Interpolation */
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
var rules = {
|
|
1165
|
+
"img-alt"(node, warnings) {
|
|
1166
|
+
if (node.tag !== "img") return;
|
|
1167
|
+
if (hasAttr(node, "alt") || hasBoundAttr(node, "alt")) return;
|
|
1168
|
+
if (hasAttr(node, "aria-label") || hasAttr(node, "aria-labelledby")) return;
|
|
1169
|
+
const role = getAttr(node, "role");
|
|
1170
|
+
if (role && (role.value === "presentation" || role.value === "none")) return;
|
|
1171
|
+
warnings.push({
|
|
1172
|
+
rule: "img-alt",
|
|
1173
|
+
message: "<img> element must have an alt attribute",
|
|
1174
|
+
tag: node.tag
|
|
1175
|
+
});
|
|
1176
|
+
},
|
|
1177
|
+
"click-keyboard"(node, warnings) {
|
|
1178
|
+
const hasClick = hasDirective(node, "on", "click");
|
|
1179
|
+
if (!hasClick) return;
|
|
1180
|
+
if (INTERACTIVE_ELEMENTS.has(node.tag)) return;
|
|
1181
|
+
const hasKeyboard = hasDirective(node, "on", "keydown") || hasDirective(node, "on", "keyup") || hasDirective(node, "on", "keypress");
|
|
1182
|
+
const role = getAttr(node, "role");
|
|
1183
|
+
const hasInteractiveRole = role && (role.value === "button" || role.value === "link");
|
|
1184
|
+
if (!hasKeyboard && !hasInteractiveRole) {
|
|
1185
|
+
warnings.push({
|
|
1186
|
+
rule: "click-keyboard",
|
|
1187
|
+
message: `<${node.tag}> with @click handler should also have a keyboard event handler or role="button"`,
|
|
1188
|
+
tag: node.tag
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
if (!hasAttr(node, "tabindex") && !hasBoundAttr(node, "tabindex")) {
|
|
1192
|
+
warnings.push({
|
|
1193
|
+
rule: "click-keyboard",
|
|
1194
|
+
message: `<${node.tag}> with @click handler should have tabindex="0" for keyboard accessibility`,
|
|
1195
|
+
tag: node.tag
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
},
|
|
1199
|
+
"anchor-content"(node, warnings) {
|
|
1200
|
+
if (node.tag !== "a") return;
|
|
1201
|
+
if (hasTextContent(node)) return;
|
|
1202
|
+
if (hasAttr(node, "aria-label") || hasAttr(node, "aria-labelledby")) return;
|
|
1203
|
+
const hasChildElements = node.children.some((c) => c.type === 1 /* Element */);
|
|
1204
|
+
if (hasChildElements) return;
|
|
1205
|
+
warnings.push({
|
|
1206
|
+
rule: "anchor-content",
|
|
1207
|
+
message: "<a> element must have content, aria-label, or aria-labelledby",
|
|
1208
|
+
tag: node.tag
|
|
1209
|
+
});
|
|
1210
|
+
},
|
|
1211
|
+
"form-label"(node, warnings) {
|
|
1212
|
+
const formElements = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
1213
|
+
if (!formElements.has(node.tag)) return;
|
|
1214
|
+
const type = getAttr(node, "type");
|
|
1215
|
+
if (type && type.value === "hidden") return;
|
|
1216
|
+
if (hasAttr(node, "aria-label") || hasAttr(node, "aria-labelledby")) return;
|
|
1217
|
+
if (hasAttr(node, "id")) return;
|
|
1218
|
+
if (hasAttr(node, "title")) return;
|
|
1219
|
+
warnings.push({
|
|
1220
|
+
rule: "form-label",
|
|
1221
|
+
message: `<${node.tag}> should have an id (with matching <label>), aria-label, or aria-labelledby`,
|
|
1222
|
+
tag: node.tag
|
|
1223
|
+
});
|
|
1224
|
+
},
|
|
1225
|
+
"no-distracting"(node, warnings) {
|
|
1226
|
+
if (node.tag === "marquee" || node.tag === "blink") {
|
|
1227
|
+
warnings.push({
|
|
1228
|
+
rule: "no-distracting",
|
|
1229
|
+
message: `<${node.tag}> is distracting and inaccessible \u2014 do not use`,
|
|
1230
|
+
tag: node.tag
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
"heading-order"(node, warnings, ctx) {
|
|
1235
|
+
const match = node.tag.match(HEADING_LEVEL_RE);
|
|
1236
|
+
if (!match) return;
|
|
1237
|
+
const level = parseInt(match[1], 10);
|
|
1238
|
+
if (ctx.lastHeadingLevel > 0 && level > ctx.lastHeadingLevel + 1) {
|
|
1239
|
+
warnings.push({
|
|
1240
|
+
rule: "heading-order",
|
|
1241
|
+
message: `Heading level <${node.tag}> skips from <h${ctx.lastHeadingLevel}> \u2014 headings should not skip levels`,
|
|
1242
|
+
tag: node.tag
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
ctx.lastHeadingLevel = level;
|
|
1246
|
+
},
|
|
1247
|
+
"aria-role"(node, warnings) {
|
|
1248
|
+
const role = getAttr(node, "role");
|
|
1249
|
+
if (!role || !role.value) return;
|
|
1250
|
+
if (!VALID_ARIA_ROLES.has(role.value)) {
|
|
1251
|
+
warnings.push({
|
|
1252
|
+
rule: "aria-role",
|
|
1253
|
+
message: `Invalid ARIA role "${role.value}" on <${node.tag}>`,
|
|
1254
|
+
tag: node.tag
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
},
|
|
1258
|
+
"no-positive-tabindex"(node, warnings) {
|
|
1259
|
+
const tabindex = getAttr(node, "tabindex");
|
|
1260
|
+
if (!tabindex || !tabindex.value) return;
|
|
1261
|
+
const val = parseInt(tabindex.value, 10);
|
|
1262
|
+
if (!isNaN(val) && val > 0) {
|
|
1263
|
+
warnings.push({
|
|
1264
|
+
rule: "no-positive-tabindex",
|
|
1265
|
+
message: `Avoid positive tabindex="${tabindex.value}" \u2014 it disrupts natural tab order`,
|
|
1266
|
+
tag: node.tag
|
|
1267
|
+
});
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
"media-captions"(node, warnings) {
|
|
1271
|
+
if (node.tag !== "video" && node.tag !== "audio") return;
|
|
1272
|
+
const hasTrack = node.children.some(
|
|
1273
|
+
(c) => c.type === 1 /* Element */ && c.tag === "track"
|
|
1274
|
+
);
|
|
1275
|
+
if (hasTrack) return;
|
|
1276
|
+
if (hasAttr(node, "aria-label") || hasAttr(node, "aria-labelledby")) return;
|
|
1277
|
+
warnings.push({
|
|
1278
|
+
rule: "media-captions",
|
|
1279
|
+
message: `<${node.tag}> should have a <track> element for captions`,
|
|
1280
|
+
tag: node.tag
|
|
1281
|
+
});
|
|
1282
|
+
},
|
|
1283
|
+
"anchor-valid"(node, warnings) {
|
|
1284
|
+
if (node.tag !== "a") return;
|
|
1285
|
+
if (hasAttr(node, "href") || hasBoundAttr(node, "href")) return;
|
|
1286
|
+
const role = getAttr(node, "role");
|
|
1287
|
+
if (role && role.value === "button") return;
|
|
1288
|
+
warnings.push({
|
|
1289
|
+
rule: "anchor-valid",
|
|
1290
|
+
message: "<a> element should have an href attribute",
|
|
1291
|
+
tag: node.tag
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
function walkNodes(nodes, enabledRules, warnings, ctx) {
|
|
1296
|
+
for (const node of nodes) {
|
|
1297
|
+
if (node.type === 1 /* Element */) {
|
|
1298
|
+
for (const rule of enabledRules) {
|
|
1299
|
+
rule(node, warnings, ctx);
|
|
1300
|
+
}
|
|
1301
|
+
walkNodes(node.children, enabledRules, warnings, ctx);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
function checkA11y(ast, options) {
|
|
1306
|
+
const disabled = new Set(options?.disable ?? []);
|
|
1307
|
+
const enabledRules = Object.entries(rules).filter(([id]) => !disabled.has(id)).map(([, fn]) => fn);
|
|
1308
|
+
const warnings = [];
|
|
1309
|
+
const ctx = { lastHeadingLevel: 0 };
|
|
1310
|
+
walkNodes(ast, enabledRules, warnings, ctx);
|
|
1311
|
+
return warnings;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1012
1314
|
// src/index.ts
|
|
1013
1315
|
function compile(source, options = {}) {
|
|
1014
1316
|
const filename = options.filename ?? "anonymous.utopia";
|
|
@@ -1027,11 +1329,16 @@ function compile(source, options = {}) {
|
|
|
1027
1329
|
scopeId = styleResult.scopeId;
|
|
1028
1330
|
}
|
|
1029
1331
|
let renderModule = "";
|
|
1332
|
+
let a11yWarnings = [];
|
|
1030
1333
|
if (descriptor.template) {
|
|
1031
1334
|
const templateResult = compileTemplate(descriptor.template.content, {
|
|
1032
1335
|
scopeId: scopeId ?? void 0
|
|
1033
1336
|
});
|
|
1034
1337
|
renderModule = templateResult.code;
|
|
1338
|
+
if (options.a11y !== false) {
|
|
1339
|
+
const ast = parseTemplate(descriptor.template.content);
|
|
1340
|
+
a11yWarnings = checkA11y(ast, options.a11y ?? void 0);
|
|
1341
|
+
}
|
|
1035
1342
|
}
|
|
1036
1343
|
const { imports, body } = splitModuleParts(renderModule);
|
|
1037
1344
|
const scriptContent = descriptor.script?.content ?? "";
|
|
@@ -1047,7 +1354,7 @@ function compile(source, options = {}) {
|
|
|
1047
1354
|
}
|
|
1048
1355
|
parts.push("export default { render: __render }");
|
|
1049
1356
|
const code = parts.join("\n\n") + "\n";
|
|
1050
|
-
return { code, css };
|
|
1357
|
+
return { code, css, a11y: a11yWarnings };
|
|
1051
1358
|
}
|
|
1052
1359
|
function splitModuleParts(moduleCode) {
|
|
1053
1360
|
const idx = moduleCode.indexOf("\n\n");
|
|
@@ -1061,6 +1368,7 @@ function splitModuleParts(moduleCode) {
|
|
|
1061
1368
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1062
1369
|
0 && (module.exports = {
|
|
1063
1370
|
SFCParseError,
|
|
1371
|
+
checkA11y,
|
|
1064
1372
|
compile,
|
|
1065
1373
|
compileStyle,
|
|
1066
1374
|
compileTemplate,
|
package/dist/index.d.cts
CHANGED
|
@@ -14,6 +14,7 @@ interface SFCDescriptor {
|
|
|
14
14
|
template: SFCBlock | null;
|
|
15
15
|
script: SFCBlock | null;
|
|
16
16
|
style: SFCBlock | null;
|
|
17
|
+
test: SFCBlock | null;
|
|
17
18
|
filename: string;
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
@@ -84,7 +85,7 @@ interface Directive {
|
|
|
84
85
|
expression: string;
|
|
85
86
|
modifiers: string[];
|
|
86
87
|
}
|
|
87
|
-
type DirectiveKind = 'on' | 'bind' | 'if' | 'else' | 'for' | 'model';
|
|
88
|
+
type DirectiveKind = 'on' | 'bind' | 'if' | 'else' | 'else-if' | 'for' | 'model';
|
|
88
89
|
/** Exported for testing — parse a template string into an AST. */
|
|
89
90
|
declare function parseTemplate(source: string): TemplateNode[];
|
|
90
91
|
|
|
@@ -119,11 +120,38 @@ declare function compileStyle(options: StyleCompileOptions): StyleCompileResult;
|
|
|
119
120
|
*/
|
|
120
121
|
declare function generateScopeId(filename: string): string;
|
|
121
122
|
|
|
123
|
+
interface A11yWarning {
|
|
124
|
+
/** Rule ID (e.g. 'img-alt'). */
|
|
125
|
+
rule: string;
|
|
126
|
+
/** Human-readable message. */
|
|
127
|
+
message: string;
|
|
128
|
+
/** The element tag that triggered the warning. */
|
|
129
|
+
tag: string;
|
|
130
|
+
}
|
|
131
|
+
interface A11yOptions {
|
|
132
|
+
/** Rules to disable (by rule ID). */
|
|
133
|
+
disable?: string[];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check a parsed template AST for accessibility issues.
|
|
137
|
+
*
|
|
138
|
+
* Returns an array of warnings. An empty array means no issues found.
|
|
139
|
+
*
|
|
140
|
+
* ```ts
|
|
141
|
+
* const ast = parseTemplate('<img src="photo.jpg">');
|
|
142
|
+
* const warnings = checkA11y(ast);
|
|
143
|
+
* // [{ rule: 'img-alt', message: '...', tag: 'img' }]
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
declare function checkA11y(ast: TemplateNode[], options?: A11yOptions): A11yWarning[];
|
|
147
|
+
|
|
122
148
|
interface CompileOptions {
|
|
123
149
|
/** Filename for error messages and scope-id generation. */
|
|
124
150
|
filename?: string;
|
|
125
151
|
/** Override the scope ID for testing. */
|
|
126
152
|
scopeId?: string;
|
|
153
|
+
/** Accessibility checking options. Pass false to disable entirely. */
|
|
154
|
+
a11y?: A11yOptions | false;
|
|
127
155
|
}
|
|
128
156
|
interface CompileResult {
|
|
129
157
|
/** The compiled JavaScript module source. */
|
|
@@ -132,6 +160,8 @@ interface CompileResult {
|
|
|
132
160
|
css: string;
|
|
133
161
|
/** Source map (reserved for future use). */
|
|
134
162
|
map?: unknown;
|
|
163
|
+
/** Accessibility warnings from template analysis. */
|
|
164
|
+
a11y: A11yWarning[];
|
|
135
165
|
}
|
|
136
166
|
/**
|
|
137
167
|
* Compile a `.utopia` single-file component source string.
|
|
@@ -153,4 +183,4 @@ interface CompileResult {
|
|
|
153
183
|
*/
|
|
154
184
|
declare function compile(source: string, options?: CompileOptions): CompileResult;
|
|
155
185
|
|
|
156
|
-
export { type CompileOptions, type CompileResult, type SFCBlock, type SFCDescriptor, SFCParseError, type StyleCompileOptions, type StyleCompileResult, type TemplateCompileOptions, type TemplateCompileResult, compile, compileStyle, compileTemplate, generateScopeId, parse, parseTemplate };
|
|
186
|
+
export { type A11yOptions, type A11yWarning, type CompileOptions, type CompileResult, type SFCBlock, type SFCDescriptor, SFCParseError, type StyleCompileOptions, type StyleCompileResult, type TemplateCompileOptions, type TemplateCompileResult, checkA11y, compile, compileStyle, compileTemplate, generateScopeId, parse, parseTemplate };
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ interface SFCDescriptor {
|
|
|
14
14
|
template: SFCBlock | null;
|
|
15
15
|
script: SFCBlock | null;
|
|
16
16
|
style: SFCBlock | null;
|
|
17
|
+
test: SFCBlock | null;
|
|
17
18
|
filename: string;
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
@@ -84,7 +85,7 @@ interface Directive {
|
|
|
84
85
|
expression: string;
|
|
85
86
|
modifiers: string[];
|
|
86
87
|
}
|
|
87
|
-
type DirectiveKind = 'on' | 'bind' | 'if' | 'else' | 'for' | 'model';
|
|
88
|
+
type DirectiveKind = 'on' | 'bind' | 'if' | 'else' | 'else-if' | 'for' | 'model';
|
|
88
89
|
/** Exported for testing — parse a template string into an AST. */
|
|
89
90
|
declare function parseTemplate(source: string): TemplateNode[];
|
|
90
91
|
|
|
@@ -119,11 +120,38 @@ declare function compileStyle(options: StyleCompileOptions): StyleCompileResult;
|
|
|
119
120
|
*/
|
|
120
121
|
declare function generateScopeId(filename: string): string;
|
|
121
122
|
|
|
123
|
+
interface A11yWarning {
|
|
124
|
+
/** Rule ID (e.g. 'img-alt'). */
|
|
125
|
+
rule: string;
|
|
126
|
+
/** Human-readable message. */
|
|
127
|
+
message: string;
|
|
128
|
+
/** The element tag that triggered the warning. */
|
|
129
|
+
tag: string;
|
|
130
|
+
}
|
|
131
|
+
interface A11yOptions {
|
|
132
|
+
/** Rules to disable (by rule ID). */
|
|
133
|
+
disable?: string[];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check a parsed template AST for accessibility issues.
|
|
137
|
+
*
|
|
138
|
+
* Returns an array of warnings. An empty array means no issues found.
|
|
139
|
+
*
|
|
140
|
+
* ```ts
|
|
141
|
+
* const ast = parseTemplate('<img src="photo.jpg">');
|
|
142
|
+
* const warnings = checkA11y(ast);
|
|
143
|
+
* // [{ rule: 'img-alt', message: '...', tag: 'img' }]
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
declare function checkA11y(ast: TemplateNode[], options?: A11yOptions): A11yWarning[];
|
|
147
|
+
|
|
122
148
|
interface CompileOptions {
|
|
123
149
|
/** Filename for error messages and scope-id generation. */
|
|
124
150
|
filename?: string;
|
|
125
151
|
/** Override the scope ID for testing. */
|
|
126
152
|
scopeId?: string;
|
|
153
|
+
/** Accessibility checking options. Pass false to disable entirely. */
|
|
154
|
+
a11y?: A11yOptions | false;
|
|
127
155
|
}
|
|
128
156
|
interface CompileResult {
|
|
129
157
|
/** The compiled JavaScript module source. */
|
|
@@ -132,6 +160,8 @@ interface CompileResult {
|
|
|
132
160
|
css: string;
|
|
133
161
|
/** Source map (reserved for future use). */
|
|
134
162
|
map?: unknown;
|
|
163
|
+
/** Accessibility warnings from template analysis. */
|
|
164
|
+
a11y: A11yWarning[];
|
|
135
165
|
}
|
|
136
166
|
/**
|
|
137
167
|
* Compile a `.utopia` single-file component source string.
|
|
@@ -153,4 +183,4 @@ interface CompileResult {
|
|
|
153
183
|
*/
|
|
154
184
|
declare function compile(source: string, options?: CompileOptions): CompileResult;
|
|
155
185
|
|
|
156
|
-
export { type CompileOptions, type CompileResult, type SFCBlock, type SFCDescriptor, SFCParseError, type StyleCompileOptions, type StyleCompileResult, type TemplateCompileOptions, type TemplateCompileResult, compile, compileStyle, compileTemplate, generateScopeId, parse, parseTemplate };
|
|
186
|
+
export { type A11yOptions, type A11yWarning, type CompileOptions, type CompileResult, type SFCBlock, type SFCDescriptor, SFCParseError, type StyleCompileOptions, type StyleCompileResult, type TemplateCompileOptions, type TemplateCompileResult, checkA11y, compile, compileStyle, compileTemplate, generateScopeId, parse, parseTemplate };
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
// src/parser.ts
|
|
2
|
+
var BLOCK_RE = /<(template|script|style|test)([\s][^>]*)?\s*>/g;
|
|
3
|
+
var ATTR_RE = /([a-zA-Z_][\w-]*)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
2
4
|
function parse(source, filename = "anonymous.utopia") {
|
|
3
5
|
const descriptor = {
|
|
4
6
|
template: null,
|
|
5
7
|
script: null,
|
|
6
8
|
style: null,
|
|
9
|
+
test: null,
|
|
7
10
|
filename
|
|
8
11
|
};
|
|
9
|
-
|
|
12
|
+
BLOCK_RE.lastIndex = 0;
|
|
10
13
|
let match;
|
|
11
|
-
while ((match =
|
|
14
|
+
while ((match = BLOCK_RE.exec(source)) !== null) {
|
|
12
15
|
const tagName = match[1];
|
|
13
16
|
const attrString = match[2] || "";
|
|
14
17
|
const openTagStart = match.index;
|
|
@@ -39,11 +42,10 @@ function parse(source, filename = "anonymous.utopia") {
|
|
|
39
42
|
);
|
|
40
43
|
}
|
|
41
44
|
descriptor[tagName] = block;
|
|
42
|
-
|
|
45
|
+
BLOCK_RE.lastIndex = blockEnd;
|
|
43
46
|
}
|
|
44
47
|
return descriptor;
|
|
45
48
|
}
|
|
46
|
-
var ATTR_RE = /([a-zA-Z_][\w-]*)\s*(?:=\s*(?:"([^"]*)"|'([^']*)'|(\S+)))?/g;
|
|
47
49
|
function parseAttributes(raw) {
|
|
48
50
|
const attrs = {};
|
|
49
51
|
let m;
|
|
@@ -80,6 +82,16 @@ var SFCParseError = class extends Error {
|
|
|
80
82
|
};
|
|
81
83
|
|
|
82
84
|
// src/template-compiler.ts
|
|
85
|
+
var TAG_NAME_CHAR_RE = /[a-zA-Z0-9\-_]/;
|
|
86
|
+
var ATTR_NAME_CHAR_RE = /[a-zA-Z0-9\-_:@.]/;
|
|
87
|
+
var ATTR_VALUE_END_RE = /[\s/>]/;
|
|
88
|
+
var WHITESPACE_RE = /\s/;
|
|
89
|
+
var TAG_START_CHAR_RE = /[a-zA-Z]/;
|
|
90
|
+
var U_FOR_EXPR_RE = /^\s*(?:\(\s*(\w+)\s*(?:,\s*(\w+)\s*)?\)|(\w+))\s+in\s+(.+)$/;
|
|
91
|
+
var COMPONENT_TAG_RE = /^[A-Z][a-zA-Z0-9_$]*$/;
|
|
92
|
+
var BACKSLASH_RE = /\\/g;
|
|
93
|
+
var SINGLE_QUOTE_RE = /'/g;
|
|
94
|
+
var HTML_ENTITY_RE = /&(?:#(\d+)|#x([0-9a-fA-F]+)|(\w+));/g;
|
|
83
95
|
function compileTemplate(template, options = {}) {
|
|
84
96
|
const ast = parseTemplate(template);
|
|
85
97
|
return generate(ast, options);
|
|
@@ -261,7 +273,7 @@ var TemplateParser = class {
|
|
|
261
273
|
// ---- Low-level helpers --------------------------------------------------
|
|
262
274
|
readTagName() {
|
|
263
275
|
const start = this.pos;
|
|
264
|
-
while (this.pos < this.source.length &&
|
|
276
|
+
while (this.pos < this.source.length && TAG_NAME_CHAR_RE.test(this.source[this.pos])) {
|
|
265
277
|
this.pos++;
|
|
266
278
|
}
|
|
267
279
|
const name = this.source.slice(start, this.pos);
|
|
@@ -270,7 +282,7 @@ var TemplateParser = class {
|
|
|
270
282
|
}
|
|
271
283
|
readAttributeName() {
|
|
272
284
|
const start = this.pos;
|
|
273
|
-
while (this.pos < this.source.length &&
|
|
285
|
+
while (this.pos < this.source.length && ATTR_NAME_CHAR_RE.test(this.source[this.pos])) {
|
|
274
286
|
this.pos++;
|
|
275
287
|
}
|
|
276
288
|
return this.source.slice(start, this.pos);
|
|
@@ -287,13 +299,13 @@ var TemplateParser = class {
|
|
|
287
299
|
return value;
|
|
288
300
|
}
|
|
289
301
|
const start = this.pos;
|
|
290
|
-
while (this.pos < this.source.length &&
|
|
302
|
+
while (this.pos < this.source.length && !ATTR_VALUE_END_RE.test(this.source[this.pos])) {
|
|
291
303
|
this.pos++;
|
|
292
304
|
}
|
|
293
305
|
return this.source.slice(start, this.pos);
|
|
294
306
|
}
|
|
295
307
|
skipWhitespace() {
|
|
296
|
-
while (this.pos < this.source.length &&
|
|
308
|
+
while (this.pos < this.source.length && WHITESPACE_RE.test(this.source[this.pos])) {
|
|
297
309
|
this.pos++;
|
|
298
310
|
}
|
|
299
311
|
}
|
|
@@ -303,7 +315,7 @@ var TemplateParser = class {
|
|
|
303
315
|
/** Returns true when the char after `<` looks like the start of a tag name. */
|
|
304
316
|
peekTagStart() {
|
|
305
317
|
const next = this.source[this.pos + 1];
|
|
306
|
-
return next !== void 0 &&
|
|
318
|
+
return next !== void 0 && TAG_START_CHAR_RE.test(next);
|
|
307
319
|
}
|
|
308
320
|
expect(str) {
|
|
309
321
|
if (!this.lookingAt(str)) {
|
|
@@ -373,7 +385,7 @@ function classifyDirective(name, value) {
|
|
|
373
385
|
return null;
|
|
374
386
|
}
|
|
375
387
|
function isDirectiveKind(s) {
|
|
376
|
-
return s === "on" || s === "bind" || s === "if" || s === "else" || s === "for" || s === "model";
|
|
388
|
+
return s === "on" || s === "bind" || s === "if" || s === "else" || s === "else-if" || s === "for" || s === "model";
|
|
377
389
|
}
|
|
378
390
|
var CodeGenerator = class {
|
|
379
391
|
constructor(options) {
|
|
@@ -464,7 +476,8 @@ ${fnBody}
|
|
|
464
476
|
}
|
|
465
477
|
}
|
|
466
478
|
for (const dir of node.directives) {
|
|
467
|
-
if (dir.kind === "if" || dir.kind === "else" || dir.kind === "for")
|
|
479
|
+
if (dir.kind === "if" || dir.kind === "else" || dir.kind === "else-if" || dir.kind === "for")
|
|
480
|
+
continue;
|
|
468
481
|
this.genDirective(elVar, dir, scope);
|
|
469
482
|
}
|
|
470
483
|
this.genChildren(elVar, node.children, scope);
|
|
@@ -550,7 +563,7 @@ ${fnBody}
|
|
|
550
563
|
this.emit(`addEventListener(${elVar}, 'input', (e) => ${signalRef}.set(e.target.value))`);
|
|
551
564
|
}
|
|
552
565
|
// ---- Structural: u-if ---------------------------------------------------
|
|
553
|
-
genIf(node, dir, scope, elseNode) {
|
|
566
|
+
genIf(node, dir, scope, elseIfChain, elseNode) {
|
|
554
567
|
this.helpers.add("createIf");
|
|
555
568
|
const anchorVar = this.freshVar();
|
|
556
569
|
this.helpers.add("createComment");
|
|
@@ -558,7 +571,7 @@ ${fnBody}
|
|
|
558
571
|
const condition = this.resolveExpression(dir.expression, scope);
|
|
559
572
|
const strippedNode = {
|
|
560
573
|
...node,
|
|
561
|
-
directives: node.directives.filter((d) => d.kind !== "if")
|
|
574
|
+
directives: node.directives.filter((d) => d.kind !== "if" && d.kind !== "else-if")
|
|
562
575
|
};
|
|
563
576
|
const trueFnVar = this.freshVar();
|
|
564
577
|
const savedCode = this.code;
|
|
@@ -573,7 +586,29 @@ ${fnBody}
|
|
|
573
586
|
this.emit(` return ${innerVar}`);
|
|
574
587
|
this.emit(`}`);
|
|
575
588
|
let elseArg = "";
|
|
576
|
-
if (
|
|
589
|
+
if (elseIfChain && elseIfChain.length > 0) {
|
|
590
|
+
const firstElseIf = elseIfChain[0];
|
|
591
|
+
const remainingElseIfs = elseIfChain.slice(1);
|
|
592
|
+
const falseFnVar = this.freshVar();
|
|
593
|
+
const savedCode2 = this.code;
|
|
594
|
+
this.code = [];
|
|
595
|
+
const nestedAnchor = this.genIf(
|
|
596
|
+
firstElseIf.node,
|
|
597
|
+
firstElseIf.dir,
|
|
598
|
+
scope,
|
|
599
|
+
remainingElseIfs.length > 0 ? remainingElseIfs : void 0,
|
|
600
|
+
elseNode
|
|
601
|
+
);
|
|
602
|
+
const nestedLines = [...this.code];
|
|
603
|
+
this.code = savedCode2;
|
|
604
|
+
this.emit(`const ${falseFnVar} = () => {`);
|
|
605
|
+
for (const line of nestedLines) {
|
|
606
|
+
this.emit(` ${line}`);
|
|
607
|
+
}
|
|
608
|
+
this.emit(` return ${nestedAnchor}`);
|
|
609
|
+
this.emit(`}`);
|
|
610
|
+
elseArg = `, ${falseFnVar}`;
|
|
611
|
+
} else if (elseNode) {
|
|
577
612
|
const falseFnVar = this.freshVar();
|
|
578
613
|
const strippedElse = {
|
|
579
614
|
...elseNode,
|
|
@@ -601,9 +636,7 @@ ${fnBody}
|
|
|
601
636
|
const anchorVar = this.freshVar();
|
|
602
637
|
this.helpers.add("createComment");
|
|
603
638
|
this.emit(`const ${anchorVar} = createComment('u-for')`);
|
|
604
|
-
const forMatch = dir.expression.match(
|
|
605
|
-
/^\s*(?:\(\s*(\w+)\s*(?:,\s*(\w+)\s*)?\)|(\w+))\s+in\s+(.+)$/
|
|
606
|
-
);
|
|
639
|
+
const forMatch = dir.expression.match(U_FOR_EXPR_RE);
|
|
607
640
|
if (!forMatch) {
|
|
608
641
|
throw new Error(`Invalid u-for expression: "${dir.expression}"`);
|
|
609
642
|
}
|
|
@@ -639,7 +672,7 @@ ${fnBody}
|
|
|
639
672
|
this.emitOrDefer(`createFor(${anchorVar}, () => ${listExpr}, ${renderFnVar}${keyArg})`);
|
|
640
673
|
return anchorVar;
|
|
641
674
|
}
|
|
642
|
-
// ---- Children processing (handles u-if / u-else pairing)
|
|
675
|
+
// ---- Children processing (handles u-if / u-else-if / u-else pairing) ----
|
|
643
676
|
genChildren(parentVar, children, scope) {
|
|
644
677
|
this.deferredCallsStack.push([]);
|
|
645
678
|
let i = 0;
|
|
@@ -648,24 +681,39 @@ ${fnBody}
|
|
|
648
681
|
if (child.type === 1 /* Element */) {
|
|
649
682
|
const ifDir = child.directives.find((d) => d.kind === "if");
|
|
650
683
|
if (ifDir) {
|
|
684
|
+
const elseIfChain = [];
|
|
651
685
|
let elseNode;
|
|
652
686
|
let skipTo = i + 1;
|
|
653
687
|
for (let j = i + 1; j < children.length; j++) {
|
|
654
688
|
const next = children[j];
|
|
655
689
|
if (next.type === 2 /* Text */ && !next.content.trim()) continue;
|
|
656
|
-
if (next.type === 1 /* Element */
|
|
657
|
-
|
|
658
|
-
|
|
690
|
+
if (next.type === 1 /* Element */) {
|
|
691
|
+
const elseIfDir = next.directives.find((d) => d.kind === "else-if");
|
|
692
|
+
if (elseIfDir) {
|
|
693
|
+
elseIfChain.push({ node: next, dir: elseIfDir });
|
|
694
|
+
skipTo = j + 1;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (next.directives.some((d) => d.kind === "else")) {
|
|
698
|
+
elseNode = next;
|
|
699
|
+
skipTo = j + 1;
|
|
700
|
+
}
|
|
659
701
|
}
|
|
660
702
|
break;
|
|
661
703
|
}
|
|
662
|
-
const childVar2 = this.genIf(
|
|
704
|
+
const childVar2 = this.genIf(
|
|
705
|
+
child,
|
|
706
|
+
ifDir,
|
|
707
|
+
scope,
|
|
708
|
+
elseIfChain.length > 0 ? elseIfChain : void 0,
|
|
709
|
+
elseNode
|
|
710
|
+
);
|
|
663
711
|
this.helpers.add("appendChild");
|
|
664
712
|
this.emit(`appendChild(${parentVar}, ${childVar2})`);
|
|
665
713
|
i = skipTo;
|
|
666
714
|
continue;
|
|
667
715
|
}
|
|
668
|
-
if (child.directives.some((d) => d.kind === "else")) {
|
|
716
|
+
if (child.directives.some((d) => d.kind === "else" || d.kind === "else-if")) {
|
|
669
717
|
i++;
|
|
670
718
|
continue;
|
|
671
719
|
}
|
|
@@ -791,10 +839,10 @@ ${fnBody}
|
|
|
791
839
|
}
|
|
792
840
|
};
|
|
793
841
|
function isComponentTag(tag) {
|
|
794
|
-
return
|
|
842
|
+
return COMPONENT_TAG_RE.test(tag);
|
|
795
843
|
}
|
|
796
844
|
function escapeStr(s) {
|
|
797
|
-
return s.replace(
|
|
845
|
+
return s.replace(BACKSLASH_RE, "\\\\").replace(SINGLE_QUOTE_RE, "\\'");
|
|
798
846
|
}
|
|
799
847
|
var ENTITY_MAP = {
|
|
800
848
|
"&": "&",
|
|
@@ -823,7 +871,8 @@ var ENTITY_MAP = {
|
|
|
823
871
|
"÷": "\xF7"
|
|
824
872
|
};
|
|
825
873
|
function decodeEntities(text) {
|
|
826
|
-
|
|
874
|
+
HTML_ENTITY_RE.lastIndex = 0;
|
|
875
|
+
return text.replace(HTML_ENTITY_RE, (match, dec, hex, named) => {
|
|
827
876
|
if (dec) {
|
|
828
877
|
const code = parseInt(dec, 10);
|
|
829
878
|
if (code >= 0 && code <= 1114111) {
|
|
@@ -856,6 +905,9 @@ function generate(ast, options) {
|
|
|
856
905
|
}
|
|
857
906
|
|
|
858
907
|
// src/style-compiler.ts
|
|
908
|
+
var WHITESPACE_RE2 = /\s/;
|
|
909
|
+
var KEYFRAMES_RE = /^@(?:-\w+-)?keyframes\b/;
|
|
910
|
+
var PSEUDO_SELECTOR_RE = /(?:::?[\w-]+(?:\([^)]*\))?)+$/;
|
|
859
911
|
function compileStyle(options) {
|
|
860
912
|
const { source, filename, scoped, scopeId: overrideScopeId } = options;
|
|
861
913
|
if (!scoped) {
|
|
@@ -876,7 +928,7 @@ function scopeSelectors(css, scopeId) {
|
|
|
876
928
|
const result = [];
|
|
877
929
|
let pos = 0;
|
|
878
930
|
while (pos < css.length) {
|
|
879
|
-
if (
|
|
931
|
+
if (WHITESPACE_RE2.test(css[pos])) {
|
|
880
932
|
result.push(css[pos]);
|
|
881
933
|
pos++;
|
|
882
934
|
continue;
|
|
@@ -925,7 +977,7 @@ function consumeAtRule(css, pos, scopeId) {
|
|
|
925
977
|
return { text: css.slice(start), end: css.length };
|
|
926
978
|
}
|
|
927
979
|
const header = css.slice(start, headerEnd);
|
|
928
|
-
const isKeyframes =
|
|
980
|
+
const isKeyframes = KEYFRAMES_RE.test(header.trim());
|
|
929
981
|
depth = 1;
|
|
930
982
|
let bodyStart = headerEnd + 1;
|
|
931
983
|
let bodyEnd = headerEnd + 1;
|
|
@@ -968,8 +1020,7 @@ function consumeRuleSet(css, pos, scopeId) {
|
|
|
968
1020
|
function scopeSingleSelector(selector, scopeId) {
|
|
969
1021
|
if (!selector) return selector;
|
|
970
1022
|
const attr = `[${scopeId}]`;
|
|
971
|
-
const
|
|
972
|
-
const pseudoMatch = selector.match(pseudoRe);
|
|
1023
|
+
const pseudoMatch = selector.match(PSEUDO_SELECTOR_RE);
|
|
973
1024
|
if (pseudoMatch) {
|
|
974
1025
|
const beforePseudo = selector.slice(0, pseudoMatch.index);
|
|
975
1026
|
return `${beforePseudo}${attr}${pseudoMatch[0]}`;
|
|
@@ -977,6 +1028,256 @@ function scopeSingleSelector(selector, scopeId) {
|
|
|
977
1028
|
return `${selector}${attr}`;
|
|
978
1029
|
}
|
|
979
1030
|
|
|
1031
|
+
// src/a11y.ts
|
|
1032
|
+
var HEADING_LEVEL_RE = /^h([1-6])$/;
|
|
1033
|
+
var VALID_ARIA_ROLES = /* @__PURE__ */ new Set([
|
|
1034
|
+
"alert",
|
|
1035
|
+
"alertdialog",
|
|
1036
|
+
"application",
|
|
1037
|
+
"article",
|
|
1038
|
+
"banner",
|
|
1039
|
+
"button",
|
|
1040
|
+
"cell",
|
|
1041
|
+
"checkbox",
|
|
1042
|
+
"columnheader",
|
|
1043
|
+
"combobox",
|
|
1044
|
+
"complementary",
|
|
1045
|
+
"contentinfo",
|
|
1046
|
+
"definition",
|
|
1047
|
+
"dialog",
|
|
1048
|
+
"directory",
|
|
1049
|
+
"document",
|
|
1050
|
+
"feed",
|
|
1051
|
+
"figure",
|
|
1052
|
+
"form",
|
|
1053
|
+
"grid",
|
|
1054
|
+
"gridcell",
|
|
1055
|
+
"group",
|
|
1056
|
+
"heading",
|
|
1057
|
+
"img",
|
|
1058
|
+
"link",
|
|
1059
|
+
"list",
|
|
1060
|
+
"listbox",
|
|
1061
|
+
"listitem",
|
|
1062
|
+
"log",
|
|
1063
|
+
"main",
|
|
1064
|
+
"marquee",
|
|
1065
|
+
"math",
|
|
1066
|
+
"menu",
|
|
1067
|
+
"menubar",
|
|
1068
|
+
"menuitem",
|
|
1069
|
+
"menuitemcheckbox",
|
|
1070
|
+
"menuitemradio",
|
|
1071
|
+
"meter",
|
|
1072
|
+
"navigation",
|
|
1073
|
+
"none",
|
|
1074
|
+
"note",
|
|
1075
|
+
"option",
|
|
1076
|
+
"presentation",
|
|
1077
|
+
"progressbar",
|
|
1078
|
+
"radio",
|
|
1079
|
+
"radiogroup",
|
|
1080
|
+
"region",
|
|
1081
|
+
"row",
|
|
1082
|
+
"rowgroup",
|
|
1083
|
+
"rowheader",
|
|
1084
|
+
"scrollbar",
|
|
1085
|
+
"search",
|
|
1086
|
+
"searchbox",
|
|
1087
|
+
"separator",
|
|
1088
|
+
"slider",
|
|
1089
|
+
"spinbutton",
|
|
1090
|
+
"status",
|
|
1091
|
+
"switch",
|
|
1092
|
+
"tab",
|
|
1093
|
+
"table",
|
|
1094
|
+
"tablist",
|
|
1095
|
+
"tabpanel",
|
|
1096
|
+
"term",
|
|
1097
|
+
"textbox",
|
|
1098
|
+
"timer",
|
|
1099
|
+
"toolbar",
|
|
1100
|
+
"tooltip",
|
|
1101
|
+
"tree",
|
|
1102
|
+
"treegrid",
|
|
1103
|
+
"treeitem"
|
|
1104
|
+
]);
|
|
1105
|
+
var INTERACTIVE_ELEMENTS = /* @__PURE__ */ new Set([
|
|
1106
|
+
"a",
|
|
1107
|
+
"button",
|
|
1108
|
+
"input",
|
|
1109
|
+
"select",
|
|
1110
|
+
"textarea",
|
|
1111
|
+
"details",
|
|
1112
|
+
"summary"
|
|
1113
|
+
]);
|
|
1114
|
+
function getAttr(node, name) {
|
|
1115
|
+
return node.attrs.find((a) => a.name === name);
|
|
1116
|
+
}
|
|
1117
|
+
function hasAttr(node, name) {
|
|
1118
|
+
return node.attrs.some((a) => a.name === name);
|
|
1119
|
+
}
|
|
1120
|
+
function hasDirective(node, kind, arg) {
|
|
1121
|
+
return node.directives.some((d) => d.kind === kind && (arg === void 0 || d.arg === arg));
|
|
1122
|
+
}
|
|
1123
|
+
function hasBoundAttr(node, name) {
|
|
1124
|
+
return node.directives.some((d) => d.kind === "bind" && d.arg === name);
|
|
1125
|
+
}
|
|
1126
|
+
function hasTextContent(node) {
|
|
1127
|
+
return node.children.some(
|
|
1128
|
+
(c) => c.type === 2 /* Text */ && c.content.trim() !== "" || c.type === 3 /* Interpolation */
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
var rules = {
|
|
1132
|
+
"img-alt"(node, warnings) {
|
|
1133
|
+
if (node.tag !== "img") return;
|
|
1134
|
+
if (hasAttr(node, "alt") || hasBoundAttr(node, "alt")) return;
|
|
1135
|
+
if (hasAttr(node, "aria-label") || hasAttr(node, "aria-labelledby")) return;
|
|
1136
|
+
const role = getAttr(node, "role");
|
|
1137
|
+
if (role && (role.value === "presentation" || role.value === "none")) return;
|
|
1138
|
+
warnings.push({
|
|
1139
|
+
rule: "img-alt",
|
|
1140
|
+
message: "<img> element must have an alt attribute",
|
|
1141
|
+
tag: node.tag
|
|
1142
|
+
});
|
|
1143
|
+
},
|
|
1144
|
+
"click-keyboard"(node, warnings) {
|
|
1145
|
+
const hasClick = hasDirective(node, "on", "click");
|
|
1146
|
+
if (!hasClick) return;
|
|
1147
|
+
if (INTERACTIVE_ELEMENTS.has(node.tag)) return;
|
|
1148
|
+
const hasKeyboard = hasDirective(node, "on", "keydown") || hasDirective(node, "on", "keyup") || hasDirective(node, "on", "keypress");
|
|
1149
|
+
const role = getAttr(node, "role");
|
|
1150
|
+
const hasInteractiveRole = role && (role.value === "button" || role.value === "link");
|
|
1151
|
+
if (!hasKeyboard && !hasInteractiveRole) {
|
|
1152
|
+
warnings.push({
|
|
1153
|
+
rule: "click-keyboard",
|
|
1154
|
+
message: `<${node.tag}> with @click handler should also have a keyboard event handler or role="button"`,
|
|
1155
|
+
tag: node.tag
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
if (!hasAttr(node, "tabindex") && !hasBoundAttr(node, "tabindex")) {
|
|
1159
|
+
warnings.push({
|
|
1160
|
+
rule: "click-keyboard",
|
|
1161
|
+
message: `<${node.tag}> with @click handler should have tabindex="0" for keyboard accessibility`,
|
|
1162
|
+
tag: node.tag
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
},
|
|
1166
|
+
"anchor-content"(node, warnings) {
|
|
1167
|
+
if (node.tag !== "a") return;
|
|
1168
|
+
if (hasTextContent(node)) return;
|
|
1169
|
+
if (hasAttr(node, "aria-label") || hasAttr(node, "aria-labelledby")) return;
|
|
1170
|
+
const hasChildElements = node.children.some((c) => c.type === 1 /* Element */);
|
|
1171
|
+
if (hasChildElements) return;
|
|
1172
|
+
warnings.push({
|
|
1173
|
+
rule: "anchor-content",
|
|
1174
|
+
message: "<a> element must have content, aria-label, or aria-labelledby",
|
|
1175
|
+
tag: node.tag
|
|
1176
|
+
});
|
|
1177
|
+
},
|
|
1178
|
+
"form-label"(node, warnings) {
|
|
1179
|
+
const formElements = /* @__PURE__ */ new Set(["input", "select", "textarea"]);
|
|
1180
|
+
if (!formElements.has(node.tag)) return;
|
|
1181
|
+
const type = getAttr(node, "type");
|
|
1182
|
+
if (type && type.value === "hidden") return;
|
|
1183
|
+
if (hasAttr(node, "aria-label") || hasAttr(node, "aria-labelledby")) return;
|
|
1184
|
+
if (hasAttr(node, "id")) return;
|
|
1185
|
+
if (hasAttr(node, "title")) return;
|
|
1186
|
+
warnings.push({
|
|
1187
|
+
rule: "form-label",
|
|
1188
|
+
message: `<${node.tag}> should have an id (with matching <label>), aria-label, or aria-labelledby`,
|
|
1189
|
+
tag: node.tag
|
|
1190
|
+
});
|
|
1191
|
+
},
|
|
1192
|
+
"no-distracting"(node, warnings) {
|
|
1193
|
+
if (node.tag === "marquee" || node.tag === "blink") {
|
|
1194
|
+
warnings.push({
|
|
1195
|
+
rule: "no-distracting",
|
|
1196
|
+
message: `<${node.tag}> is distracting and inaccessible \u2014 do not use`,
|
|
1197
|
+
tag: node.tag
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
},
|
|
1201
|
+
"heading-order"(node, warnings, ctx) {
|
|
1202
|
+
const match = node.tag.match(HEADING_LEVEL_RE);
|
|
1203
|
+
if (!match) return;
|
|
1204
|
+
const level = parseInt(match[1], 10);
|
|
1205
|
+
if (ctx.lastHeadingLevel > 0 && level > ctx.lastHeadingLevel + 1) {
|
|
1206
|
+
warnings.push({
|
|
1207
|
+
rule: "heading-order",
|
|
1208
|
+
message: `Heading level <${node.tag}> skips from <h${ctx.lastHeadingLevel}> \u2014 headings should not skip levels`,
|
|
1209
|
+
tag: node.tag
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
ctx.lastHeadingLevel = level;
|
|
1213
|
+
},
|
|
1214
|
+
"aria-role"(node, warnings) {
|
|
1215
|
+
const role = getAttr(node, "role");
|
|
1216
|
+
if (!role || !role.value) return;
|
|
1217
|
+
if (!VALID_ARIA_ROLES.has(role.value)) {
|
|
1218
|
+
warnings.push({
|
|
1219
|
+
rule: "aria-role",
|
|
1220
|
+
message: `Invalid ARIA role "${role.value}" on <${node.tag}>`,
|
|
1221
|
+
tag: node.tag
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
},
|
|
1225
|
+
"no-positive-tabindex"(node, warnings) {
|
|
1226
|
+
const tabindex = getAttr(node, "tabindex");
|
|
1227
|
+
if (!tabindex || !tabindex.value) return;
|
|
1228
|
+
const val = parseInt(tabindex.value, 10);
|
|
1229
|
+
if (!isNaN(val) && val > 0) {
|
|
1230
|
+
warnings.push({
|
|
1231
|
+
rule: "no-positive-tabindex",
|
|
1232
|
+
message: `Avoid positive tabindex="${tabindex.value}" \u2014 it disrupts natural tab order`,
|
|
1233
|
+
tag: node.tag
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
},
|
|
1237
|
+
"media-captions"(node, warnings) {
|
|
1238
|
+
if (node.tag !== "video" && node.tag !== "audio") return;
|
|
1239
|
+
const hasTrack = node.children.some(
|
|
1240
|
+
(c) => c.type === 1 /* Element */ && c.tag === "track"
|
|
1241
|
+
);
|
|
1242
|
+
if (hasTrack) return;
|
|
1243
|
+
if (hasAttr(node, "aria-label") || hasAttr(node, "aria-labelledby")) return;
|
|
1244
|
+
warnings.push({
|
|
1245
|
+
rule: "media-captions",
|
|
1246
|
+
message: `<${node.tag}> should have a <track> element for captions`,
|
|
1247
|
+
tag: node.tag
|
|
1248
|
+
});
|
|
1249
|
+
},
|
|
1250
|
+
"anchor-valid"(node, warnings) {
|
|
1251
|
+
if (node.tag !== "a") return;
|
|
1252
|
+
if (hasAttr(node, "href") || hasBoundAttr(node, "href")) return;
|
|
1253
|
+
const role = getAttr(node, "role");
|
|
1254
|
+
if (role && role.value === "button") return;
|
|
1255
|
+
warnings.push({
|
|
1256
|
+
rule: "anchor-valid",
|
|
1257
|
+
message: "<a> element should have an href attribute",
|
|
1258
|
+
tag: node.tag
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
};
|
|
1262
|
+
function walkNodes(nodes, enabledRules, warnings, ctx) {
|
|
1263
|
+
for (const node of nodes) {
|
|
1264
|
+
if (node.type === 1 /* Element */) {
|
|
1265
|
+
for (const rule of enabledRules) {
|
|
1266
|
+
rule(node, warnings, ctx);
|
|
1267
|
+
}
|
|
1268
|
+
walkNodes(node.children, enabledRules, warnings, ctx);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
function checkA11y(ast, options) {
|
|
1273
|
+
const disabled = new Set(options?.disable ?? []);
|
|
1274
|
+
const enabledRules = Object.entries(rules).filter(([id]) => !disabled.has(id)).map(([, fn]) => fn);
|
|
1275
|
+
const warnings = [];
|
|
1276
|
+
const ctx = { lastHeadingLevel: 0 };
|
|
1277
|
+
walkNodes(ast, enabledRules, warnings, ctx);
|
|
1278
|
+
return warnings;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
980
1281
|
// src/index.ts
|
|
981
1282
|
function compile(source, options = {}) {
|
|
982
1283
|
const filename = options.filename ?? "anonymous.utopia";
|
|
@@ -995,11 +1296,16 @@ function compile(source, options = {}) {
|
|
|
995
1296
|
scopeId = styleResult.scopeId;
|
|
996
1297
|
}
|
|
997
1298
|
let renderModule = "";
|
|
1299
|
+
let a11yWarnings = [];
|
|
998
1300
|
if (descriptor.template) {
|
|
999
1301
|
const templateResult = compileTemplate(descriptor.template.content, {
|
|
1000
1302
|
scopeId: scopeId ?? void 0
|
|
1001
1303
|
});
|
|
1002
1304
|
renderModule = templateResult.code;
|
|
1305
|
+
if (options.a11y !== false) {
|
|
1306
|
+
const ast = parseTemplate(descriptor.template.content);
|
|
1307
|
+
a11yWarnings = checkA11y(ast, options.a11y ?? void 0);
|
|
1308
|
+
}
|
|
1003
1309
|
}
|
|
1004
1310
|
const { imports, body } = splitModuleParts(renderModule);
|
|
1005
1311
|
const scriptContent = descriptor.script?.content ?? "";
|
|
@@ -1015,7 +1321,7 @@ function compile(source, options = {}) {
|
|
|
1015
1321
|
}
|
|
1016
1322
|
parts.push("export default { render: __render }");
|
|
1017
1323
|
const code = parts.join("\n\n") + "\n";
|
|
1018
|
-
return { code, css };
|
|
1324
|
+
return { code, css, a11y: a11yWarnings };
|
|
1019
1325
|
}
|
|
1020
1326
|
function splitModuleParts(moduleCode) {
|
|
1021
1327
|
const idx = moduleCode.indexOf("\n\n");
|
|
@@ -1028,6 +1334,7 @@ function splitModuleParts(moduleCode) {
|
|
|
1028
1334
|
}
|
|
1029
1335
|
export {
|
|
1030
1336
|
SFCParseError,
|
|
1337
|
+
checkA11y,
|
|
1031
1338
|
compile,
|
|
1032
1339
|
compileStyle,
|
|
1033
1340
|
compileTemplate,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthesketh/utopia-compiler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Compiler for .utopia single-file components",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"dist"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@matthesketh/utopia-core": "0.
|
|
42
|
+
"@matthesketh/utopia-core": "0.5.0"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
45
|
"build": "tsup src/index.ts --format esm,cjs --dts",
|