rip-lang 3.13.119 → 3.13.121
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/README.md +11 -2
- package/docs/RIP-LANG.md +4 -0
- package/docs/dist/rip.js +257 -27
- package/docs/dist/rip.min.js +183 -183
- package/docs/dist/rip.min.js.br +0 -0
- package/docs/ui/accordion.rip +103 -0
- package/docs/ui/alert-dialog.rip +53 -0
- package/docs/ui/autocomplete.rip +115 -0
- package/docs/ui/avatar.rip +37 -0
- package/docs/ui/badge.rip +15 -0
- package/docs/ui/breadcrumb.rip +47 -0
- package/docs/ui/button-group.rip +26 -0
- package/docs/ui/button.rip +23 -0
- package/docs/ui/card.rip +25 -0
- package/docs/ui/carousel.rip +110 -0
- package/docs/ui/checkbox-group.rip +61 -0
- package/docs/ui/checkbox.rip +33 -0
- package/docs/ui/collapsible.rip +50 -0
- package/docs/ui/combobox.rip +130 -0
- package/docs/ui/context-menu.rip +88 -0
- package/docs/ui/date-picker.rip +206 -0
- package/docs/ui/dialog.rip +60 -0
- package/docs/ui/drawer.rip +58 -0
- package/docs/ui/editable-value.rip +82 -0
- package/docs/ui/field.rip +53 -0
- package/docs/ui/fieldset.rip +22 -0
- package/docs/ui/form.rip +39 -0
- package/docs/ui/grid.rip +901 -0
- package/docs/ui/hljs-rip.js +209 -0
- package/docs/ui/index.css +1797 -0
- package/docs/ui/index.html +2385 -0
- package/docs/ui/input-group.rip +28 -0
- package/docs/ui/input.rip +36 -0
- package/docs/ui/label.rip +16 -0
- package/docs/ui/menu.rip +134 -0
- package/docs/ui/menubar.rip +151 -0
- package/docs/ui/meter.rip +36 -0
- package/docs/ui/multi-select.rip +203 -0
- package/docs/ui/native-select.rip +33 -0
- package/docs/ui/nav-menu.rip +126 -0
- package/docs/ui/number-field.rip +162 -0
- package/docs/ui/otp-field.rip +89 -0
- package/docs/ui/pagination.rip +123 -0
- package/docs/ui/popover.rip +93 -0
- package/docs/ui/preview-card.rip +75 -0
- package/docs/ui/progress.rip +25 -0
- package/docs/ui/radio-group.rip +57 -0
- package/docs/ui/resizable.rip +123 -0
- package/docs/ui/scroll-area.rip +145 -0
- package/docs/ui/select.rip +151 -0
- package/docs/ui/separator.rip +17 -0
- package/docs/ui/skeleton.rip +22 -0
- package/docs/ui/slider.rip +165 -0
- package/docs/ui/spinner.rip +17 -0
- package/docs/ui/table.rip +27 -0
- package/docs/ui/tabs.rip +113 -0
- package/docs/ui/textarea.rip +48 -0
- package/docs/ui/toast.rip +87 -0
- package/docs/ui/toggle-group.rip +71 -0
- package/docs/ui/toggle.rip +24 -0
- package/docs/ui/toolbar.rip +38 -0
- package/docs/ui/tooltip.rip +85 -0
- package/package.json +1 -1
- package/src/compiler.js +24 -12
- package/src/components.js +43 -6
- package/src/grammar/grammar.rip +2 -2
- package/src/lexer.js +26 -0
- package/src/parser.js +2 -2
- package/src/sourcemap-utils.js +91 -0
- package/src/typecheck.js +33 -8
- package/src/ui.rip +118 -2
package/src/components.js
CHANGED
|
@@ -489,12 +489,14 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
489
489
|
}
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
+
let atLineStart = tag === 'IDENTIFIER' && (prevTag === 'INDENT' || prevTag === 'TERMINATOR' || prevTag === 'RENDER');
|
|
493
|
+
|
|
492
494
|
if (isClsxCallEnd) {
|
|
493
495
|
isTemplateElement = true;
|
|
494
496
|
} else if (tag === 'IDENTIFIER' && isTemplateTag(token[1]) && !isAfterControlFlow) {
|
|
495
497
|
isTemplateElement = true;
|
|
496
498
|
} else if (tag === 'IDENTIFIER' && !isAfterControlFlow) {
|
|
497
|
-
isTemplateElement = startsWithTag(tokens, i);
|
|
499
|
+
isTemplateElement = atLineStart || startsWithTag(tokens, i);
|
|
498
500
|
} else if (tag === 'PROPERTY' || tag === 'STRING' || tag === 'STRING_END' || tag === 'NUMBER' || tag === 'BOOL' || tag === 'CALL_END' || tag === ')' || tag === 'PRESENCE') {
|
|
499
501
|
isTemplateElement = startsWithTag(tokens, i);
|
|
500
502
|
}
|
|
@@ -513,7 +515,7 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
513
515
|
}
|
|
514
516
|
}
|
|
515
517
|
}
|
|
516
|
-
let isBareTag = isClsxCallEnd || (tag === 'IDENTIFIER' && isTemplateTag(token[1])) || isClassOrIdTail;
|
|
518
|
+
let isBareTag = isClsxCallEnd || (tag === 'IDENTIFIER' && (isTemplateTag(token[1]) || atLineStart)) || isClassOrIdTail;
|
|
517
519
|
|
|
518
520
|
if (isBareTag) {
|
|
519
521
|
let callStartToken = gen('CALL_START', '(', token);
|
|
@@ -804,6 +806,8 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
804
806
|
|
|
805
807
|
const sl = [];
|
|
806
808
|
sl.push('class {');
|
|
809
|
+
sl.push(' declare _root: Element | null;');
|
|
810
|
+
sl.push(' emit(name: string, detail?: any): void {}');
|
|
807
811
|
|
|
808
812
|
// Constructor — typed props for public state/readonly (matches DTS)
|
|
809
813
|
const propEntries = [];
|
|
@@ -892,6 +896,9 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
892
896
|
|
|
893
897
|
// Pre-scan render block for @event: @method bindings to type method params
|
|
894
898
|
const eventMethodTypes = new Map();
|
|
899
|
+
for (const [eventName, methodName] of autoEventHandlers) {
|
|
900
|
+
eventMethodTypes.set(methodName, eventName);
|
|
901
|
+
}
|
|
895
902
|
if (renderBlock) {
|
|
896
903
|
const scanEvents = (node) => {
|
|
897
904
|
if (!Array.isArray(node)) return;
|
|
@@ -929,7 +936,8 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
929
936
|
// Methods
|
|
930
937
|
for (const { name, func } of methods) {
|
|
931
938
|
if (Array.isArray(func) && (func[0] === '->' || func[0] === '=>')) {
|
|
932
|
-
|
|
939
|
+
let [, params, methodBody] = func;
|
|
940
|
+
if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(methodBody)) params = ['it'];
|
|
933
941
|
let paramStr = Array.isArray(params) ? params.map(p => this.formatParam(p)).join(', ') : '';
|
|
934
942
|
// Inject event type on untyped first param when method is bound to an event
|
|
935
943
|
const boundEvent = eventMethodTypes.get(name);
|
|
@@ -1063,7 +1071,8 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
1063
1071
|
constructions.push(` };`);
|
|
1064
1072
|
}
|
|
1065
1073
|
}
|
|
1066
|
-
} else if (typeof head === 'string' && head !== 'object' && head !== 'switch' && TEMPLATE_TAGS.has(head.split(/[.#]/)[0])
|
|
1074
|
+
} else if (typeof head === 'string' && head !== 'object' && head !== 'switch' && (TEMPLATE_TAGS.has(head.split(/[.#]/)[0]) ||
|
|
1075
|
+
(/^[a-z]/.test(head) && node.length > 1 && Array.isArray(node[1]) && (node[1][0] === '->' || node[1][0] === '=>')))) {
|
|
1067
1076
|
const tagName = head.split(/[.#]/)[0];
|
|
1068
1077
|
const iProps = extractIntrinsicProps(node.slice(1));
|
|
1069
1078
|
const tagLine = node.loc?.r;
|
|
@@ -1203,7 +1212,7 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
1203
1212
|
lines.push(' for (const key in this._rest) this._applyInheritedProp(this._inheritedEl, key, this._rest[key]);');
|
|
1204
1213
|
lines.push(' }');
|
|
1205
1214
|
lines.push(' _applyInheritedProp(el, key, value) {');
|
|
1206
|
-
lines.push(' if (!el || key === \'key\' || key === \'ref\' || key.startsWith(\'__bind_\')) return;');
|
|
1215
|
+
lines.push(' if (!el || key === \'key\' || key === \'ref\' || key === \'children\' || key.startsWith(\'__bind_\')) return;');
|
|
1207
1216
|
lines.push(' if (key[0] === \'@\') {');
|
|
1208
1217
|
lines.push(' const event = key.slice(1).split(\'.\')[0];');
|
|
1209
1218
|
lines.push(' this._restHandlers || (this._restHandlers = {});');
|
|
@@ -1251,7 +1260,8 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
1251
1260
|
// --- Methods ---
|
|
1252
1261
|
for (const { name, func } of methods) {
|
|
1253
1262
|
if (Array.isArray(func) && (func[0] === '->' || func[0] === '=>')) {
|
|
1254
|
-
|
|
1263
|
+
let [, params, methodBody] = func;
|
|
1264
|
+
if ((!params || (Array.isArray(params) && params.length === 0)) && this.containsIt(methodBody)) params = ['it'];
|
|
1255
1265
|
const paramStr = Array.isArray(params) ? params.map(p => this.formatParam(p)).join(', ') : '';
|
|
1256
1266
|
const transformed = this.reactiveMembers ? this.transformComponentMembers(methodBody) : methodBody;
|
|
1257
1267
|
const isAsync = this.containsAwait(methodBody);
|
|
@@ -1494,6 +1504,33 @@ export function installComponentSupport(CodeGenerator, Lexer) {
|
|
|
1494
1504
|
return slotVar;
|
|
1495
1505
|
}
|
|
1496
1506
|
|
|
1507
|
+
// Switch: convert to if/else-if chain for conditional rendering
|
|
1508
|
+
if (headStr === 'switch') {
|
|
1509
|
+
const disc = rest[0];
|
|
1510
|
+
const whens = rest[1] || [];
|
|
1511
|
+
const defaultCase = rest[2] || null;
|
|
1512
|
+
let chain = defaultCase;
|
|
1513
|
+
for (let i = whens.length - 1; i >= 0; i--) {
|
|
1514
|
+
const [, tests, body] = whens[i];
|
|
1515
|
+
let cond;
|
|
1516
|
+
if (disc === null) {
|
|
1517
|
+
cond = tests.length === 1 ? tests[0]
|
|
1518
|
+
: tests.reduce((a, t) => a ? ['||', a, t] : t, null);
|
|
1519
|
+
} else {
|
|
1520
|
+
cond = tests.length === 1 ? ['==', disc, tests[0]]
|
|
1521
|
+
: tests.map(t => ['==', disc, t]).reduce((a, c) => a ? ['||', a, c] : c, null);
|
|
1522
|
+
}
|
|
1523
|
+
chain = ['if', cond, body, chain];
|
|
1524
|
+
}
|
|
1525
|
+
if (chain) {
|
|
1526
|
+
if (Array.isArray(chain) && chain[0] === 'if') return this.generateConditional(chain);
|
|
1527
|
+
return this.generateTemplateBlock(chain);
|
|
1528
|
+
}
|
|
1529
|
+
const cv = this.newElementVar('c');
|
|
1530
|
+
this._createLines.push(`${cv} = document.createComment('switch');`);
|
|
1531
|
+
return cv;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1497
1534
|
// HTML tag (possibly with #id, e.g. div#content)
|
|
1498
1535
|
if (headStr && this.isHtmlTag(headStr) && !meta(head, 'text')) {
|
|
1499
1536
|
let [tagName, id] = headStr.split('#');
|
package/src/grammar/grammar.rip
CHANGED
|
@@ -772,8 +772,8 @@ grammar =
|
|
|
772
772
|
o 'IMPORT ImportNamespaceSpecifier FROM String' , '["import", 2, 4]'
|
|
773
773
|
o 'IMPORT { } FROM String' , '["import", "{}", 5]'
|
|
774
774
|
o 'IMPORT { ImportSpecifierList OptComma } FROM String' , '["import", 3, 7]'
|
|
775
|
-
o 'IMPORT ImportDefaultSpecifier , ImportNamespaceSpecifier FROM String' , '["import",
|
|
776
|
-
o 'IMPORT ImportDefaultSpecifier , { ImportSpecifierList OptComma } FROM String', '["import",
|
|
775
|
+
o 'IMPORT ImportDefaultSpecifier , ImportNamespaceSpecifier FROM String' , '["import", 2, 4, 6]'
|
|
776
|
+
o 'IMPORT ImportDefaultSpecifier , { ImportSpecifierList OptComma } FROM String', '["import", 2, 5, 9]'
|
|
777
777
|
]
|
|
778
778
|
|
|
779
779
|
ImportSpecifierList: [
|
package/src/lexer.js
CHANGED
|
@@ -1638,6 +1638,26 @@ export class Lexer {
|
|
|
1638
1638
|
return forward(1);
|
|
1639
1639
|
}
|
|
1640
1640
|
|
|
1641
|
+
// Dotted keys: collapse `IDENTIFIER . PROPERTY . PROPERTY` into a STRING
|
|
1642
|
+
if (tokens[i - 1]?.[0] === 'PROPERTY' && tokens[i - 2]?.[0] === '.') {
|
|
1643
|
+
let j = i - 2;
|
|
1644
|
+
while (j >= 2 && tokens[j]?.[0] === '.' && (tokens[j - 1]?.[0] === 'PROPERTY' || tokens[j - 1]?.[0] === 'IDENTIFIER')) {
|
|
1645
|
+
j -= 2;
|
|
1646
|
+
}
|
|
1647
|
+
j += 1;
|
|
1648
|
+
if (tokens[j]?.[0] === 'IDENTIFIER' || tokens[j]?.[0] === 'PROPERTY') {
|
|
1649
|
+
let parts = [];
|
|
1650
|
+
for (let k = j; k < i; k += 2) parts.push(tokens[k][1]);
|
|
1651
|
+
let str = gen('STRING', `"${parts.join('.')}"`, tokens[j]);
|
|
1652
|
+
str.pre = tokens[j].pre;
|
|
1653
|
+
str.spaced = tokens[j].spaced;
|
|
1654
|
+
str.newLine = tokens[j].newLine;
|
|
1655
|
+
str.loc = tokens[j].loc;
|
|
1656
|
+
tokens.splice(j, i - j, str);
|
|
1657
|
+
i = j + 1;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1641
1661
|
// Find the start of this key
|
|
1642
1662
|
let s = EXPRESSION_END.has(this.tokens[i - 1]?.[0]) ? stack[stack.length - 1]?.[1] ?? i - 1 : i - 1;
|
|
1643
1663
|
if (this.tokens[i - 2]?.[0] === '@') s = i - 2;
|
|
@@ -1825,6 +1845,12 @@ export class Lexer {
|
|
|
1825
1845
|
if (!this.tokens[j]) return false;
|
|
1826
1846
|
if (this.tokens[j]?.[0] === '@' && this.tokens[j + 2]?.[0] === ':') return true;
|
|
1827
1847
|
if (this.tokens[j + 1]?.[0] === ':') return true;
|
|
1848
|
+
// Dotted keys: IDENTIFIER . PROPERTY ... :
|
|
1849
|
+
if ((this.tokens[j]?.[0] === 'IDENTIFIER' || this.tokens[j]?.[0] === 'PROPERTY') && this.tokens[j + 1]?.[0] === '.') {
|
|
1850
|
+
let k = j + 2;
|
|
1851
|
+
while (this.tokens[k]?.[0] === 'PROPERTY' && this.tokens[k + 1]?.[0] === '.') k += 2;
|
|
1852
|
+
if (this.tokens[k]?.[0] === 'PROPERTY' && this.tokens[k + 1]?.[0] === ':') return true;
|
|
1853
|
+
}
|
|
1828
1854
|
if (EXPRESSION_START.has(this.tokens[j]?.[0])) {
|
|
1829
1855
|
let end = null;
|
|
1830
1856
|
this.detectEnd(j + 1,
|
package/src/parser.js
CHANGED
|
@@ -189,8 +189,8 @@ const parserInstance = {
|
|
|
189
189
|
case 327: case 330: return ["import", "{}", $[$0]];
|
|
190
190
|
case 328: case 329: return ["import", $[$0-2], $[$0]];
|
|
191
191
|
case 331: return ["import", $[$0-4], $[$0]];
|
|
192
|
-
case 332: return ["import",
|
|
193
|
-
case 333: return ["import",
|
|
192
|
+
case 332: return ["import", $[$0-4], $[$0-2], $[$0]];
|
|
193
|
+
case 333: return ["import", $[$0-7], $[$0-4], $[$0]];
|
|
194
194
|
case 344: return ["*", $[$0]];
|
|
195
195
|
case 345: return ["export", "{}"];
|
|
196
196
|
case 346: return ["export", $[$0-2]];
|
package/src/sourcemap-utils.js
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
// Shared source-map position utilities used by both the CLI type-checker
|
|
2
2
|
// (src/typecheck.js) and the VS Code language server (packages/vscode/src/lsp.js).
|
|
3
3
|
|
|
4
|
+
// When a switch expression is the implicit return of a function, the compiler
|
|
5
|
+
// wraps it in an IIFE: `return (() => { switch ... })()`. Non-exhaustive
|
|
6
|
+
// switches produce TS2322 on the IIFE return, which source-maps to the `switch`
|
|
7
|
+
// line. This helper detects that pattern and remaps the diagnostic to the
|
|
8
|
+
// function's return-type annotation (matching TS behaviour on the raw .ts).
|
|
9
|
+
// Returns { line, col, len } if remapped, or null if no adjustment needed.
|
|
10
|
+
export function adjustSwitchDiagnostic(source, pos, code) {
|
|
11
|
+
if (code !== 2322) return null;
|
|
12
|
+
const srcLines = source.split('\n');
|
|
13
|
+
const line = srcLines[pos.line] || '';
|
|
14
|
+
if (!/^\s*switch\b/.test(line)) return null;
|
|
15
|
+
|
|
16
|
+
const switchIndent = line.match(/^(\s*)/)[1].length;
|
|
17
|
+
for (let i = pos.line - 1; i >= 0; i--) {
|
|
18
|
+
const defLine = srcLines[i];
|
|
19
|
+
const defMatch = defLine.match(/^(\s*)def\b/);
|
|
20
|
+
if (defMatch && defMatch[1].length < switchIndent) {
|
|
21
|
+
// Found enclosing function — look for return-type annotation "):: Type"
|
|
22
|
+
const retMatch = defLine.match(/\)\s*::\s*(\w+)\s*$/);
|
|
23
|
+
if (retMatch) {
|
|
24
|
+
const typeStart = defLine.lastIndexOf(retMatch[1]);
|
|
25
|
+
return { line: i, col: typeStart, len: retMatch[1].length };
|
|
26
|
+
}
|
|
27
|
+
return { line: i, col: defMatch[1].length, len: 3 }; // fallback: highlight `def`
|
|
28
|
+
}
|
|
29
|
+
// Stop if we leave the indentation context
|
|
30
|
+
if (/\S/.test(defLine) && !defLine.match(/^(\s*)/)[1].length && !/^\s*#/.test(defLine)) break;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
4
35
|
export function getLineText(text, lineNum) {
|
|
5
36
|
let start = 0, line = 0;
|
|
6
37
|
for (let i = 0; i <= text.length; i++) {
|
|
@@ -73,6 +104,22 @@ export function mapToSourcePos(entry, offset) {
|
|
|
73
104
|
const srcLines = entry.source.split('\n');
|
|
74
105
|
const re = new RegExp('\\b' + word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
|
|
75
106
|
|
|
107
|
+
// For let/var declarations, the error word may appear on many source lines
|
|
108
|
+
// (e.g. `Status` referenced in multiple variable annotations). Narrow the
|
|
109
|
+
// search to the source line that declares the same variable.
|
|
110
|
+
const letMatch = genLineText.match(/^(?:export\s+)?(?:declare\s+)?(?:let|var)\s+(\w+)/);
|
|
111
|
+
if (letMatch) {
|
|
112
|
+
const varName = letMatch[1];
|
|
113
|
+
const varRe = new RegExp('\\b' + varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*::');
|
|
114
|
+
for (let s = 0; s < srcLines.length; s++) {
|
|
115
|
+
if (varRe.test(srcLines[s])) {
|
|
116
|
+
const m = re.exec(srcLines[s]);
|
|
117
|
+
if (m) return { line: s, col: m.index };
|
|
118
|
+
return { line: s, col: 0 };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
76
123
|
// Find enclosing type/interface from DTS context to narrow search —
|
|
77
124
|
// without this, duplicate member names (e.g. "host" in two types) always
|
|
78
125
|
// resolve to the first occurrence in the source.
|
|
@@ -98,6 +145,50 @@ export function mapToSourcePos(entry, offset) {
|
|
|
98
145
|
return null;
|
|
99
146
|
}
|
|
100
147
|
|
|
148
|
+
// Hoisted multi-variable `let` declaration (e.g. `let a, b, items, ...;`) —
|
|
149
|
+
// the compiler aggregates variable declarations into one line with no useful
|
|
150
|
+
// per-variable source mapping. Detect the pattern (both top-level and inside
|
|
151
|
+
// functions), extract the word at the offset, and find its assignment in
|
|
152
|
+
// the Rip source. Use the genToSrc mapping of the preceding TS line (the
|
|
153
|
+
// function declaration) to scope the search and avoid matching a same-named
|
|
154
|
+
// variable in a different function.
|
|
155
|
+
const hoistLine = getLineText(entry.tsContent, tsLine);
|
|
156
|
+
if (/^\s*let\s+[$\w]+\s*,/.test(hoistLine) && entry.source) {
|
|
157
|
+
let hl = 0;
|
|
158
|
+
for (let i = 0; i < entry.tsContent.length; i++) {
|
|
159
|
+
if (hl === tsLine) { hl = i; break; }
|
|
160
|
+
if (entry.tsContent[i] === '\n') hl++;
|
|
161
|
+
}
|
|
162
|
+
const hCol = offset - hl;
|
|
163
|
+
const hWord = hoistLine.slice(hCol).match(/^[$\w]+/);
|
|
164
|
+
if (hWord) {
|
|
165
|
+
const word = hWord[0];
|
|
166
|
+
const srcLines = entry.source.split('\n');
|
|
167
|
+
const assignRe = new RegExp('^' + word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*(?:::|=!|:=|~=|=)');
|
|
168
|
+
|
|
169
|
+
// Scope the search: find the source line of the enclosing function by
|
|
170
|
+
// checking genToSrc for the TS line just before the hoisted let.
|
|
171
|
+
let searchStart = 0;
|
|
172
|
+
if (entry.genToSrc) {
|
|
173
|
+
for (let g = tsLine - 1; g >= 0; g--) {
|
|
174
|
+
const s = entry.genToSrc.get(g);
|
|
175
|
+
if (s !== undefined) { searchStart = s; break; }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
for (let s = searchStart; s < srcLines.length; s++) {
|
|
180
|
+
if (assignRe.test(srcLines[s].trimStart())) {
|
|
181
|
+
const col = srcLines[s].indexOf(word);
|
|
182
|
+
if (col >= 0) return { line: s, col };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Variable is on a hoisted let but has no recognisable assignment in
|
|
186
|
+
// the source (e.g. for-loop iterators, destructured names). Return
|
|
187
|
+
// null so callers skip it rather than producing a garbage mapping.
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
101
192
|
// Resolve source line from genToSrc
|
|
102
193
|
let srcLine = entry.genToSrc.get(tsLine);
|
|
103
194
|
if (srcLine === undefined) {
|
package/src/typecheck.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { Compiler, getStdlibCode } from './compiler.js';
|
|
14
14
|
import { createRequire } from 'module';
|
|
15
15
|
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
16
|
-
import { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol } from './sourcemap-utils.js';
|
|
16
|
+
import { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol, adjustSwitchDiagnostic } from './sourcemap-utils.js';
|
|
17
17
|
import { resolve, relative, dirname } from 'path';
|
|
18
18
|
import { buildLineMap } from './sourcemaps.js';
|
|
19
19
|
|
|
@@ -22,7 +22,12 @@ import { buildLineMap } from './sourcemaps.js';
|
|
|
22
22
|
// Detect type annotations (:: followed by space or =) ignoring comments,
|
|
23
23
|
// string literals, and prototype syntax (Class::method).
|
|
24
24
|
export function hasTypeAnnotations(source) {
|
|
25
|
+
let inHeredoc = false;
|
|
25
26
|
return source.split('\n').some(line => {
|
|
27
|
+
// Track heredoc boundaries (''' or """)
|
|
28
|
+
const ticks = (line.match(/'''|"""/g) || []);
|
|
29
|
+
for (const t of ticks) inHeredoc = !inHeredoc;
|
|
30
|
+
if (inHeredoc) return false;
|
|
26
31
|
// Strip comment
|
|
27
32
|
line = line.replace(/#.*$/, '');
|
|
28
33
|
// Strip string literals (single and double quoted)
|
|
@@ -247,6 +252,22 @@ export function cleanDiagnosticMessage(msg) {
|
|
|
247
252
|
msg = msg.replace(/\b__ripEl\b/g, 'element');
|
|
248
253
|
// Deduplicate consecutive identical lines (unwrapping can collapse nested messages)
|
|
249
254
|
msg = msg.split('\n').filter((line, i, arr) => i === 0 || line.trim() !== arr[i - 1].trim()).join('\n');
|
|
255
|
+
// Remove redundant nested "Type 'X' is not assignable to type 'Y'" when
|
|
256
|
+
// the parent already says "Type 'X | Z' is not assignable to type 'Y'"
|
|
257
|
+
// and X is just one member of the union — the drill-down adds no information.
|
|
258
|
+
const lines = msg.split('\n');
|
|
259
|
+
if (lines.length >= 2) {
|
|
260
|
+
const parentMatch = lines[0].match(/^Type '(.+)' is not assignable to type '(.+)'\.$/);
|
|
261
|
+
if (parentMatch && parentMatch[1].includes(' | ')) {
|
|
262
|
+
const members = parentMatch[1].split(' | ').map(s => s.trim());
|
|
263
|
+
const filtered = lines.filter((line, i) => {
|
|
264
|
+
if (i === 0) return true;
|
|
265
|
+
const childMatch = line.trim().match(/^Type '(.+)' is not assignable to type '(.+)'\.$/);
|
|
266
|
+
return !(childMatch && childMatch[2] === parentMatch[2] && members.includes(childMatch[1]));
|
|
267
|
+
});
|
|
268
|
+
msg = filtered.join('\n');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
250
271
|
return msg;
|
|
251
272
|
}
|
|
252
273
|
|
|
@@ -843,7 +864,7 @@ export function compileForCheck(filePath, source, compiler) {
|
|
|
843
864
|
|
|
844
865
|
// ── Source mapping helpers (delegated to sourcemap-utils.js) ───────
|
|
845
866
|
|
|
846
|
-
export { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol } from './sourcemap-utils.js';
|
|
867
|
+
export { mapToSourcePos, offsetToLine, getLineText, findNearestWord, lineColToOffset, offsetToLineCol, adjustSwitchDiagnostic } from './sourcemap-utils.js';
|
|
847
868
|
|
|
848
869
|
// Map a TypeScript diagnostic offset back to a Rip source line number.
|
|
849
870
|
// Returns -1 if the offset falls in the DTS header.
|
|
@@ -957,15 +978,15 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
957
978
|
}
|
|
958
979
|
}
|
|
959
980
|
|
|
960
|
-
// Check for unresolved
|
|
981
|
+
// Check for unresolved relative imports in all files (not just typed ones)
|
|
961
982
|
const fileResults = [];
|
|
962
983
|
let totalErrors = 0, totalWarnings = 0;
|
|
963
|
-
for (const [fp,
|
|
964
|
-
|
|
965
|
-
const srcLines = entry.source.split('\n');
|
|
984
|
+
for (const [fp, source] of sourcesByPath) {
|
|
985
|
+
const srcLines = source.split('\n');
|
|
966
986
|
const errors = [];
|
|
967
987
|
for (let s = 0; s < srcLines.length; s++) {
|
|
968
|
-
|
|
988
|
+
if (/^\s*#/.test(srcLines[s])) continue;
|
|
989
|
+
const m = srcLines[s].match(/^(?:import|export)\b.*from\s+['"](\.\.?\/[^'"]+)['"]/);
|
|
969
990
|
if (!m) continue;
|
|
970
991
|
const imported = resolve(dirname(fp), m[1]);
|
|
971
992
|
if (!existsSync(imported)) {
|
|
@@ -1046,7 +1067,11 @@ export async function runCheck(targetDir, opts = {}) {
|
|
|
1046
1067
|
const pos = mapToSourcePos(entry, d.start);
|
|
1047
1068
|
if (!pos) continue;
|
|
1048
1069
|
|
|
1049
|
-
|
|
1070
|
+
// Remap IIFE-switch diagnostics to the enclosing function declaration
|
|
1071
|
+
const adj = adjustSwitchDiagnostic(entry.source, pos, d.code);
|
|
1072
|
+
if (adj) { pos.line = adj.line; pos.col = adj.col; }
|
|
1073
|
+
|
|
1074
|
+
const endPos = adj ? { line: adj.line, col: adj.col + adj.len } : (d.length ? mapToSourcePos(entry, d.start + d.length) : null);
|
|
1050
1075
|
const len = endPos && endPos.line === pos.line ? endPos.col - pos.col : 1;
|
|
1051
1076
|
|
|
1052
1077
|
const message = cleanDiagnosticMessage(ts.flattenDiagnosticMessageText(d.messageText, '\n'));
|
package/src/ui.rip
CHANGED
|
@@ -1075,6 +1075,18 @@ _ariaPopupDismiss = (open, popup, close, els = [], repos = null) ->
|
|
|
1075
1075
|
document.removeEventListener 'mousedown', onDown
|
|
1076
1076
|
window.removeEventListener 'scroll', onScroll, true
|
|
1077
1077
|
|
|
1078
|
+
# popupGuard — per-component reopen suppression for pointer-driven popup closes.
|
|
1079
|
+
# Use when the same gesture that closes a popup can otherwise refocus/reclick a
|
|
1080
|
+
# trigger and reopen it immediately on the tail end of that sequence.
|
|
1081
|
+
_ariaPopupGuard = (delay = 250) ->
|
|
1082
|
+
blockedUntil = 0
|
|
1083
|
+
{
|
|
1084
|
+
block: (ms = delay) ->
|
|
1085
|
+
blockedUntil = Date.now() + ms
|
|
1086
|
+
canOpen: ->
|
|
1087
|
+
Date.now() >= blockedUntil
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1078
1090
|
# bindPopover — sync reactive open state with native Popover API
|
|
1079
1091
|
# open: reactive boolean
|
|
1080
1092
|
# popover: element or lazy getter (=> el)
|
|
@@ -1093,6 +1105,18 @@ _ariaBindPopover = (open, popover, setOpen, source = null) ->
|
|
|
1093
1105
|
return unless Object.hasOwn(HTMLElement.prototype, 'togglePopover')
|
|
1094
1106
|
restoreEl = null
|
|
1095
1107
|
|
|
1108
|
+
syncState = (isOpen) ->
|
|
1109
|
+
if isOpen
|
|
1110
|
+
el.hidden = false
|
|
1111
|
+
try el.inert = false
|
|
1112
|
+
catch then null
|
|
1113
|
+
el.removeAttribute 'aria-hidden'
|
|
1114
|
+
else
|
|
1115
|
+
try el.inert = true
|
|
1116
|
+
catch then null
|
|
1117
|
+
el.setAttribute 'aria-hidden', 'true'
|
|
1118
|
+
el.hidden = true
|
|
1119
|
+
|
|
1096
1120
|
restoreFocus = ->
|
|
1097
1121
|
target = restoreEl
|
|
1098
1122
|
restoreEl = null
|
|
@@ -1109,7 +1133,9 @@ _ariaBindPopover = (open, popover, setOpen, source = null) ->
|
|
|
1109
1133
|
isOpen = e.newState is 'open'
|
|
1110
1134
|
if isOpen
|
|
1111
1135
|
restoreEl = get(source) or currentFocus()
|
|
1136
|
+
syncState(true)
|
|
1112
1137
|
else
|
|
1138
|
+
syncState(false)
|
|
1113
1139
|
restoreFocus()
|
|
1114
1140
|
setOpen?(isOpen)
|
|
1115
1141
|
el.addEventListener 'toggle', onToggle
|
|
@@ -1120,6 +1146,7 @@ _ariaBindPopover = (open, popover, setOpen, source = null) ->
|
|
|
1120
1146
|
src = get(source)
|
|
1121
1147
|
if desired
|
|
1122
1148
|
restoreEl = src or currentFocus()
|
|
1149
|
+
syncState(true)
|
|
1123
1150
|
opts = if src and desired then { force: desired, source: src } else { force: desired }
|
|
1124
1151
|
try
|
|
1125
1152
|
el.togglePopover(opts)
|
|
@@ -1127,6 +1154,8 @@ _ariaBindPopover = (open, popover, setOpen, source = null) ->
|
|
|
1127
1154
|
# If the element cannot be toggled right now (for example detached),
|
|
1128
1155
|
# keep runtime stable; effect will retry on the next reactive pass.
|
|
1129
1156
|
null
|
|
1157
|
+
else
|
|
1158
|
+
syncState(desired)
|
|
1130
1159
|
|
|
1131
1160
|
-> el.removeEventListener 'toggle', onToggle
|
|
1132
1161
|
|
|
@@ -1147,6 +1176,18 @@ _ariaBindDialog = (open, dialog, setOpen, dismissable = true) ->
|
|
|
1147
1176
|
return unless el?.showModal?
|
|
1148
1177
|
restoreEl = null
|
|
1149
1178
|
|
|
1179
|
+
syncState = (isOpen) ->
|
|
1180
|
+
if isOpen
|
|
1181
|
+
el.hidden = false
|
|
1182
|
+
try el.inert = false
|
|
1183
|
+
catch then null
|
|
1184
|
+
el.removeAttribute 'aria-hidden'
|
|
1185
|
+
else
|
|
1186
|
+
try el.inert = true
|
|
1187
|
+
catch then null
|
|
1188
|
+
el.setAttribute 'aria-hidden', 'true'
|
|
1189
|
+
el.hidden = true
|
|
1190
|
+
|
|
1150
1191
|
restoreFocus = ->
|
|
1151
1192
|
target = restoreEl
|
|
1152
1193
|
restoreEl = null
|
|
@@ -1167,16 +1208,20 @@ _ariaBindDialog = (open, dialog, setOpen, dismissable = true) ->
|
|
|
1167
1208
|
|
|
1168
1209
|
onClose = ->
|
|
1169
1210
|
setOpen?(false)
|
|
1211
|
+
syncState(false)
|
|
1170
1212
|
restoreFocus()
|
|
1171
1213
|
el.addEventListener 'cancel', onCancel
|
|
1172
1214
|
el.addEventListener 'close', onClose
|
|
1173
1215
|
|
|
1174
1216
|
if open and not el.open
|
|
1175
1217
|
restoreEl = currentFocus() unless restoreEl
|
|
1218
|
+
syncState(true)
|
|
1176
1219
|
try el.showModal()
|
|
1177
1220
|
catch then null
|
|
1178
|
-
if not open and el.open
|
|
1221
|
+
else if not open and el.open
|
|
1179
1222
|
el.close()
|
|
1223
|
+
else
|
|
1224
|
+
syncState(!!open)
|
|
1180
1225
|
|
|
1181
1226
|
->
|
|
1182
1227
|
el.removeEventListener 'cancel', onCancel
|
|
@@ -1262,10 +1307,81 @@ _ariaUnlockScroll = (instance) ->
|
|
|
1262
1307
|
document.body.style.width = ''
|
|
1263
1308
|
window.scrollTo 0, scrollY
|
|
1264
1309
|
|
|
1310
|
+
_ariaHasAnchor = do ->
|
|
1311
|
+
try
|
|
1312
|
+
return false unless document?.createElement
|
|
1313
|
+
anchor = document.createElement('div')
|
|
1314
|
+
floating = document.createElement('div')
|
|
1315
|
+
anchor.style.cssText = 'position:fixed;top:100px;left:100px;width:10px;height:10px;anchor-name:--probe'
|
|
1316
|
+
floating.style.cssText = 'position:fixed;inset:auto;margin:0;position-anchor:--probe;position-area:bottom start;width:10px;height:10px'
|
|
1317
|
+
document.body.appendChild(anchor)
|
|
1318
|
+
document.body.appendChild(floating)
|
|
1319
|
+
rect = floating.getBoundingClientRect()
|
|
1320
|
+
anchor.remove()
|
|
1321
|
+
floating.remove()
|
|
1322
|
+
rect.top > 50
|
|
1323
|
+
catch
|
|
1324
|
+
false
|
|
1325
|
+
|
|
1326
|
+
_ariaPosition = (trigger, floating, opts = {}) ->
|
|
1327
|
+
return unless trigger and floating
|
|
1328
|
+
placement = opts.placement ?? 'bottom start'
|
|
1329
|
+
offset = opts.offset ?? 4
|
|
1330
|
+
matchWidth = opts.matchWidth ?? false
|
|
1331
|
+
if _ariaHasAnchor
|
|
1332
|
+
name = "--anchor-#{floating.id or Math.random().toString(36).slice(2, 8)}"
|
|
1333
|
+
trigger.style.anchorName = name
|
|
1334
|
+
floating.style.positionAnchor = name
|
|
1335
|
+
floating.style.position = 'fixed'
|
|
1336
|
+
floating.style.inset = 'auto'
|
|
1337
|
+
floating.style.margin = '0'
|
|
1338
|
+
floating.style.positionArea = placement
|
|
1339
|
+
floating.style.positionTry = 'flip-block, flip-inline, flip-block flip-inline'
|
|
1340
|
+
floating.style.positionVisibility = 'anchors-visible'
|
|
1341
|
+
[side] = placement.split(' ')
|
|
1342
|
+
floating.style.marginTop = ''
|
|
1343
|
+
floating.style.marginBottom = ''
|
|
1344
|
+
floating.style.marginLeft = ''
|
|
1345
|
+
floating.style.marginRight = ''
|
|
1346
|
+
switch side
|
|
1347
|
+
when 'bottom' then floating.style.marginTop = "#{offset}px"
|
|
1348
|
+
when 'top' then floating.style.marginBottom = "#{offset}px"
|
|
1349
|
+
when 'left' then floating.style.marginRight = "#{offset}px"
|
|
1350
|
+
when 'right' then floating.style.marginLeft = "#{offset}px"
|
|
1351
|
+
floating.style.minWidth = 'anchor-size(width)' if matchWidth
|
|
1352
|
+
else
|
|
1353
|
+
rect = trigger.getBoundingClientRect()
|
|
1354
|
+
floating.style.position = 'fixed'
|
|
1355
|
+
floating.style.inset = 'auto'
|
|
1356
|
+
floating.style.margin = '0'
|
|
1357
|
+
[side, align] = placement.split(' ')
|
|
1358
|
+
align ??= 'start'
|
|
1359
|
+
switch side
|
|
1360
|
+
when 'bottom'
|
|
1361
|
+
floating.style.top = "#{rect.bottom + offset}px"
|
|
1362
|
+
when 'top'
|
|
1363
|
+
floating.style.bottom = "#{window.innerHeight - rect.top + offset}px"
|
|
1364
|
+
when 'left'
|
|
1365
|
+
floating.style.right = "#{window.innerWidth - rect.left + offset}px"
|
|
1366
|
+
when 'right'
|
|
1367
|
+
floating.style.left = "#{rect.right + offset}px"
|
|
1368
|
+
if side in ['bottom', 'top']
|
|
1369
|
+
switch align
|
|
1370
|
+
when 'start' then floating.style.left = "#{rect.left}px"
|
|
1371
|
+
when 'center' then floating.style.left = "#{rect.left + rect.width / 2}px"; floating.style.transform = 'translateX(-50%)'
|
|
1372
|
+
when 'end' then floating.style.right = "#{window.innerWidth - rect.right}px"
|
|
1373
|
+
else
|
|
1374
|
+
switch align
|
|
1375
|
+
when 'start' then floating.style.top = "#{rect.top}px"
|
|
1376
|
+
when 'center' then floating.style.top = "#{rect.top + rect.height / 2}px"; floating.style.transform = 'translateY(-50%)'
|
|
1377
|
+
when 'end' then floating.style.bottom = "#{window.innerHeight - rect.bottom}px"
|
|
1378
|
+
floating.style.minWidth = "#{rect.width}px" if matchWidth
|
|
1379
|
+
|
|
1265
1380
|
globalThis.__aria ??= {
|
|
1266
|
-
listNav: _ariaListNav, rovingNav: _ariaRovingNav, popupDismiss: _ariaPopupDismiss,
|
|
1381
|
+
listNav: _ariaListNav, rovingNav: _ariaRovingNav, popupDismiss: _ariaPopupDismiss, popupGuard: _ariaPopupGuard,
|
|
1267
1382
|
bindPopover: _ariaBindPopover, bindDialog: _ariaBindDialog,
|
|
1268
1383
|
positionBelow: _ariaPositionBelow, trapFocus: _ariaTrapFocus, wireAria: _ariaWireAria,
|
|
1269
1384
|
lockScroll: _ariaLockScroll, unlockScroll: _ariaUnlockScroll,
|
|
1385
|
+
position: _ariaPosition, hasAnchor: _ariaHasAnchor,
|
|
1270
1386
|
}
|
|
1271
1387
|
globalThis.ARIA ??= globalThis.__aria
|