@letsrunit/gherkin 0.1.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.
@@ -0,0 +1,172 @@
1
+ // @ts-ignore
2
+ import { parse } from './parser'; // generated by peggy from parser.peggy
3
+
4
+ // ---------- AST Types (mirror the grammar; keep minimal) ----------
5
+
6
+ type RoleSelector = {
7
+ type: 'Selector';
8
+ mode: 'role';
9
+ role: string; // any ARIA role, or the specials: field | image | text
10
+ name: string | null; // the quoted name after role
11
+ };
12
+
13
+ type TagSelector = {
14
+ type: 'Selector';
15
+ mode: 'tag';
16
+ tag: string; // e.g., section, form, button
17
+ };
18
+
19
+ type RawSelector = {
20
+ type: 'Selector';
21
+ mode: 'raw';
22
+ raw: string; // backticked Playwright selector, without the backticks
23
+ };
24
+
25
+ type DateSelector = {
26
+ type: 'Selector';
27
+ mode: 'date';
28
+ name: string;
29
+ };
30
+
31
+ type Selector = RoleSelector | TagSelector | RawSelector | DateSelector;
32
+
33
+ type Predicate = { type: 'HasDescendant'; selector: Selector };
34
+
35
+ type WithExpr = {
36
+ type: 'With';
37
+ selector: Selector;
38
+ include: Predicate[]; // from `with <selector>`
39
+ exclude: Predicate[]; // from `without <selector>`
40
+ };
41
+
42
+ type Expr = {
43
+ type: 'Within';
44
+ base: WithExpr;
45
+ ancestors: Selector[]; // outermost → innermost containers from chained `within`
46
+ };
47
+
48
+ // ---------- Escapers: same semantics as your snippet ----------
49
+ function escapeRegexForSelector(re: RegExp): string {
50
+ // keep unicode flags as-is; otherwise escape quotes and >> combinators
51
+ // @ts-ignore - .unicodeSets may exist on some runtimes
52
+ if (re.unicode || (re as any).unicodeSets) return String(re);
53
+ return String(re)
54
+ .replace(/(^|[^\\])(\\\\)*(["'`])/g, '$1$2\\$3')
55
+ .replace(/>>/g, '\\>\\>');
56
+ }
57
+
58
+ function escapeForTextSelector(text: string | RegExp, exact = false): string {
59
+ if (typeof text !== 'string') return escapeRegexForSelector(text);
60
+ return `${JSON.stringify(text)}${exact ? 's' : 'i'}`;
61
+ }
62
+
63
+ function escapeForAttributeSelector(value: string | RegExp, exact = false): string {
64
+ if (typeof value !== 'string') return escapeRegexForSelector(value);
65
+ return `"${value.trim().replace(/\\/g, '\\\\').replace(/["]/g, '\\"')}"${exact ? 's' : 'i'}`;
66
+ }
67
+
68
+ // ---------- Builders for internal engines ----------
69
+ function getByAttributeTextSelector(attrName: string, text: string | RegExp, props = ''): string {
70
+ return `[${attrName}=${escapeForAttributeSelector(text)}]` + (props ? ` ${props}` : '');
71
+ }
72
+
73
+ function getByAltTextSelector(text: string | RegExp, props = ''): string {
74
+ return getByAttributeTextSelector('alt', text, props);
75
+ }
76
+
77
+ function getByFieldSelector(text: string | RegExp, props = ''): string {
78
+ const q = escapeForTextSelector(text);
79
+ return `field=${q}${props ? ` ${props}` : ''}`;
80
+ }
81
+
82
+ function getByTextSelector(text: string | RegExp, props = ''): string {
83
+ return 'text=' + escapeForTextSelector(text) + (props ? ` ${props}` : '');
84
+ }
85
+
86
+ function getByDateSelector(text: string, props = ''): string {
87
+ return 'date=' + text.trim() + (props ? ` ${props}` : '');
88
+ }
89
+
90
+ function getByRoleSelector(role: string, name: string | RegExp = '', props = ''): string {
91
+ return `role=${role}` + (name ? ` [name=${escapeForAttributeSelector(name)}]` : '') + (props ? ` ${props}` : '');
92
+ }
93
+
94
+ // ---------- Core compile helpers ----------
95
+ /**
96
+ * Compile a base selector (no predicates, no ancestors).
97
+ */
98
+ function compileBaseSelector(sel: Selector): string {
99
+ switch (sel.mode) {
100
+ case 'raw':
101
+ return sel.raw;
102
+ case 'tag':
103
+ return sel.tag;
104
+ case 'date':
105
+ return getByDateSelector(sel.name);
106
+ case 'role': {
107
+ const r = sel.role.toLowerCase();
108
+ if (r === 'field') {
109
+ if (!sel.name) throw new Error('`field` requires a label string: field "Name"');
110
+ return getByFieldSelector(sel.name);
111
+ }
112
+ if (r === 'image') {
113
+ if (!sel.name) throw new Error('`image` requires an alt string: image "Logo"');
114
+ return getByAltTextSelector(sel.name);
115
+ }
116
+ if (r === 'text') {
117
+ if (!sel.name) throw new Error('`text` requires a string: text "Hello"');
118
+ return getByTextSelector(sel.name);
119
+ }
120
+ // Generic ARIA role
121
+ return getByRoleSelector(r, sel.name ?? '');
122
+ }
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Compile a predicate into ` >> has=...` or ` >> has-not=...`.
128
+ * We always use the general `has` flavor and stringify the *inner* selector chain,
129
+ * which matches the constructor pattern you showed.
130
+ */
131
+ function compilePredicates(include: Predicate[], exclude: Predicate[]): string {
132
+ const parts: string[] = [];
133
+
134
+ for (const p of include) {
135
+ const inner = compileBaseSelector(p.selector);
136
+ parts.push(`>> has="${inner}"`);
137
+ }
138
+ for (const p of exclude) {
139
+ const inner = compileBaseSelector(p.selector);
140
+ parts.push(`>> has-not="${inner}"`);
141
+ }
142
+ return parts.length ? ' ' + parts.join(' ') : '';
143
+ }
144
+
145
+ /**
146
+ * Join a chain with Playwright’s engine combinator.
147
+ */
148
+ function joinChain(parts: string[]): string {
149
+ return parts.filter(Boolean).join(' >> ');
150
+ }
151
+
152
+ /**
153
+ * Public API: compile a DSL locator string into a Playwright selector string.
154
+ */
155
+ export function compileLocator(input: string): string {
156
+ const ast = parse(input) as Expr;
157
+
158
+ // 1) compile ancestor containers (outer → inner)
159
+ const chain: string[] = ast.ancestors.map(compileBaseSelector);
160
+
161
+ // 2) compile base
162
+ const base = compileBaseSelector(ast.base.selector);
163
+
164
+ // 3) add predicates to the *base* (they apply to the current rightmost selector)
165
+ const pred = compilePredicates(ast.base.include, ast.base.exclude);
166
+
167
+ // 4) build final chain
168
+ let selector = joinChain([...chain, base]);
169
+ if (pred) selector += ' ' + pred.trim();
170
+
171
+ return selector;
172
+ }
@@ -0,0 +1,2 @@
1
+ export * from './compile';
2
+ export * from './regexp';