@spintax/core 0.2.0 → 0.3.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/CHANGELOG.md +69 -0
- package/README.md +4 -3
- package/dist/index.cjs +154 -63
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +154 -63
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -15,8 +15,14 @@
|
|
|
15
15
|
* the AST here.
|
|
16
16
|
* Permutation's `<config>` + per-element separators are fully parsed (PR-11b).
|
|
17
17
|
*/
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Bumped only on a breaking change to the node shape (independent of syntax v1).
|
|
20
|
+
*
|
|
21
|
+
* 2 — `ParsedAst` gained `defDefs`. An `Ast` cached by an older version carries no `#def` map, so
|
|
22
|
+
* rendering it would silently drop every definition; the version guard turns that into an
|
|
23
|
+
* `AstVersionError` instead.
|
|
24
|
+
*/
|
|
25
|
+
declare const AST_VERSION = 2;
|
|
20
26
|
/** Opaque public handle (re-exported as `Ast` from the package index). */
|
|
21
27
|
interface Ast {
|
|
22
28
|
readonly astVersion: typeof AST_VERSION;
|
|
@@ -166,7 +172,10 @@ interface Diagnostic {
|
|
|
166
172
|
}
|
|
167
173
|
interface ExtractResult {
|
|
168
174
|
refs: string[];
|
|
175
|
+
/** Names defined by `#set` — macros, re-picked at every reference. */
|
|
169
176
|
sets: string[];
|
|
177
|
+
/** Names defined by `#def` — rolled once per render and held. */
|
|
178
|
+
defs: string[];
|
|
170
179
|
includes: string[];
|
|
171
180
|
}
|
|
172
181
|
interface Analysis extends ExtractResult {
|
package/dist/index.d.ts
CHANGED
|
@@ -15,8 +15,14 @@
|
|
|
15
15
|
* the AST here.
|
|
16
16
|
* Permutation's `<config>` + per-element separators are fully parsed (PR-11b).
|
|
17
17
|
*/
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Bumped only on a breaking change to the node shape (independent of syntax v1).
|
|
20
|
+
*
|
|
21
|
+
* 2 — `ParsedAst` gained `defDefs`. An `Ast` cached by an older version carries no `#def` map, so
|
|
22
|
+
* rendering it would silently drop every definition; the version guard turns that into an
|
|
23
|
+
* `AstVersionError` instead.
|
|
24
|
+
*/
|
|
25
|
+
declare const AST_VERSION = 2;
|
|
20
26
|
/** Opaque public handle (re-exported as `Ast` from the package index). */
|
|
21
27
|
interface Ast {
|
|
22
28
|
readonly astVersion: typeof AST_VERSION;
|
|
@@ -166,7 +172,10 @@ interface Diagnostic {
|
|
|
166
172
|
}
|
|
167
173
|
interface ExtractResult {
|
|
168
174
|
refs: string[];
|
|
175
|
+
/** Names defined by `#set` — macros, re-picked at every reference. */
|
|
169
176
|
sets: string[];
|
|
177
|
+
/** Names defined by `#def` — rolled once per render and held. */
|
|
178
|
+
defs: string[];
|
|
170
179
|
includes: string[];
|
|
171
180
|
}
|
|
172
181
|
interface Analysis extends ExtractResult {
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/internal/ast.ts
|
|
2
|
-
var AST_VERSION =
|
|
2
|
+
var AST_VERSION = 2;
|
|
3
3
|
function walk(nodes, visit) {
|
|
4
4
|
for (const n of nodes) {
|
|
5
5
|
visit(n);
|
|
@@ -23,21 +23,39 @@ function isParsedAst(value) {
|
|
|
23
23
|
|
|
24
24
|
// src/internal/parser.ts
|
|
25
25
|
var VARIABLE_RE = /^%(\w+)%/;
|
|
26
|
-
var
|
|
26
|
+
var DIRECTIVE_RE = /^[ \t]*#(set|def)[ \t]+%(\w+)%[ \t]*=[ \t]*(.*?)[ \t]*\r?$/gmu;
|
|
27
27
|
var CONDITIONAL_NAME_RE = /^[A-Za-z_]\w*/;
|
|
28
28
|
var PLURAL_PREFIX = "plural ";
|
|
29
29
|
function parseTemplate(src) {
|
|
30
|
-
const { body, setDefs } =
|
|
31
|
-
return { astVersion: AST_VERSION, source: src, setDefs, nodes: parseSequence(body) };
|
|
30
|
+
const { body, setDefs, defDefs } = extractDirectives(stripComments(src));
|
|
31
|
+
return { astVersion: AST_VERSION, source: src, setDefs, defDefs, nodes: parseSequence(body) };
|
|
32
32
|
}
|
|
33
|
-
function
|
|
33
|
+
function extractDirectives(text) {
|
|
34
34
|
const setDefs = {};
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
const defDefs = {};
|
|
36
|
+
const occurrences = [];
|
|
37
|
+
DIRECTIVE_RE.lastIndex = 0;
|
|
38
|
+
const stripped = text.replace(
|
|
39
|
+
DIRECTIVE_RE,
|
|
40
|
+
(full, kind, rawName, value, offset) => {
|
|
41
|
+
const name = rawName.toLowerCase();
|
|
42
|
+
occurrences.push({
|
|
43
|
+
kind,
|
|
44
|
+
name,
|
|
45
|
+
value,
|
|
46
|
+
line: countLines(text, offset)
|
|
47
|
+
});
|
|
48
|
+
if (kind === "def") defDefs[name] = value;
|
|
49
|
+
else setDefs[name] = value;
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
return { body: stripped.replace(/\n{3,}/gu, "\n\n"), setDefs, defDefs, occurrences };
|
|
54
|
+
}
|
|
55
|
+
function countLines(text, offset) {
|
|
56
|
+
let line = 1;
|
|
57
|
+
for (let i = 0; i < offset; i += 1) if (text[i] === "\n") line += 1;
|
|
58
|
+
return line;
|
|
41
59
|
}
|
|
42
60
|
function stripComments(text) {
|
|
43
61
|
return text.replace(/\/#[\s\S]*?#\//g, "");
|
|
@@ -376,7 +394,7 @@ function validateTemplate(src, opts = {}) {
|
|
|
376
394
|
const diagnostics = [];
|
|
377
395
|
const text = stripComments(src);
|
|
378
396
|
checkBrackets(text, diagnostics);
|
|
379
|
-
|
|
397
|
+
checkDirectives(text, diagnostics);
|
|
380
398
|
checkPermutationConfigs(text, diagnostics);
|
|
381
399
|
checkPlurals(text, opts.locale, diagnostics);
|
|
382
400
|
checkVariableReferences(text, opts.knownVariables, diagnostics);
|
|
@@ -435,17 +453,43 @@ function checkBrackets(text, out) {
|
|
|
435
453
|
out.push(err("bracket.unclosed", `Unclosed '${unclosed.char}'.`, { line: unclosed.line, column: unclosed.column, endLine: unclosed.line, endColumn: unclosed.column + 1, data: { bracket: unclosed.char } }));
|
|
436
454
|
}
|
|
437
455
|
}
|
|
438
|
-
function
|
|
456
|
+
function checkDirectives(text, out) {
|
|
439
457
|
const lines = text.split("\n");
|
|
440
458
|
lines.forEach((lineText, idx) => {
|
|
441
459
|
const trimmed = lineText.replace(/^[ \t]+/, "");
|
|
442
|
-
|
|
443
|
-
|
|
460
|
+
const kind = ["#set", "#def"].find(
|
|
461
|
+
(candidate) => trimmed.startsWith(`${candidate} `) || trimmed.startsWith(`${candidate} `)
|
|
462
|
+
);
|
|
463
|
+
if (!kind) return;
|
|
464
|
+
DIRECTIVE_RE.lastIndex = 0;
|
|
465
|
+
if (!DIRECTIVE_RE.test(trimmed)) {
|
|
444
466
|
const column = lineText.length - trimmed.length + 1;
|
|
445
467
|
const line = idx + 1;
|
|
446
|
-
|
|
468
|
+
const code = kind === "#def" ? "def.malformed" : "set.malformed";
|
|
469
|
+
out.push(err(code, `Malformed ${kind}. Expected: ${kind} %name% = value`, { line, column, endLine: line, endColumn: lineText.length + 1 }));
|
|
447
470
|
}
|
|
448
471
|
});
|
|
472
|
+
const { occurrences } = extractDirectives(text);
|
|
473
|
+
const seen = /* @__PURE__ */ new Map();
|
|
474
|
+
for (const occurrence of occurrences) {
|
|
475
|
+
const first = seen.get(occurrence.name);
|
|
476
|
+
if (first !== void 0) {
|
|
477
|
+
out.push(err(
|
|
478
|
+
"definition.duplicate-name",
|
|
479
|
+
`Variable '${occurrence.name}' is defined more than once (first on line ${first}). A name belongs to one directive, once.`,
|
|
480
|
+
{ line: occurrence.line, column: 1 }
|
|
481
|
+
));
|
|
482
|
+
} else {
|
|
483
|
+
seen.set(occurrence.name, occurrence.line);
|
|
484
|
+
}
|
|
485
|
+
if (occurrence.kind === "def" && /#include\b/u.test(occurrence.value)) {
|
|
486
|
+
out.push(err(
|
|
487
|
+
"def.include-in-value",
|
|
488
|
+
`#include cannot appear in a #def value ('${occurrence.name}'): includes resolve after the value is frozen. Use #set, or put the #include in the body.`,
|
|
489
|
+
{ line: occurrence.line, column: 1 }
|
|
490
|
+
));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
449
493
|
}
|
|
450
494
|
function checkPermutationConfigs(text, out) {
|
|
451
495
|
for (const m of text.matchAll(/\[<([^>]*?)>/gu)) {
|
|
@@ -483,10 +527,20 @@ function checkPermutationConfigs(text, out) {
|
|
|
483
527
|
function checkPlurals(text, locale, out) {
|
|
484
528
|
const base = locale && locale !== "" ? normalizeBaseLang(locale) : "";
|
|
485
529
|
const arity = base !== "" ? pluralArity(base) : 0;
|
|
530
|
+
const tainted = macroTaintedNames(text);
|
|
486
531
|
for (const block of findPluralBlocks(text)) {
|
|
487
532
|
const at = span(text, block.start, block.end - block.start);
|
|
533
|
+
for (const m of block.countSlot.matchAll(/%(\w+)%/gu)) {
|
|
534
|
+
const name = (m[1] ?? "").toLowerCase();
|
|
535
|
+
if (!tainted.has(name)) continue;
|
|
536
|
+
out.push(err(
|
|
537
|
+
"plural.count-macro",
|
|
538
|
+
`{plural ...}: the count '${m[1]}' is a #set macro, so it is still unresolved spintax when the plural is decided and the block renders empty. Define it with #def instead.`,
|
|
539
|
+
at
|
|
540
|
+
));
|
|
541
|
+
}
|
|
488
542
|
if (/[{}[\]]/.test(block.formsRaw)) {
|
|
489
|
-
out.push(err("plural.nested-brackets", "{plural ...}: forms must not contain nested spintax brackets.", at));
|
|
543
|
+
out.push(err("plural.nested-brackets", "{plural ...}: forms must not contain nested spintax brackets. Extract via #def first \u2014 a #set is substituted verbatim and would put the brackets straight back.", at));
|
|
490
544
|
continue;
|
|
491
545
|
}
|
|
492
546
|
if (arity > 0) {
|
|
@@ -501,11 +555,33 @@ function checkPlurals(text, locale, out) {
|
|
|
501
555
|
}
|
|
502
556
|
}
|
|
503
557
|
}
|
|
558
|
+
var UNRESOLVED_AT_PLURAL_TIME = /\[|\{(?!\?)/u;
|
|
559
|
+
function macroTaintedNames(text) {
|
|
560
|
+
const macros = extractDirectives(text).setDefs;
|
|
561
|
+
const tainted = /* @__PURE__ */ new Set();
|
|
562
|
+
for (const [name, value] of Object.entries(macros)) {
|
|
563
|
+
if (UNRESOLVED_AT_PLURAL_TIME.test(value)) tainted.add(name);
|
|
564
|
+
}
|
|
565
|
+
let grew = true;
|
|
566
|
+
while (grew) {
|
|
567
|
+
grew = false;
|
|
568
|
+
for (const [name, value] of Object.entries(macros)) {
|
|
569
|
+
if (tainted.has(name)) continue;
|
|
570
|
+
for (const m of value.matchAll(/%(\w+)%/gu)) {
|
|
571
|
+
if (!tainted.has((m[1] ?? "").toLowerCase())) continue;
|
|
572
|
+
tainted.add(name);
|
|
573
|
+
grew = true;
|
|
574
|
+
break;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return tainted;
|
|
579
|
+
}
|
|
504
580
|
function checkVariableReferences(text, known, out) {
|
|
505
581
|
const knownSet = new Set((known ?? []).map((n) => n.toLowerCase()));
|
|
506
582
|
const defs = /* @__PURE__ */ new Map();
|
|
507
583
|
const defPos = /* @__PURE__ */ new Map();
|
|
508
|
-
for (const m of text.matchAll(/^[ \t]*#set[ \t]+%(\w+)%[ \t]*=[ \t]*(.*?)$/gmu)) {
|
|
584
|
+
for (const m of text.matchAll(/^[ \t]*#(?:set|def)[ \t]+%(\w+)%[ \t]*=[ \t]*(.*?)$/gmu)) {
|
|
509
585
|
const name = (m[1] ?? "").toLowerCase();
|
|
510
586
|
defs.set(name, m[2] ?? "");
|
|
511
587
|
const nameOffset = (m.index ?? 0) + m[0].indexOf("%");
|
|
@@ -520,7 +596,7 @@ function checkVariableReferences(text, known, out) {
|
|
|
520
596
|
for (const name of defs.keys()) {
|
|
521
597
|
detectCycle(name, defs, [name], defPos.get(name) ?? somewhere, out);
|
|
522
598
|
}
|
|
523
|
-
const body = text.replace(/^[ \t]*#set[ \t]+%\w+%[ \t]*=[ \t]*.*?$/gmu, (m) => m.replace(/[^\n]/g, " "));
|
|
599
|
+
const body = text.replace(/^[ \t]*#(?:set|def)[ \t]+%\w+%[ \t]*=[ \t]*.*?$/gmu, (m) => m.replace(/[^\n]/g, " "));
|
|
524
600
|
const seen = /* @__PURE__ */ new Set();
|
|
525
601
|
const undefinedAt = (name, offset, length) => {
|
|
526
602
|
const key = name.toLowerCase();
|
|
@@ -567,19 +643,21 @@ function checkIncludeTargets(text, known, out) {
|
|
|
567
643
|
|
|
568
644
|
// src/internal/extract.ts
|
|
569
645
|
var SET_DEF_RE = /^[ \t]*#set[ \t]+%(\w+)%[ \t]*=/gmu;
|
|
570
|
-
var
|
|
646
|
+
var DEF_DEF_RE = /^[ \t]*#def[ \t]+%(\w+)%[ \t]*=/gmu;
|
|
647
|
+
var DEFINITION_LHS_RE = /^[ \t]*#(?:set|def)[ \t]+%\w+%[ \t]*=/gmu;
|
|
571
648
|
var INCLUDE_RE2 = /^[ \t]*#include[ \t\n\r\f\x0B]+"([^"]+)"[ \t\n\r\f\x0B]*$/gmu;
|
|
572
649
|
var VARIABLE_RE2 = /%(\w+)%/gu;
|
|
573
650
|
var CONDITIONAL_REF_RE = /\{\?!?([A-Za-z_]\w*)\?/gu;
|
|
574
651
|
function extractFromSource(src) {
|
|
575
652
|
const text = stripComments(src);
|
|
576
653
|
const sets = collect(text, SET_DEF_RE, true);
|
|
654
|
+
const defs = collect(text, DEF_DEF_RE, true);
|
|
577
655
|
const includes = collect(text, INCLUDE_RE2, false);
|
|
578
|
-
const body = text.replace(
|
|
656
|
+
const body = text.replace(DEFINITION_LHS_RE, "");
|
|
579
657
|
const refs = /* @__PURE__ */ new Set();
|
|
580
658
|
addAll(refs, body, VARIABLE_RE2, true);
|
|
581
659
|
addAll(refs, body, CONDITIONAL_REF_RE, true);
|
|
582
|
-
return { refs: [...refs], sets, includes };
|
|
660
|
+
return { refs: [...refs], sets, defs, includes };
|
|
583
661
|
}
|
|
584
662
|
function collect(text, re, fold) {
|
|
585
663
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -651,14 +729,15 @@ function stripSentinels(text) {
|
|
|
651
729
|
var MAX_VARIABLE_DEPTH = 50;
|
|
652
730
|
var INCLUDE_LINE_RE = /^[ \t]*#include[ \t\n\r\f\x0B]+"([^"]+)"[ \t\n\r\f\x0B]*$/gmu;
|
|
653
731
|
function renderAst(ast, ctx) {
|
|
654
|
-
const
|
|
655
|
-
const
|
|
656
|
-
vars,
|
|
732
|
+
const base = buildVars(ast.setDefs, ctx.runtimeContext);
|
|
733
|
+
const walkOpts = {
|
|
657
734
|
rng: ctx.rng,
|
|
658
735
|
locale: ctx.locale,
|
|
659
736
|
depth: 0,
|
|
660
737
|
onPluralError: ctx.onPluralError
|
|
661
|
-
}
|
|
738
|
+
};
|
|
739
|
+
const vars = Object.keys(ast.defDefs).length > 0 ? { ...base, ...rollDefinitions(ast.defDefs, base, ctx.runtimeContext, walkOpts) } : base;
|
|
740
|
+
const text = renderNodes(ast.nodes, { ...walkOpts, vars });
|
|
662
741
|
return ctx.resolver ? resolveIncludes(text, ctx) : text;
|
|
663
742
|
}
|
|
664
743
|
function resolveIncludes(text, ctx) {
|
|
@@ -680,33 +759,65 @@ function resolveIncludes(text, ctx) {
|
|
|
680
759
|
});
|
|
681
760
|
});
|
|
682
761
|
}
|
|
683
|
-
function buildVars(setDefs, context
|
|
762
|
+
function buildVars(setDefs, context) {
|
|
684
763
|
const vars = {};
|
|
685
764
|
for (const [name, value] of Object.entries(setDefs)) {
|
|
686
|
-
vars[name] =
|
|
765
|
+
vars[name] = value;
|
|
687
766
|
}
|
|
688
767
|
for (const [name, value] of Object.entries(context)) {
|
|
689
768
|
vars[name.toLowerCase()] = value;
|
|
690
769
|
}
|
|
691
770
|
return vars;
|
|
692
771
|
}
|
|
693
|
-
function
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
772
|
+
function rollDefinitions(defDefs, vars, context, opts) {
|
|
773
|
+
const outranked = new Set(Object.keys(context).map((key) => key.toLowerCase()));
|
|
774
|
+
const rolled = {};
|
|
775
|
+
const aliases = {};
|
|
776
|
+
for (const [name, value] of Object.entries(vars)) {
|
|
777
|
+
if (name in defDefs && !outranked.has(name)) continue;
|
|
778
|
+
aliases[name] = value;
|
|
779
|
+
}
|
|
780
|
+
for (const name of orderDefinitions(defDefs, aliases)) {
|
|
781
|
+
if (outranked.has(name)) continue;
|
|
782
|
+
const value = defDefs[name] ?? "";
|
|
783
|
+
rolled[name] = renderNodes(parseSequence(value), { ...opts, vars: { ...vars, ...rolled } });
|
|
784
|
+
}
|
|
785
|
+
return rolled;
|
|
786
|
+
}
|
|
787
|
+
function orderDefinitions(defDefs, aliases) {
|
|
788
|
+
const names = Object.keys(defDefs);
|
|
789
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
790
|
+
for (const name of names) {
|
|
791
|
+
const reached = referencedNames(defDefs[name] ?? "", aliases);
|
|
792
|
+
blocked.set(name, new Set(names.filter((candidate) => reached.has(candidate))));
|
|
793
|
+
}
|
|
794
|
+
const ordered = [];
|
|
795
|
+
let pending = names;
|
|
796
|
+
while (pending.length > 0) {
|
|
797
|
+
const ready = pending.filter((name) => {
|
|
798
|
+
const deps = blocked.get(name);
|
|
799
|
+
return !deps || ![...deps].some((dep) => dep !== name && pending.includes(dep));
|
|
706
800
|
});
|
|
707
|
-
if (
|
|
801
|
+
if (ready.length === 0) return [...ordered, ...pending];
|
|
802
|
+
ordered.push(...ready);
|
|
803
|
+
pending = pending.filter((name) => !ready.includes(name));
|
|
708
804
|
}
|
|
709
|
-
return
|
|
805
|
+
return ordered;
|
|
806
|
+
}
|
|
807
|
+
function referencedNames(value, aliases) {
|
|
808
|
+
const seen = /* @__PURE__ */ new Set();
|
|
809
|
+
const queue = directReferences(value);
|
|
810
|
+
while (queue.length > 0) {
|
|
811
|
+
const name = queue.shift();
|
|
812
|
+
if (seen.has(name)) continue;
|
|
813
|
+
seen.add(name);
|
|
814
|
+
const alias = aliases[name];
|
|
815
|
+
if (alias !== void 0) queue.push(...directReferences(alias));
|
|
816
|
+
}
|
|
817
|
+
return seen;
|
|
818
|
+
}
|
|
819
|
+
function directReferences(text) {
|
|
820
|
+
return [...text.matchAll(/%(\w+)%/gu)].map((match) => (match[1] ?? "").toLowerCase());
|
|
710
821
|
}
|
|
711
822
|
function renderNodes(nodes, opts) {
|
|
712
823
|
let out = "";
|
|
@@ -865,26 +976,6 @@ function padSeparator(sep) {
|
|
|
865
976
|
if (/^\p{L}+$/u.test(trimmed)) return ` ${trimmed} `;
|
|
866
977
|
return sep;
|
|
867
978
|
}
|
|
868
|
-
function splitTopLevelPipes(inner) {
|
|
869
|
-
const parts = [];
|
|
870
|
-
let brace = 0;
|
|
871
|
-
let bracket = 0;
|
|
872
|
-
let cur = "";
|
|
873
|
-
for (const ch of inner) {
|
|
874
|
-
if (ch === "{") brace += 1;
|
|
875
|
-
else if (ch === "}") brace -= 1;
|
|
876
|
-
else if (ch === "[") bracket += 1;
|
|
877
|
-
else if (ch === "]") bracket -= 1;
|
|
878
|
-
if (ch === "|" && brace === 0 && bracket === 0) {
|
|
879
|
-
parts.push(cur);
|
|
880
|
-
cur = "";
|
|
881
|
-
} else {
|
|
882
|
-
cur += ch;
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
parts.push(cur);
|
|
886
|
-
return parts;
|
|
887
|
-
}
|
|
888
979
|
var PHP_LTRIM_RE2 = /^[ \t\n\r\0\x0B]+/u;
|
|
889
980
|
var PHP_RTRIM_RE2 = /[ \t\n\r\0\x0B]+$/u;
|
|
890
981
|
function phpTrim2(s) {
|