rip-lang 3.0.2 → 3.1.1
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/CHANGELOG.md +47 -5
- package/README.md +26 -3
- package/docs/RIP-INTERNALS.md +5 -7
- package/docs/RIP-LANG.md +7 -2
- package/docs/dist/rip.browser.js +1670 -333
- package/docs/dist/rip.browser.min.js +298 -224
- package/docs/dist/rip.browser.min.js.br +0 -0
- package/package.json +6 -2
- package/src/browser.js +1 -1
- package/src/compiler.js +46 -5
- package/src/components.js +1239 -0
- package/src/grammar/grammar.rip +37 -1
- package/src/grammar/solar.rip +11 -11
- package/src/lexer.js +286 -2
- package/src/parser.js +216 -214
- package/src/repl.js +53 -1
- package/src/tags.js +62 -0
package/src/grammar/grammar.rip
CHANGED
|
@@ -80,6 +80,8 @@ grammar =
|
|
|
80
80
|
o 'For'
|
|
81
81
|
o 'Switch'
|
|
82
82
|
o 'Class'
|
|
83
|
+
o 'Component'
|
|
84
|
+
o 'Render'
|
|
83
85
|
o 'Throw'
|
|
84
86
|
o 'Yield'
|
|
85
87
|
o 'Def'
|
|
@@ -706,6 +708,39 @@ grammar =
|
|
|
706
708
|
o 'CLASS SimpleAssignable EXTENDS Expression Block', '["class", 2, 4, 5]'
|
|
707
709
|
]
|
|
708
710
|
|
|
711
|
+
# ============================================================================
|
|
712
|
+
# Components
|
|
713
|
+
# ============================================================================
|
|
714
|
+
|
|
715
|
+
# Component: expression-based anonymous class with render support.
|
|
716
|
+
# Usage: Counter = component
|
|
717
|
+
# @count := 0
|
|
718
|
+
# render
|
|
719
|
+
# div "Count: {@count}"
|
|
720
|
+
Component: [
|
|
721
|
+
o 'COMPONENT INDENT ComponentBody OUTDENT', '["component", null, ["block", ...3]]'
|
|
722
|
+
]
|
|
723
|
+
|
|
724
|
+
ComponentBody: [
|
|
725
|
+
o 'ComponentLine' , '[1]'
|
|
726
|
+
o 'ComponentBody TERMINATOR ComponentLine', '[...1, 3]'
|
|
727
|
+
o 'ComponentBody TERMINATOR'
|
|
728
|
+
]
|
|
729
|
+
|
|
730
|
+
ComponentLine: [
|
|
731
|
+
o 'Expression'
|
|
732
|
+
o 'ExpressionLine'
|
|
733
|
+
o 'Statement'
|
|
734
|
+
]
|
|
735
|
+
|
|
736
|
+
# Render block: template DSL for fine-grained reactive DOM.
|
|
737
|
+
# Usage: render
|
|
738
|
+
# div.card
|
|
739
|
+
# h1 "Hello"
|
|
740
|
+
Render: [
|
|
741
|
+
o 'RENDER Block', '["render", 2]'
|
|
742
|
+
]
|
|
743
|
+
|
|
709
744
|
# ============================================================================
|
|
710
745
|
# Import / Export
|
|
711
746
|
# ============================================================================
|
|
@@ -747,6 +782,7 @@ grammar =
|
|
|
747
782
|
o 'EXPORT { }' , '["export", "{}"]'
|
|
748
783
|
o 'EXPORT { ExportSpecifierList OptComma }' , '["export", 3]'
|
|
749
784
|
o 'EXPORT Class' , '["export", 2]'
|
|
785
|
+
o 'EXPORT Component' , '["export", 2]'
|
|
750
786
|
o 'EXPORT Def' , '["export", 2]'
|
|
751
787
|
o 'EXPORT Identifier = Expression' , '["export", ["=", 2, 4]]'
|
|
752
788
|
o 'EXPORT Identifier = TERMINATOR Expression' , '["export", ["=", 2, 5]]'
|
|
@@ -877,7 +913,7 @@ operators = """
|
|
|
877
913
|
right YIELD
|
|
878
914
|
right = : COMPOUND_ASSIGN RETURN THROW EXTENDS
|
|
879
915
|
right FORIN FOROF FORAS FORASAWAIT BY WHEN
|
|
880
|
-
right IF ELSE FOR WHILE UNTIL LOOP SUPER CLASS IMPORT EXPORT DYNAMIC_IMPORT
|
|
916
|
+
right IF ELSE FOR WHILE UNTIL LOOP SUPER CLASS COMPONENT RENDER IMPORT EXPORT DYNAMIC_IMPORT
|
|
881
917
|
left POST_IF
|
|
882
918
|
""".trim().split('\n').reverse().map (line) -> line.trim().split /\s+/
|
|
883
919
|
|
package/src/grammar/solar.rip
CHANGED
|
@@ -335,7 +335,7 @@ class Generator
|
|
|
335
335
|
|
|
336
336
|
# Single pass: group items by nextSymbol
|
|
337
337
|
symbolItems = new Map
|
|
338
|
-
for item
|
|
338
|
+
for item as itemSet.items when item.nextSymbol and item.nextSymbol isnt '$end'
|
|
339
339
|
items = symbolItems.get(item.nextSymbol)
|
|
340
340
|
unless items
|
|
341
341
|
items = []
|
|
@@ -343,7 +343,7 @@ class Generator
|
|
|
343
343
|
items.push(item)
|
|
344
344
|
|
|
345
345
|
# Process each symbol with its pre-collected items
|
|
346
|
-
for [symbol, items]
|
|
346
|
+
for [symbol, items] as symbolItems
|
|
347
347
|
@_insertStateWithItems symbol, items, itemSet, states, stateMap
|
|
348
348
|
|
|
349
349
|
@states = states
|
|
@@ -359,7 +359,7 @@ class Generator
|
|
|
359
359
|
newItems = new Set
|
|
360
360
|
|
|
361
361
|
# Only process item cores we haven't yet seen
|
|
362
|
-
for item
|
|
362
|
+
for item as workingSet when !itemCores.has(item.id)
|
|
363
363
|
|
|
364
364
|
# Add item to closure
|
|
365
365
|
closureSet.items.add(item)
|
|
@@ -392,7 +392,7 @@ class Generator
|
|
|
392
392
|
_goto: (itemSet, symbol) ->
|
|
393
393
|
gotoSet = new State
|
|
394
394
|
|
|
395
|
-
for item
|
|
395
|
+
for item as itemSet.items when item.nextSymbol is symbol
|
|
396
396
|
# Create advanced item (lookaheads will be set from FOLLOW sets later)
|
|
397
397
|
newItem = new Item item.rule, null, item.dot + 1
|
|
398
398
|
gotoSet.items.add newItem
|
|
@@ -522,11 +522,11 @@ class Generator
|
|
|
522
522
|
# Assign FOLLOW sets to reduction items
|
|
523
523
|
_assignItemLookaheads!: ->
|
|
524
524
|
for state in @states
|
|
525
|
-
for item
|
|
525
|
+
for item as state.reductions
|
|
526
526
|
follows = @types[item.rule.type]?.follows
|
|
527
527
|
if follows
|
|
528
528
|
item.lookaheads.clear()
|
|
529
|
-
item.lookaheads.add token for token
|
|
529
|
+
item.lookaheads.add token for token as follows
|
|
530
530
|
|
|
531
531
|
# ============================================================================
|
|
532
532
|
# Parse Table Generation
|
|
@@ -541,19 +541,19 @@ class Generator
|
|
|
541
541
|
state = states[k] = {}
|
|
542
542
|
|
|
543
543
|
# Shift and goto actions
|
|
544
|
-
for [stackSymbol, gotoState]
|
|
544
|
+
for [stackSymbol, gotoState] as itemSet.transitions when @symbolIds[stackSymbol]?
|
|
545
545
|
if types[stackSymbol]
|
|
546
546
|
state[@symbolIds[stackSymbol]] = gotoState
|
|
547
547
|
else
|
|
548
548
|
state[@symbolIds[stackSymbol]] = [SHIFT, gotoState]
|
|
549
549
|
|
|
550
550
|
# Accept action
|
|
551
|
-
for item
|
|
551
|
+
for item as itemSet.items when item.nextSymbol is "$end" and @symbolIds["$end"]?
|
|
552
552
|
state[@symbolIds["$end"]] = [ACCEPT]
|
|
553
553
|
|
|
554
554
|
# Reduce actions
|
|
555
|
-
for item
|
|
556
|
-
for stackSymbol
|
|
555
|
+
for item as itemSet.reductions
|
|
556
|
+
for stackSymbol as item.lookaheads when @symbolIds[stackSymbol]?
|
|
557
557
|
action = state[@symbolIds[stackSymbol]]
|
|
558
558
|
op = operators[stackSymbol]
|
|
559
559
|
|
|
@@ -964,7 +964,7 @@ if isRunAsScript
|
|
|
964
964
|
|
|
965
965
|
if grammar.grammar
|
|
966
966
|
parts.push ' (rules'
|
|
967
|
-
for [name, productions]
|
|
967
|
+
for [name, productions] as Object.entries(grammar.grammar)
|
|
968
968
|
parts.push " (#{name}"
|
|
969
969
|
for production in productions
|
|
970
970
|
[pattern, action, opts] = production
|
package/src/lexer.js
CHANGED
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
//
|
|
40
40
|
// ==========================================================================
|
|
41
41
|
|
|
42
|
+
import { TEMPLATE_TAGS } from './tags.js';
|
|
43
|
+
|
|
42
44
|
// ==========================================================================
|
|
43
45
|
// Token Category Sets
|
|
44
46
|
// ==========================================================================
|
|
@@ -59,6 +61,7 @@ let JS_KEYWORDS = new Set([
|
|
|
59
61
|
let RIP_KEYWORDS = new Set([
|
|
60
62
|
'undefined', 'Infinity', 'NaN',
|
|
61
63
|
'then', 'unless', 'until', 'loop', 'of', 'by', 'when', 'def',
|
|
64
|
+
'component', 'render',
|
|
62
65
|
]);
|
|
63
66
|
|
|
64
67
|
// Rip aliases: word → operator/value
|
|
@@ -158,7 +161,7 @@ let IMPLICIT_FUNC = new Set([
|
|
|
158
161
|
]);
|
|
159
162
|
|
|
160
163
|
// Control flow tokens that don't end implicit calls/objects
|
|
161
|
-
let CONTROL_IN_IMPLICIT = new Set(['IF', 'TRY', 'FINALLY', 'CATCH', 'CLASS', 'SWITCH']);
|
|
164
|
+
let CONTROL_IN_IMPLICIT = new Set(['IF', 'TRY', 'FINALLY', 'CATCH', 'CLASS', 'SWITCH', 'COMPONENT']);
|
|
162
165
|
|
|
163
166
|
// Single-liner keywords that get implicit INDENT/OUTDENT
|
|
164
167
|
let SINGLE_LINERS = new Set(['ELSE', '->', '=>', 'TRY', 'FINALLY', 'THEN']);
|
|
@@ -297,6 +300,8 @@ export class Lexer {
|
|
|
297
300
|
this.seenExport = false;
|
|
298
301
|
this.importSpecifierList = false;
|
|
299
302
|
this.exportSpecifierList = false;
|
|
303
|
+
this.inRenderBlock = false;
|
|
304
|
+
this.renderIndent = 0;
|
|
300
305
|
|
|
301
306
|
// Clean source
|
|
302
307
|
code = this.clean(code);
|
|
@@ -498,6 +503,18 @@ export class Lexer {
|
|
|
498
503
|
// Property vs identifier
|
|
499
504
|
if (colon || (prev && (prev[0] === '.' || prev[0] === '?.' || (!prev.spaced && prev[0] === '@')))) {
|
|
500
505
|
tag = 'PROPERTY';
|
|
506
|
+
|
|
507
|
+
// In render blocks, consume hyphenated CSS class names: .counter-display → PROPERTY "counter-display"
|
|
508
|
+
if (this.inRenderBlock && prev && prev[0] === '.' && !colon) {
|
|
509
|
+
let rest = this.chunk.slice(idLen);
|
|
510
|
+
while (rest[0] === '-' && /^-[a-zA-Z]/.test(rest)) {
|
|
511
|
+
let m = /^-([a-zA-Z][\w]*)/.exec(rest);
|
|
512
|
+
if (!m) break;
|
|
513
|
+
id += '-' + m[1];
|
|
514
|
+
idLen += 1 + m[1].length;
|
|
515
|
+
rest = this.chunk.slice(idLen);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
501
518
|
} else {
|
|
502
519
|
tag = 'IDENTIFIER';
|
|
503
520
|
}
|
|
@@ -551,6 +568,12 @@ export class Lexer {
|
|
|
551
568
|
// --- Emit ---
|
|
552
569
|
let t = this.emit(tag, id, {len: idLen, data: Object.keys(data).length ? data : null});
|
|
553
570
|
|
|
571
|
+
// Track render block context for line continuation suppression
|
|
572
|
+
if (tag === 'RENDER') {
|
|
573
|
+
this.inRenderBlock = true;
|
|
574
|
+
this.renderIndent = this.indent;
|
|
575
|
+
}
|
|
576
|
+
|
|
554
577
|
if (colon) {
|
|
555
578
|
this.emit(':', ':', {len: 1});
|
|
556
579
|
return idLen + colon.length;
|
|
@@ -687,6 +710,10 @@ export class Lexer {
|
|
|
687
710
|
|
|
688
711
|
// Emit OUTDENT tokens to reach target indent level
|
|
689
712
|
outdentTo(targetSize, outdentLength = 0) {
|
|
713
|
+
// Exit render block when outdenting past its level
|
|
714
|
+
if (this.inRenderBlock && targetSize <= this.renderIndent) {
|
|
715
|
+
this.inRenderBlock = false;
|
|
716
|
+
}
|
|
690
717
|
let moveOut = this.indent - targetSize;
|
|
691
718
|
while (moveOut > 0) {
|
|
692
719
|
let lastIndent = this.indents[this.indents.length - 1];
|
|
@@ -717,6 +744,10 @@ export class Lexer {
|
|
|
717
744
|
|
|
718
745
|
// Check if the current line is unfinished (continuation)
|
|
719
746
|
isUnfinished() {
|
|
747
|
+
// Inside render blocks, a line starting with . is a new element, not method chaining
|
|
748
|
+
if (this.inRenderBlock && LINE_CONTINUER_RE.test(this.chunk) && /^\s*\./.test(this.chunk)) {
|
|
749
|
+
return false;
|
|
750
|
+
}
|
|
720
751
|
return LINE_CONTINUER_RE.test(this.chunk) || UNFINISHED.has(this.prevTag());
|
|
721
752
|
}
|
|
722
753
|
|
|
@@ -1064,6 +1095,7 @@ export class Lexer {
|
|
|
1064
1095
|
// Reactive operators
|
|
1065
1096
|
else if (val === '~=') tag = 'COMPUTED_ASSIGN';
|
|
1066
1097
|
else if (val === ':=') tag = 'REACTIVE_ASSIGN';
|
|
1098
|
+
else if (val === '<=>') tag = 'BIND';
|
|
1067
1099
|
else if (val === '~>') tag = 'REACT_ASSIGN';
|
|
1068
1100
|
else if (val === '=!') tag = 'READONLY_ASSIGN';
|
|
1069
1101
|
// Export all
|
|
@@ -1134,7 +1166,7 @@ export class Lexer {
|
|
|
1134
1166
|
}
|
|
1135
1167
|
|
|
1136
1168
|
// ==========================================================================
|
|
1137
|
-
// Rewriter —
|
|
1169
|
+
// Rewriter — 8 passes
|
|
1138
1170
|
// ==========================================================================
|
|
1139
1171
|
|
|
1140
1172
|
rewrite(tokens) {
|
|
@@ -1143,6 +1175,7 @@ export class Lexer {
|
|
|
1143
1175
|
this.closeOpenCalls();
|
|
1144
1176
|
this.closeOpenIndexes();
|
|
1145
1177
|
this.normalizeLines();
|
|
1178
|
+
this.rewriteRender();
|
|
1146
1179
|
this.tagPostfixConditionals();
|
|
1147
1180
|
this.addImplicitBracesAndParens();
|
|
1148
1181
|
this.addImplicitCallCommas();
|
|
@@ -1196,6 +1229,7 @@ export class Lexer {
|
|
|
1196
1229
|
return token[1] !== ';' && SINGLE_CLOSERS.has(token[0]) &&
|
|
1197
1230
|
!(token[0] === 'TERMINATOR' && EXPRESSION_CLOSE.has(this.tokens[i + 1]?.[0])) &&
|
|
1198
1231
|
!(token[0] === 'ELSE' && starter !== 'THEN') ||
|
|
1232
|
+
token[0] === 'INDENT' && !token.generated && (starter === '->' || starter === '=>') ||
|
|
1199
1233
|
token[0] === ',' && (starter === '->' || starter === '=>') && !this.commaInImplicitCall(i) ||
|
|
1200
1234
|
CALL_CLOSERS.has(token[0]) && (this.tokens[i - 1]?.newLine || this.tokens[i - 1]?.[0] === 'OUTDENT');
|
|
1201
1235
|
};
|
|
@@ -1250,6 +1284,256 @@ export class Lexer {
|
|
|
1250
1284
|
});
|
|
1251
1285
|
}
|
|
1252
1286
|
|
|
1287
|
+
// =========================================================================
|
|
1288
|
+
// Render block rewriter
|
|
1289
|
+
// =========================================================================
|
|
1290
|
+
// Transforms template syntax inside render blocks:
|
|
1291
|
+
// - Implicit div for class-only selectors: .card → div.card
|
|
1292
|
+
// - Combine #id selectors: div # main → div#main
|
|
1293
|
+
// - Two-way binding: value <=> username → __bind_value__: username
|
|
1294
|
+
// - Event modifiers: @click.prevent: → [@click.prevent]:
|
|
1295
|
+
// - Dynamic classes: div.('card', x && 'active') → div.__cx__(...)
|
|
1296
|
+
// - Implicit nesting: inject -> before INDENT for template elements
|
|
1297
|
+
// - Hyphenated attributes: data-foo: "x" → "data-foo": "x"
|
|
1298
|
+
// =========================================================================
|
|
1299
|
+
rewriteRender() {
|
|
1300
|
+
let inRender = false;
|
|
1301
|
+
let renderIndentLevel = 0;
|
|
1302
|
+
let currentIndent = 0;
|
|
1303
|
+
let pendingCallEnds = [];
|
|
1304
|
+
|
|
1305
|
+
let isHtmlTag = (name) => {
|
|
1306
|
+
let tagPart = name.split('#')[0];
|
|
1307
|
+
return TEMPLATE_TAGS.has(tagPart);
|
|
1308
|
+
};
|
|
1309
|
+
|
|
1310
|
+
let isComponent = (name) => {
|
|
1311
|
+
if (!name || typeof name !== 'string') return false;
|
|
1312
|
+
return /^[A-Z]/.test(name);
|
|
1313
|
+
};
|
|
1314
|
+
|
|
1315
|
+
let isTemplateTag = (name) => {
|
|
1316
|
+
return isHtmlTag(name) || isComponent(name);
|
|
1317
|
+
};
|
|
1318
|
+
|
|
1319
|
+
let startsWithHtmlTag = (tokens, i) => {
|
|
1320
|
+
let j = i;
|
|
1321
|
+
while (j > 0) {
|
|
1322
|
+
let pt = tokens[j - 1][0];
|
|
1323
|
+
if (pt === 'INDENT' || pt === 'OUTDENT' || pt === 'TERMINATOR' || pt === 'RENDER' || pt === 'CALL_END' || pt === ')') {
|
|
1324
|
+
break;
|
|
1325
|
+
}
|
|
1326
|
+
j--;
|
|
1327
|
+
}
|
|
1328
|
+
return tokens[j] && tokens[j][0] === 'IDENTIFIER' && isHtmlTag(tokens[j][1]);
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
this.scanTokens(function(token, i, tokens) {
|
|
1332
|
+
let tag = token[0];
|
|
1333
|
+
let nextToken = i < tokens.length - 1 ? tokens[i + 1] : null;
|
|
1334
|
+
|
|
1335
|
+
// Track entering render blocks
|
|
1336
|
+
if (tag === 'RENDER') {
|
|
1337
|
+
inRender = true;
|
|
1338
|
+
renderIndentLevel = currentIndent + 1;
|
|
1339
|
+
return 1;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// Track indentation
|
|
1343
|
+
if (tag === 'INDENT') {
|
|
1344
|
+
currentIndent++;
|
|
1345
|
+
return 1;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (tag === 'OUTDENT') {
|
|
1349
|
+
currentIndent--;
|
|
1350
|
+
|
|
1351
|
+
// Insert pending CALL_END(s) after this OUTDENT
|
|
1352
|
+
let inserted = 0;
|
|
1353
|
+
while (pendingCallEnds.length > 0 && pendingCallEnds[pendingCallEnds.length - 1] > currentIndent) {
|
|
1354
|
+
let callEndToken = gen('CALL_END', ')', token);
|
|
1355
|
+
tokens.splice(i + 1 + inserted, 0, callEndToken);
|
|
1356
|
+
pendingCallEnds.pop();
|
|
1357
|
+
inserted++;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Exit render block when we outdent past where it started
|
|
1361
|
+
if (inRender && currentIndent < renderIndentLevel) {
|
|
1362
|
+
inRender = false;
|
|
1363
|
+
}
|
|
1364
|
+
return 1 + inserted;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// Only process if we're inside a render block
|
|
1368
|
+
if (!inRender) return 1;
|
|
1369
|
+
|
|
1370
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1371
|
+
// Hyphenated attributes
|
|
1372
|
+
// data-lucide: "search" → "data-lucide": "search"
|
|
1373
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1374
|
+
if (tag === 'IDENTIFIER' && !token.spaced) {
|
|
1375
|
+
let parts = [token[1]];
|
|
1376
|
+
let j = i + 1;
|
|
1377
|
+
while (j + 1 < tokens.length) {
|
|
1378
|
+
let hyphen = tokens[j];
|
|
1379
|
+
let nextPart = tokens[j + 1];
|
|
1380
|
+
if (hyphen[0] === '-' && !hyphen.spaced &&
|
|
1381
|
+
(nextPart[0] === 'IDENTIFIER' || nextPart[0] === 'PROPERTY')) {
|
|
1382
|
+
parts.push(nextPart[1]);
|
|
1383
|
+
j += 2;
|
|
1384
|
+
if (nextPart[0] === 'PROPERTY') break;
|
|
1385
|
+
} else {
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (parts.length > 1 && j > i + 1 && tokens[j - 1][0] === 'PROPERTY') {
|
|
1390
|
+
token[0] = 'STRING';
|
|
1391
|
+
token[1] = `"${parts.join('-')}"`;
|
|
1392
|
+
tokens.splice(i + 1, j - i - 1);
|
|
1393
|
+
return 1;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1398
|
+
// Implicit div for class-only selectors
|
|
1399
|
+
// .card → div.card
|
|
1400
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1401
|
+
if (tag === '.') {
|
|
1402
|
+
let prevToken = i > 0 ? tokens[i - 1] : null;
|
|
1403
|
+
let prevTag = prevToken ? prevToken[0] : null;
|
|
1404
|
+
if (prevTag === 'INDENT' || prevTag === 'TERMINATOR') {
|
|
1405
|
+
if (nextToken && nextToken[0] === 'PROPERTY') {
|
|
1406
|
+
let divToken = gen('IDENTIFIER', 'div', token);
|
|
1407
|
+
tokens.splice(i, 0, divToken);
|
|
1408
|
+
return 2;
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1414
|
+
// Combine #id selectors
|
|
1415
|
+
// div # main → div#main
|
|
1416
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1417
|
+
if (tag === 'IDENTIFIER' || tag === 'PROPERTY') {
|
|
1418
|
+
let next = tokens[i + 1];
|
|
1419
|
+
let nextNext = tokens[i + 2];
|
|
1420
|
+
if (next && next[0] === '#' && nextNext && nextNext[0] === 'PROPERTY') {
|
|
1421
|
+
token[1] = token[1] + '#' + nextNext[1];
|
|
1422
|
+
if (nextNext.spaced) token.spaced = true;
|
|
1423
|
+
tokens.splice(i + 1, 2);
|
|
1424
|
+
return 1;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1429
|
+
// Two-way binding
|
|
1430
|
+
// value <=> username → __bind_value__: username
|
|
1431
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1432
|
+
if (tag === 'BIND') {
|
|
1433
|
+
let prevToken = i > 0 ? tokens[i - 1] : null;
|
|
1434
|
+
let nextBindToken = tokens[i + 1];
|
|
1435
|
+
if (prevToken && (prevToken[0] === 'IDENTIFIER' || prevToken[0] === 'PROPERTY') &&
|
|
1436
|
+
nextBindToken && nextBindToken[0] === 'IDENTIFIER') {
|
|
1437
|
+
prevToken[1] = `__bind_${prevToken[1]}__`;
|
|
1438
|
+
token[0] = ':';
|
|
1439
|
+
token[1] = ':';
|
|
1440
|
+
return 1;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1445
|
+
// Event modifiers
|
|
1446
|
+
// @click.prevent: handler → [@click.prevent]: handler
|
|
1447
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1448
|
+
if (tag === '@') {
|
|
1449
|
+
let j = i + 1;
|
|
1450
|
+
if (j < tokens.length && tokens[j][0] === 'PROPERTY') {
|
|
1451
|
+
j++;
|
|
1452
|
+
while (j + 1 < tokens.length && tokens[j][0] === '.' && tokens[j + 1][0] === 'PROPERTY') {
|
|
1453
|
+
j += 2;
|
|
1454
|
+
}
|
|
1455
|
+
if (j > i + 2 && j < tokens.length && tokens[j][0] === ':') {
|
|
1456
|
+
let openBracket = gen('[', '[', token);
|
|
1457
|
+
tokens.splice(i, 0, openBracket);
|
|
1458
|
+
let closeBracket = gen(']', ']', tokens[j + 1]);
|
|
1459
|
+
tokens.splice(j + 1, 0, closeBracket);
|
|
1460
|
+
return 2;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1466
|
+
// Dynamic classes
|
|
1467
|
+
// div.('card', x && 'active') → div.__cx__('card', x && 'active')
|
|
1468
|
+
// .('card') → div.__cx__('card')
|
|
1469
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1470
|
+
if (tag === '.' && nextToken && nextToken[0] === '(') {
|
|
1471
|
+
let prevToken = i > 0 ? tokens[i - 1] : null;
|
|
1472
|
+
let prevTag = prevToken ? prevToken[0] : null;
|
|
1473
|
+
let atLineStart = prevTag === 'INDENT' || prevTag === 'TERMINATOR';
|
|
1474
|
+
|
|
1475
|
+
let cxToken = gen('PROPERTY', '__cx__', token);
|
|
1476
|
+
nextToken[0] = 'CALL_START';
|
|
1477
|
+
let depth = 1;
|
|
1478
|
+
for (let j = i + 2; j < tokens.length && depth > 0; j++) {
|
|
1479
|
+
if (tokens[j][0] === '(' || tokens[j][0] === 'CALL_START') depth++;
|
|
1480
|
+
else if (tokens[j][0] === ')') {
|
|
1481
|
+
depth--;
|
|
1482
|
+
if (depth === 0) tokens[j][0] = 'CALL_END';
|
|
1483
|
+
} else if (tokens[j][0] === 'CALL_END') depth--;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
if (atLineStart) {
|
|
1487
|
+
let divToken = gen('IDENTIFIER', 'div', token);
|
|
1488
|
+
tokens.splice(i, 0, divToken);
|
|
1489
|
+
tokens.splice(i + 2, 0, cxToken);
|
|
1490
|
+
return 3;
|
|
1491
|
+
} else {
|
|
1492
|
+
tokens.splice(i + 1, 0, cxToken);
|
|
1493
|
+
return 2;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1498
|
+
// Implicit nesting (inject -> before INDENT)
|
|
1499
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
1500
|
+
if (nextToken && nextToken[0] === 'INDENT') {
|
|
1501
|
+
if (tag === '->' || tag === '=>' || tag === 'CALL_START' || tag === '(') {
|
|
1502
|
+
return 1;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
let isTemplateElement = false;
|
|
1506
|
+
|
|
1507
|
+
if (tag === 'IDENTIFIER' && isTemplateTag(token[1])) {
|
|
1508
|
+
isTemplateElement = true;
|
|
1509
|
+
} else if (tag === 'PROPERTY' || tag === 'STRING' || tag === 'CALL_END' || tag === ')') {
|
|
1510
|
+
isTemplateElement = startsWithHtmlTag(tokens, i);
|
|
1511
|
+
}
|
|
1512
|
+
else if (tag === 'IDENTIFIER' && i > 1 && tokens[i - 1][0] === '...') {
|
|
1513
|
+
if (startsWithHtmlTag(tokens, i)) {
|
|
1514
|
+
let commaToken = gen(',', ',', token);
|
|
1515
|
+
let arrowToken = gen('->', '->', token);
|
|
1516
|
+
arrowToken.newLine = true;
|
|
1517
|
+
tokens.splice(i + 1, 0, commaToken, arrowToken);
|
|
1518
|
+
return 3;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
if (isTemplateElement) {
|
|
1523
|
+
let callStartToken = gen('CALL_START', '(', token);
|
|
1524
|
+
let arrowToken = gen('->', '->', token);
|
|
1525
|
+
arrowToken.newLine = true;
|
|
1526
|
+
|
|
1527
|
+
tokens.splice(i + 1, 0, callStartToken, arrowToken);
|
|
1528
|
+
pendingCallEnds.push(currentIndent + 1);
|
|
1529
|
+
return 3;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
return 1;
|
|
1534
|
+
});
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1253
1537
|
tagPostfixConditionals() {
|
|
1254
1538
|
let original = null;
|
|
1255
1539
|
|