rip-lang 3.13.136 → 3.14.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/README.md +46 -4
- package/docs/RIP-LANG.md +116 -11
- package/docs/RIP-SCHEMA.md +2390 -0
- package/docs/RIP-TYPES.md +21 -14
- package/docs/assets/rip-schema-logo-960w.png +0 -0
- package/docs/assets/rip-schema-social.png +0 -0
- package/docs/dist/rip.js +6817 -3670
- package/docs/dist/rip.min.js +1454 -211
- package/docs/dist/rip.min.js.br +0 -0
- package/package.json +10 -4
- package/src/AGENTS.md +130 -0
- package/src/compiler.js +65 -2
- package/src/components.js +19 -5
- package/src/grammar/grammar.rip +20 -1
- package/src/lexer.js +42 -0
- package/src/parser.js +222 -220
- package/src/schema.js +3298 -0
- package/src/sourcemap-utils.js +155 -0
- package/src/typecheck.js +395 -23
- package/src/types.js +25 -0
- package/src/ui.rip +203 -45
- package/CHANGELOG.md +0 -1500
package/docs/dist/rip.min.js.br
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rip-lang",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.14.0",
|
|
4
4
|
"description": "A modern language that compiles to JavaScript",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/compiler.js",
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
"scripts/serve.js",
|
|
28
28
|
"rip-loader.js",
|
|
29
29
|
"README.md",
|
|
30
|
-
"LICENSE"
|
|
31
|
-
"CHANGELOG.md"
|
|
30
|
+
"LICENSE"
|
|
32
31
|
],
|
|
33
32
|
"scripts": {
|
|
34
33
|
"build": "bun scripts/build.js",
|
|
@@ -36,9 +35,16 @@
|
|
|
36
35
|
"gen:dom": "bun scripts/gen-dom.js",
|
|
37
36
|
"gallery": "bun scripts/gallery.js",
|
|
38
37
|
"parser": "bun src/grammar/solar.rip -o src/parser.js src/grammar/grammar.rip",
|
|
38
|
+
"postinstall": "bun scripts/link-local.js --quiet && bun scripts/link-check.js --quiet",
|
|
39
|
+
"link-local": "bun scripts/link-local.js",
|
|
40
|
+
"link-global": "bun scripts/link-global.js",
|
|
41
|
+
"link-check": "bun scripts/link-check.js",
|
|
39
42
|
"serve": "bun scripts/serve.js",
|
|
40
43
|
"test": "bun test/runner.js",
|
|
41
|
-
"test:types": "
|
|
44
|
+
"test:types": "bun test/types/runner.js",
|
|
45
|
+
"test:server": "./bin/rip packages/server/tests/runner.rip",
|
|
46
|
+
"test:time": "bun run --cwd packages/time test",
|
|
47
|
+
"test:all": "bun run test && bun run test:types && bun run test:server && bun run test:time",
|
|
42
48
|
"test:ui": "bun run --cwd packages/ui test:e2e",
|
|
43
49
|
"test:ui:chromium": "bun run --cwd packages/ui test:e2e:chromium",
|
|
44
50
|
"test:ui:axe": "bun run --cwd packages/ui test:e2e:axe",
|
package/src/AGENTS.md
CHANGED
|
@@ -300,6 +300,12 @@ div.card.active
|
|
|
300
300
|
.card.primary.("flex", x)
|
|
301
301
|
```
|
|
302
302
|
|
|
303
|
+
Tag and component name resolution:
|
|
304
|
+
|
|
305
|
+
- **Lowercase names** that match the template tag set are **DOM elements** (`div`, `span`, `button`)
|
|
306
|
+
- **PascalCase names** are **child components** — must start with uppercase, contain at least one lowercase letter, and have no underscores (`App`, `AuthScreen`, `HomeSection`)
|
|
307
|
+
- **ALL_CAPS names** are treated as regular variables, not components (`DEFAULT_SCREEN`, `SECTIONS`, `A`, `IO`)
|
|
308
|
+
|
|
303
309
|
Key mechanisms:
|
|
304
310
|
|
|
305
311
|
- `startsWithTag` — backward scan to decide whether a line starts a template tag
|
|
@@ -527,6 +533,130 @@ Types are processed at the token layer before parsing.
|
|
|
527
533
|
|
|
528
534
|
`rip --shadow file.rip` dumps the virtual TypeScript file that `rip check` and the VS Code extension feed into the TypeScript language service.
|
|
529
535
|
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## Schema System
|
|
539
|
+
|
|
540
|
+
Inline schemas are a third compiler sidecar — `schema.js` — that parallels
|
|
541
|
+
`types.js` and `components.js`.
|
|
542
|
+
|
|
543
|
+
### Lexer path
|
|
544
|
+
|
|
545
|
+
- `installSchemaSupport(Lexer, CodeEmitter)` adds `rewriteSchema()` to the
|
|
546
|
+
Lexer prototype. It runs between `rewriteRender()` and `rewriteTypes()` in
|
|
547
|
+
the rewriter pipeline.
|
|
548
|
+
- `rewriteSchema()` detects a contextual `schema` identifier at expression-
|
|
549
|
+
start positions followed by either a `:kind` SYMBOL or a direct INDENT.
|
|
550
|
+
The matching INDENT...OUTDENT range is parsed by a schema-specific
|
|
551
|
+
sub-parser and collapsed into a single `SCHEMA_BODY` token whose `.data`
|
|
552
|
+
carries a structured descriptor (kind, entries, per-entry `.loc`).
|
|
553
|
+
- The main grammar has one tiny production, `Schema: SCHEMA SCHEMA_BODY`,
|
|
554
|
+
under `Expression`. Schema body syntax — field declarations (with
|
|
555
|
+
optional type, `min..max` range, `[default]`, `/regex/`, `{attrs}`,
|
|
556
|
+
terminal `-> transform` with whole-raw-input `it`), directives
|
|
557
|
+
(`@name`), methods (`name: -> body`), computed getters
|
|
558
|
+
(`name: ~> body`), eager-derived fields (`name: !> body`), and
|
|
559
|
+
`@ensure "msg", (x) -> predicate` refinements — never reaches the
|
|
560
|
+
main parser, so the state table stays lean.
|
|
561
|
+
- Bodies of methods, computed getters, and hooks are captured as token
|
|
562
|
+
slices. At codegen time those slices run through the tail rewriter
|
|
563
|
+
passes (implicit braces, tagged templates, etc.) and feed into
|
|
564
|
+
`parser.parse()` via a temporary lex adapter. The parsed body is wrapped
|
|
565
|
+
as a thin-arrow `['->', [], body]` AST and emitted through the existing
|
|
566
|
+
codegen path — Rip `->` is already a `function()` (not a JS arrow), so
|
|
567
|
+
`this` binds to the instance.
|
|
568
|
+
|
|
569
|
+
### Layered runtime
|
|
570
|
+
|
|
571
|
+
The descriptor passed to `__schema({...})` is Layer 1. Layer 2 normalization
|
|
572
|
+
(fields, methods, computed, hooks, relations, expanded mixins, collision
|
|
573
|
+
checks) runs once per schema on first downstream use. Layer 3 (validator
|
|
574
|
+
plan) builds on the first `.parse/.safe/.ok`; Layer 4a (ORM) on the first
|
|
575
|
+
`.find/.create/.save`; Layer 4b (DDL) on the first `.toSQL()`. The four
|
|
576
|
+
caches are independent — a DDL-emitting script that only calls `.toSQL()`
|
|
577
|
+
never builds the ORM plan.
|
|
578
|
+
|
|
579
|
+
### Registry
|
|
580
|
+
|
|
581
|
+
`__SchemaRegistry` (process-global) holds every named `:model` and `:mixin`.
|
|
582
|
+
Relations look up `:model` targets by name; `@mixin Name` looks up `:mixin`
|
|
583
|
+
targets. Registration happens in the `__SchemaDef` constructor so just
|
|
584
|
+
importing a file that defines named schemas activates them. Tests can call
|
|
585
|
+
`__SchemaRegistry.reset()` between runs.
|
|
586
|
+
|
|
587
|
+
### Algebra invariant
|
|
588
|
+
|
|
589
|
+
`.pick/.omit/.partial/.required/.extend` always return `kind: 'shape'`.
|
|
590
|
+
**Field semantics survive** — type (including literal unions), modifiers,
|
|
591
|
+
constraints, and **inline transforms** — because they describe how a
|
|
592
|
+
field's value is obtained from raw input, not what the instance does.
|
|
593
|
+
**Instance behavior drops** — methods, computed getters (`~>`),
|
|
594
|
+
eager-derived fields (`!>`), hooks, ORM methods, and `@ensure`
|
|
595
|
+
refinements. Calling `.find()` or `.toSQL()` on a derived shape throws
|
|
596
|
+
a dedicated error pointing the user at query projection on the source
|
|
597
|
+
model. Refinements drop because they're schema-level invariants that
|
|
598
|
+
reference field names by identifier — the algebra operation has no
|
|
599
|
+
static way to know which names survive the derivation, so the safe
|
|
600
|
+
rule is "never carry them through." Runtime tests and the shadow TS
|
|
601
|
+
signatures both enforce this.
|
|
602
|
+
|
|
603
|
+
### Parser invariants (don't break these)
|
|
604
|
+
|
|
605
|
+
- **Field-line classification**: `IDENTIFIER` start → field; `PROPERTY`
|
|
606
|
+
start (trailing `:` absorbed into the identifier's tag) → callable.
|
|
607
|
+
Don't merge the two paths.
|
|
608
|
+
- **Type slot is optional** and defaults to `string` — the parser only
|
|
609
|
+
consumes a type when `typeFirst[0] === 'IDENTIFIER'` or `'STRING'`
|
|
610
|
+
(the literal-union case). Anything else triggers the default.
|
|
611
|
+
- **Transform is terminal**: once `->` appears as a field-line part,
|
|
612
|
+
no further comma-separated parts are allowed. Reject with a diagnostic
|
|
613
|
+
that says "move everything else before the arrow".
|
|
614
|
+
- **Comma before `->` is required** whenever anything precedes it
|
|
615
|
+
(type, range, regex, default, attrs). Only the bare form `name! -> fn`
|
|
616
|
+
parses comma-less. There are two enforcement points: after type
|
|
617
|
+
consumption (if `rest[0]` is `->`), and inside the parts loop (via
|
|
618
|
+
`findTopLevelArrowIdx` scanning depth-zero arrows within a part).
|
|
619
|
+
- **Transforms run on `.parse()` only, never `_hydrate`.** Hydrate
|
|
620
|
+
bypasses the whole parse pipeline (`_applyTransforms` → defaults →
|
|
621
|
+
validation → assign) and goes directly from column row to instance.
|
|
622
|
+
- **Eager-derived (`!>`) runs on both parse AND hydrate** — in
|
|
623
|
+
declaration order, on the partially-constructed instance. It is
|
|
624
|
+
NOT re-run on field mutation (materialized once, stored as own
|
|
625
|
+
enumerable property).
|
|
626
|
+
- **`@ensure` is a special directive** — parsed into its own
|
|
627
|
+
`tag: 'ensure'` entry (distinct from generic `tag: 'directive'`)
|
|
628
|
+
because it carries a compiled fn + message, not just args. Both
|
|
629
|
+
inline (`@ensure "msg", (x) -> body`) and array
|
|
630
|
+
(`@ensure [ "msg", fn, "msg", fn ]`) forms compile to one entry
|
|
631
|
+
per refinement; downstream runtime can't tell them apart. The
|
|
632
|
+
array-form splitter treats both `,` and TERMINATOR as element
|
|
633
|
+
separators at depth 0 to match Rip's array-literal convention.
|
|
634
|
+
- **@ensure runs AFTER field validation and BEFORE eager-derived.**
|
|
635
|
+
`.parse/.safe/.ok` short-circuit `@ensure` when per-field errors
|
|
636
|
+
fire (predicates assume field types are correct). `_hydrate` skips
|
|
637
|
+
`@ensure` entirely (trusted data). Runtime method name
|
|
638
|
+
`_applyEnsures` mirrors the directive (parallel to
|
|
639
|
+
`_applyTransforms` for `-> transform` and `_applyEagerDerived` for
|
|
640
|
+
`!> derived`). See `src/schema.js`.
|
|
641
|
+
|
|
642
|
+
### Shadow TS
|
|
643
|
+
|
|
644
|
+
`emitSchemaTypes(sexpr, lines)` walks the parsed s-expression for named
|
|
645
|
+
schema declarations, emits mixins first so intersections resolve, then
|
|
646
|
+
emits type aliases and `declare const` per kind:
|
|
647
|
+
|
|
648
|
+
- `:input` → `Schema<ValueType, ValueType>`
|
|
649
|
+
- `:shape` → `Schema<ShapeInstance, ShapeData>` (or `Schema<Data, Data>` when
|
|
650
|
+
fields-only)
|
|
651
|
+
- `:model` → `ModelSchema<Instance, Data>` with ORM methods and relation
|
|
652
|
+
accessors (same-file targets typed, cross-file degrades to `unknown`)
|
|
653
|
+
- `:mixin` → field-only `type Foo = { ... }` alias, no runtime value
|
|
654
|
+
- `:enum` → discriminated-union alias + const with `ok(data): data is
|
|
655
|
+
Role` type predicate
|
|
656
|
+
|
|
657
|
+
`hasSchemas(source)` is the cheap probe that gates intrinsic preamble
|
|
658
|
+
injection and file-level type checking (parallels `hasTypeAnnotations`).
|
|
659
|
+
|
|
530
660
|
Typical debugging sequence:
|
|
531
661
|
|
|
532
662
|
```bash
|
package/src/compiler.js
CHANGED
|
@@ -12,6 +12,7 @@ import { Lexer } from './lexer.js';
|
|
|
12
12
|
import { parser } from './parser.js';
|
|
13
13
|
import { installComponentSupport } from './components.js';
|
|
14
14
|
import { emitTypes, emitEnum } from './types.js';
|
|
15
|
+
import { installSchemaSupport } from './schema.js';
|
|
15
16
|
import { SourceMapGenerator } from './sourcemaps.js';
|
|
16
17
|
import { RipError, toRipError } from './error.js';
|
|
17
18
|
|
|
@@ -251,6 +252,9 @@ export class CodeEmitter {
|
|
|
251
252
|
// Types
|
|
252
253
|
'enum': 'emitEnum',
|
|
253
254
|
|
|
255
|
+
// Schema
|
|
256
|
+
'schema': 'emitSchema',
|
|
257
|
+
|
|
254
258
|
// Modules
|
|
255
259
|
'import': 'emitImport',
|
|
256
260
|
'export': 'emitExport',
|
|
@@ -263,6 +267,9 @@ export class CodeEmitter {
|
|
|
263
267
|
'regex': 'emitRegex',
|
|
264
268
|
'tagged-template': 'emitTaggedTemplate',
|
|
265
269
|
'str': 'emitString',
|
|
270
|
+
|
|
271
|
+
// Symbol literals
|
|
272
|
+
'symbol': 'emitSymbol',
|
|
266
273
|
};
|
|
267
274
|
|
|
268
275
|
constructor(options = {}) {
|
|
@@ -318,12 +325,30 @@ export class CodeEmitter {
|
|
|
318
325
|
}
|
|
319
326
|
}
|
|
320
327
|
|
|
328
|
+
// Check whether a column position falls inside a string literal on a line of
|
|
329
|
+
// generated JavaScript/TypeScript. Used by recordSubMappings to skip false
|
|
330
|
+
// matches (e.g. identifiers appearing as values inside union type strings).
|
|
331
|
+
static _isColInsideString(line, col) {
|
|
332
|
+
let inStr = false, quote = '';
|
|
333
|
+
for (let i = 0; i < line.length && i < col; i++) {
|
|
334
|
+
let ch = line[i];
|
|
335
|
+
if (inStr) {
|
|
336
|
+
if (ch === '\\') { i++; continue; }
|
|
337
|
+
if (ch === quote) inStr = false;
|
|
338
|
+
} else if (ch === '"' || ch === "'" || ch === '`') {
|
|
339
|
+
inStr = true; quote = ch;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return inStr;
|
|
343
|
+
}
|
|
344
|
+
|
|
321
345
|
// Walk the s-expression tree and record source map entries for
|
|
322
346
|
// sub-expressions that carry .loc, giving column-level precision.
|
|
323
347
|
recordSubMappings(code, sexpr, lineOffset) {
|
|
324
348
|
let stmtOrigLine = sexpr.loc ? sexpr.loc.r : 0;
|
|
325
349
|
let subs = [];
|
|
326
350
|
this.collectSubExprs(sexpr, subs);
|
|
351
|
+
let codeLines = code.split('\n');
|
|
327
352
|
for (let { name, origLine, origCol } of subs) {
|
|
328
353
|
let escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
329
354
|
let re = new RegExp('\\b' + escaped + '\\b', 'g');
|
|
@@ -332,11 +357,15 @@ export class CodeEmitter {
|
|
|
332
357
|
while ((m = re.exec(code)) !== null) {
|
|
333
358
|
let before = code.substring(0, m.index);
|
|
334
359
|
let nl = before.split('\n');
|
|
335
|
-
let
|
|
360
|
+
let genLineInStmt = nl.length - 1;
|
|
336
361
|
let genCol = nl[nl.length - 1].length;
|
|
362
|
+
// Skip matches inside string literals — prevents false mappings when
|
|
363
|
+
// an identifier also appears as a string value (e.g. union type member)
|
|
364
|
+
let lineText = codeLines[genLineInStmt];
|
|
365
|
+
if (lineText && CodeEmitter._isColInsideString(lineText, genCol)) continue;
|
|
366
|
+
let genLine = lineOffset + genLineInStmt;
|
|
337
367
|
// Prefer matches on the same relative line within the statement,
|
|
338
368
|
// falling back to column distance as tiebreaker.
|
|
339
|
-
let genLineInStmt = nl.length - 1;
|
|
340
369
|
let dist = Math.abs(genLineInStmt - origLineInStmt) * 10000 + Math.abs(genCol - origCol);
|
|
341
370
|
if (dist < bestDist) { bestDist = dist; bestMatch = { genLine, genCol }; }
|
|
342
371
|
}
|
|
@@ -770,6 +799,17 @@ export class CodeEmitter {
|
|
|
770
799
|
needsBlank = true;
|
|
771
800
|
}
|
|
772
801
|
|
|
802
|
+
if (this.usesSchemas && !skip) {
|
|
803
|
+
if (skipRT) {
|
|
804
|
+
code += 'var { __schema, SchemaError, __SchemaRegistry, __schemaSetAdapter } = globalThis.__ripSchema;\n';
|
|
805
|
+
} else if (typeof globalThis !== 'undefined' && globalThis.__ripSchema) {
|
|
806
|
+
code += 'const { __schema, SchemaError, __SchemaRegistry, __schemaSetAdapter } = globalThis.__ripSchema;\n';
|
|
807
|
+
} else {
|
|
808
|
+
code += this.getSchemaRuntime();
|
|
809
|
+
}
|
|
810
|
+
needsBlank = true;
|
|
811
|
+
}
|
|
812
|
+
|
|
773
813
|
if (this.dataSection !== null && this.dataSection !== undefined && !skip) {
|
|
774
814
|
code += 'var DATA;\n_setDataSection();\n';
|
|
775
815
|
needsBlank = true;
|
|
@@ -971,13 +1011,18 @@ export class CodeEmitter {
|
|
|
971
1011
|
|
|
972
1012
|
const prevComponentName = this._componentName;
|
|
973
1013
|
const prevComponentTypeParams = this._componentTypeParams;
|
|
1014
|
+
const prevSchemaName = this._schemaName;
|
|
974
1015
|
if (this.is(value, 'component') && (typeof target === 'string' || target instanceof String)) {
|
|
975
1016
|
this._componentName = str(target);
|
|
976
1017
|
this._componentTypeParams = target.typeParams || '';
|
|
977
1018
|
}
|
|
1019
|
+
if (this.is(value, 'schema') && (typeof target === 'string' || target instanceof String)) {
|
|
1020
|
+
this._schemaName = str(target);
|
|
1021
|
+
}
|
|
978
1022
|
let valueCode = this.emit(value, 'value');
|
|
979
1023
|
this._componentName = prevComponentName;
|
|
980
1024
|
this._componentTypeParams = prevComponentTypeParams;
|
|
1025
|
+
this._schemaName = prevSchemaName;
|
|
981
1026
|
let isObjLit = this.is(value, 'object');
|
|
982
1027
|
if (!isObjLit) valueCode = this.unwrap(valueCode);
|
|
983
1028
|
|
|
@@ -1630,6 +1675,12 @@ export class CodeEmitter {
|
|
|
1630
1675
|
return `(${ops.map(o => this.emit(o, 'value')).join(' || ')})`;
|
|
1631
1676
|
}
|
|
1632
1677
|
|
|
1678
|
+
// ---------------------------------------------------------------------------
|
|
1679
|
+
// Symbol literals
|
|
1680
|
+
// ---------------------------------------------------------------------------
|
|
1681
|
+
|
|
1682
|
+
emitSymbol(head, rest) { return `Symbol.for(${JSON.stringify(rest[0])})`; }
|
|
1683
|
+
|
|
1633
1684
|
// ---------------------------------------------------------------------------
|
|
1634
1685
|
// Data structures
|
|
1635
1686
|
// ---------------------------------------------------------------------------
|
|
@@ -2162,13 +2213,16 @@ export class CodeEmitter {
|
|
|
2162
2213
|
if (this.is(decl, '=')) {
|
|
2163
2214
|
const prev = this._componentName;
|
|
2164
2215
|
const prevTP = this._componentTypeParams;
|
|
2216
|
+
const prevSchema = this._schemaName;
|
|
2165
2217
|
if (this.is(decl[2], 'component')) {
|
|
2166
2218
|
this._componentName = str(decl[1]);
|
|
2167
2219
|
this._componentTypeParams = decl[1]?.typeParams || '';
|
|
2168
2220
|
}
|
|
2221
|
+
if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
|
|
2169
2222
|
const result = `const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
|
|
2170
2223
|
this._componentName = prev;
|
|
2171
2224
|
this._componentTypeParams = prevTP;
|
|
2225
|
+
this._schemaName = prevSchema;
|
|
2172
2226
|
return result;
|
|
2173
2227
|
}
|
|
2174
2228
|
if (Array.isArray(decl) && decl.every(i => typeof i === 'string')) return '';
|
|
@@ -2177,13 +2231,16 @@ export class CodeEmitter {
|
|
|
2177
2231
|
if (this.is(decl, '=')) {
|
|
2178
2232
|
const prev = this._componentName;
|
|
2179
2233
|
const prevTP = this._componentTypeParams;
|
|
2234
|
+
const prevSchema = this._schemaName;
|
|
2180
2235
|
if (this.is(decl[2], 'component')) {
|
|
2181
2236
|
this._componentName = str(decl[1]);
|
|
2182
2237
|
this._componentTypeParams = decl[1]?.typeParams || '';
|
|
2183
2238
|
}
|
|
2239
|
+
if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
|
|
2184
2240
|
const result = `export const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
|
|
2185
2241
|
this._componentName = prev;
|
|
2186
2242
|
this._componentTypeParams = prevTP;
|
|
2243
|
+
this._schemaName = prevSchema;
|
|
2187
2244
|
return result;
|
|
2188
2245
|
}
|
|
2189
2246
|
if (Array.isArray(decl) && decl.every(i => typeof i === 'string')) return `export { ${decl.join(', ')} }`;
|
|
@@ -3555,6 +3612,12 @@ installComponentSupport(CodeEmitter, Lexer);
|
|
|
3555
3612
|
|
|
3556
3613
|
CodeEmitter.prototype.emitEnum = emitEnum;
|
|
3557
3614
|
|
|
3615
|
+
// =============================================================================
|
|
3616
|
+
// Schema Support (prototype installation)
|
|
3617
|
+
// =============================================================================
|
|
3618
|
+
|
|
3619
|
+
installSchemaSupport(null, CodeEmitter);
|
|
3620
|
+
|
|
3558
3621
|
// =============================================================================
|
|
3559
3622
|
// Convenience Functions
|
|
3560
3623
|
// =============================================================================
|
package/src/components.js
CHANGED
|
@@ -145,7 +145,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
145
145
|
|
|
146
146
|
let isComponent = (name) => {
|
|
147
147
|
if (!name || typeof name !== 'string') return false;
|
|
148
|
-
return /^[A-Z]
|
|
148
|
+
return /^[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$/.test(name);
|
|
149
149
|
};
|
|
150
150
|
|
|
151
151
|
let isTemplateTag = (name) => {
|
|
@@ -538,7 +538,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
538
538
|
*/
|
|
539
539
|
proto.isComponent = function(name) {
|
|
540
540
|
if (!name || typeof name !== 'string') return false;
|
|
541
|
-
return /^[A-Z]
|
|
541
|
+
return /^[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$/.test(name);
|
|
542
542
|
};
|
|
543
543
|
|
|
544
544
|
/**
|
|
@@ -1132,7 +1132,9 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1132
1132
|
return; // Don't walk children again below
|
|
1133
1133
|
}
|
|
1134
1134
|
} else if (head === '__text__') {
|
|
1135
|
-
// = expr — text expression: emit the expression for type-checking
|
|
1135
|
+
// = expr — text expression: emit the expression for type-checking.
|
|
1136
|
+
// Return early — the expression is fully handled; walking children
|
|
1137
|
+
// would mis-interpret the call target as an element tag name.
|
|
1136
1138
|
const textExpr = node[1];
|
|
1137
1139
|
if (textExpr != null) {
|
|
1138
1140
|
const exprCode = this.emitInComponent(textExpr, 'value');
|
|
@@ -1140,6 +1142,18 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1140
1142
|
const srcMarker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
|
|
1141
1143
|
constructions.push(` ${exprCode};${srcMarker}`);
|
|
1142
1144
|
}
|
|
1145
|
+
return;
|
|
1146
|
+
} else if (head === 'str') {
|
|
1147
|
+
// Interpolated string — emit the full expression so TS sees
|
|
1148
|
+
// references to variables/functions inside the interpolation
|
|
1149
|
+
// (e.g. "#{format(x)}" must count as a read of `format`).
|
|
1150
|
+
try {
|
|
1151
|
+
const exprCode = this.emitInComponent(node, 'value');
|
|
1152
|
+
const srcLine = node.loc?.r;
|
|
1153
|
+
const srcMarker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
|
|
1154
|
+
constructions.push(` ${exprCode};${srcMarker}`);
|
|
1155
|
+
} catch {}
|
|
1156
|
+
return;
|
|
1143
1157
|
}
|
|
1144
1158
|
|
|
1145
1159
|
// Emit a bare lowercase identifier as either a property access
|
|
@@ -1534,7 +1548,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
|
|
|
1534
1548
|
const statements = this.is(body, 'block') ? body.slice(1) : [body];
|
|
1535
1549
|
|
|
1536
1550
|
let rootVar;
|
|
1537
|
-
if (statements.length === 0) {
|
|
1551
|
+
if (statements.length === 0 || (statements.length === 1 && statements[0] === 'null')) {
|
|
1538
1552
|
rootVar = 'null';
|
|
1539
1553
|
} else if (statements.length === 1) {
|
|
1540
1554
|
this._pendingAutoWire = !!this._autoEventHandlers;
|
|
@@ -2895,7 +2909,7 @@ class __Component {
|
|
|
2895
2909
|
this._target = target;
|
|
2896
2910
|
try {
|
|
2897
2911
|
this._root = this._create();
|
|
2898
|
-
target.appendChild(this._root);
|
|
2912
|
+
if (this._root) target.appendChild(this._root);
|
|
2899
2913
|
if (this._setup) this._setup();
|
|
2900
2914
|
if (this.mounted) this.mounted();
|
|
2901
2915
|
} catch (error) {
|
package/src/grammar/grammar.rip
CHANGED
|
@@ -86,6 +86,7 @@ grammar =
|
|
|
86
86
|
o 'Yield'
|
|
87
87
|
o 'Def'
|
|
88
88
|
o 'Enum'
|
|
89
|
+
o 'Schema'
|
|
89
90
|
]
|
|
90
91
|
|
|
91
92
|
# Single-line expressions (for postfix forms and inline arrows).
|
|
@@ -122,6 +123,7 @@ grammar =
|
|
|
122
123
|
o 'BOOL' # true/false
|
|
123
124
|
o 'INFINITY' # Infinity
|
|
124
125
|
o 'NAN' # NaN
|
|
126
|
+
o 'SYMBOL' , '["symbol", 1]'
|
|
125
127
|
]
|
|
126
128
|
|
|
127
129
|
AlphaNumeric: [
|
|
@@ -360,6 +362,7 @@ grammar =
|
|
|
360
362
|
o 'Array'
|
|
361
363
|
o 'Object'
|
|
362
364
|
o 'Parenthetical'
|
|
365
|
+
o 'SYMBOL' , '["symbol", 1]'
|
|
363
366
|
]
|
|
364
367
|
|
|
365
368
|
# ============================================================================
|
|
@@ -760,6 +763,21 @@ grammar =
|
|
|
760
763
|
o 'ENUM Identifier Block', '["enum", 2, 3]'
|
|
761
764
|
]
|
|
762
765
|
|
|
766
|
+
# ============================================================================
|
|
767
|
+
# Schema
|
|
768
|
+
# ============================================================================
|
|
769
|
+
# `schema [:kind] INDENT ... OUTDENT` is parsed entirely by the schema
|
|
770
|
+
# sub-parser at lexer-rewrite time (src/schema.js). The rewriter emits
|
|
771
|
+
# a synthetic SCHEMA_BODY token whose .data carries the full descriptor
|
|
772
|
+
# (kind, entries, per-entry loc). The main grammar only sees these two
|
|
773
|
+
# terminals, which keeps schema body syntax (`name! type`, `@directive`,
|
|
774
|
+
# `name: -> body`, `name: ~> body`) fully localized and prevents
|
|
775
|
+
# state-table growth.
|
|
776
|
+
|
|
777
|
+
Schema: [
|
|
778
|
+
o 'SCHEMA SCHEMA_BODY', '["schema", 2]'
|
|
779
|
+
]
|
|
780
|
+
|
|
763
781
|
# ============================================================================
|
|
764
782
|
# Components
|
|
765
783
|
# ============================================================================
|
|
@@ -793,7 +811,8 @@ grammar =
|
|
|
793
811
|
# div.card
|
|
794
812
|
# h1 "Hello"
|
|
795
813
|
Render: [
|
|
796
|
-
o 'RENDER Block', '["render", 2]'
|
|
814
|
+
o 'RENDER Block' , '["render", 2]'
|
|
815
|
+
o 'RENDER Expression' , '["render", 2]'
|
|
797
816
|
]
|
|
798
817
|
|
|
799
818
|
# ============================================================================
|
package/src/lexer.js
CHANGED
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
// ==========================================================================
|
|
41
41
|
|
|
42
42
|
import { installTypeSupport } from './types.js';
|
|
43
|
+
import { installSchemaSupport } from './schema.js';
|
|
43
44
|
|
|
44
45
|
// ==========================================================================
|
|
45
46
|
// Token Category Sets
|
|
@@ -107,6 +108,7 @@ let INDEXABLE = new Set([
|
|
|
107
108
|
...CALLABLE,
|
|
108
109
|
'NUMBER', 'INFINITY', 'NAN', 'STRING', 'STRING_END',
|
|
109
110
|
'REGEX', 'REGEX_END', 'BOOL', 'NULL', 'UNDEFINED', '}', 'MAP_END',
|
|
111
|
+
'SYMBOL',
|
|
110
112
|
]);
|
|
111
113
|
|
|
112
114
|
// Tokens that can follow IMPLICIT_FUNC to start an implicit call
|
|
@@ -118,6 +120,7 @@ let IMPLICIT_CALL = new Set([
|
|
|
118
120
|
'UNDEFINED', 'NULL', 'BOOL', 'UNARY', 'DO', 'DO_IIFE',
|
|
119
121
|
'YIELD', 'AWAIT', 'UNARY_MATH', 'SUPER', 'THROW',
|
|
120
122
|
'@', '->', '=>', '[', '(', '{', 'MAP_START', '--', '++',
|
|
123
|
+
'SYMBOL',
|
|
121
124
|
]);
|
|
122
125
|
|
|
123
126
|
// Tokens that can start an implicit call (unspaced, like +/-)
|
|
@@ -133,6 +136,7 @@ let IMPLICIT_END = new Set([
|
|
|
133
136
|
let IMPLICIT_COMMA_BEFORE_ARROW = new Set([
|
|
134
137
|
'STRING', 'STRING_END', 'REGEX', 'REGEX_END', 'NUMBER',
|
|
135
138
|
'BOOL', 'NULL', 'UNDEFINED', 'INFINITY', 'NAN', ']', '}', 'MAP_END',
|
|
139
|
+
'SYMBOL',
|
|
136
140
|
]);
|
|
137
141
|
|
|
138
142
|
// Tokens that start/end balanced pairs
|
|
@@ -506,6 +510,9 @@ export class Lexer {
|
|
|
506
510
|
// Don't treat colon as property when in ternary context
|
|
507
511
|
if (colon && prev && prev[0] === 'TERNARY') colon = null;
|
|
508
512
|
|
|
513
|
+
// Don't capture spaced colon when followed by identifier start (symbol literal)
|
|
514
|
+
if (colon && colon.length > 1 && /[a-zA-Z_$]/.test(this.chunk[idLen + colon.length])) colon = null;
|
|
515
|
+
|
|
509
516
|
// Property vs identifier
|
|
510
517
|
if (colon || (prev && (prev[0] === '.' || prev[0] === '?.' || (!prev.spaced && prev[0] === '@')))) {
|
|
511
518
|
tag = 'PROPERTY';
|
|
@@ -649,6 +656,18 @@ export class Lexer {
|
|
|
649
656
|
}
|
|
650
657
|
if (/^\s+#[a-zA-Z_]/.test(this.chunk)) return 0; // let lineToken handle indentation first
|
|
651
658
|
}
|
|
659
|
+
// Schema field modifier: `#` adjacent (unspaced) to an identifier acts
|
|
660
|
+
// as the unique-marker inside schema bodies (e.g. `email!# email`).
|
|
661
|
+
// Return 0 so literalToken emits a standalone `#` token; rewriteSchema
|
|
662
|
+
// absorbs it. Outside schema bodies the `#` token is harmless because
|
|
663
|
+
// nothing else in the grammar accepts `IDENTIFIER #` without a space.
|
|
664
|
+
if (this.chunk[0] === '#') {
|
|
665
|
+
let prev = this.prev();
|
|
666
|
+
if (prev && !prev.spaced && !prev.newLine &&
|
|
667
|
+
(prev[0] === 'IDENTIFIER' || prev[0] === 'PROPERTY')) {
|
|
668
|
+
return 0;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
652
671
|
let match = COMMENT_RE.exec(this.chunk);
|
|
653
672
|
if (!match) return 0;
|
|
654
673
|
return match[0].length;
|
|
@@ -1162,6 +1181,23 @@ export class Lexer {
|
|
|
1162
1181
|
return total;
|
|
1163
1182
|
}
|
|
1164
1183
|
|
|
1184
|
+
// --------------------------------------------------------------------------
|
|
1185
|
+
// Symbol Literal: :name → Symbol.for('name')
|
|
1186
|
+
// --------------------------------------------------------------------------
|
|
1187
|
+
|
|
1188
|
+
symbolToken() {
|
|
1189
|
+
if (this.chunk[0] !== ':') return 0;
|
|
1190
|
+
let next = this.chunk[1];
|
|
1191
|
+
if (!next || next === '=' || next === ':' || /\s/.test(next)) return 0;
|
|
1192
|
+
if (!/[a-zA-Z_$]/.test(next)) return 0;
|
|
1193
|
+
let match = /^((?:(?!\s)[$\w\x7f-\uffff])+)/.exec(this.chunk.slice(1));
|
|
1194
|
+
if (!match) return 0;
|
|
1195
|
+
let name = match[1];
|
|
1196
|
+
let total = name.length + 1;
|
|
1197
|
+
this.emit('SYMBOL', name, {len: total});
|
|
1198
|
+
return total;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1165
1201
|
// --------------------------------------------------------------------------
|
|
1166
1202
|
// 9. Literal Token (operators, punctuation, everything else)
|
|
1167
1203
|
// --------------------------------------------------------------------------
|
|
@@ -1171,6 +1207,10 @@ export class Lexer {
|
|
|
1171
1207
|
let wl = this.wordLiteral();
|
|
1172
1208
|
if (wl) return wl;
|
|
1173
1209
|
|
|
1210
|
+
// :name symbol literal: :redo → Symbol.for("redo")
|
|
1211
|
+
let sl = this.symbolToken();
|
|
1212
|
+
if (sl) return sl;
|
|
1213
|
+
|
|
1174
1214
|
let match = OPERATOR_RE.exec(this.chunk);
|
|
1175
1215
|
let val = match ? match[0] : this.chunk.charAt(0);
|
|
1176
1216
|
let tag = val;
|
|
@@ -1380,6 +1420,7 @@ export class Lexer {
|
|
|
1380
1420
|
this.closeOpenIndexes();
|
|
1381
1421
|
this.normalizeLines();
|
|
1382
1422
|
this.rewriteRender?.();
|
|
1423
|
+
this.rewriteSchema?.();
|
|
1383
1424
|
this.rewriteTypes();
|
|
1384
1425
|
this.tagPostfixConditionals();
|
|
1385
1426
|
this.rewriteTaggedTemplates();
|
|
@@ -1951,6 +1992,7 @@ export class Lexer {
|
|
|
1951
1992
|
// ==========================================================================
|
|
1952
1993
|
|
|
1953
1994
|
installTypeSupport(Lexer);
|
|
1995
|
+
installSchemaSupport(Lexer);
|
|
1954
1996
|
|
|
1955
1997
|
// ==========================================================================
|
|
1956
1998
|
// Convenience export
|