@barefootjs/mojolicious 0.6.1 → 0.8.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/dist/adapter/index.js +116 -0
- package/dist/adapter/mojo-adapter.d.ts +80 -0
- package/dist/adapter/mojo-adapter.d.ts.map +1 -1
- package/dist/build.js +116 -0
- package/dist/index.js +116 -0
- package/dist/test-render.d.ts.map +1 -1
- package/lib/BarefootJS/Backend/Mojo.pm +91 -0
- package/lib/Mojolicious/Plugin/BarefootJS/DevReload.pm +11 -51
- package/lib/Mojolicious/Plugin/BarefootJS.pm +56 -0
- package/package.json +7 -6
- package/src/__tests__/mojo-adapter.test.ts +124 -10
- package/src/adapter/mojo-adapter.ts +258 -1
- package/src/test-render.ts +30 -3
- package/lib/BarefootJS.pm +0 -1060
package/dist/adapter/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/adapter/mojo-adapter.ts
|
|
2
|
+
import ts from "typescript";
|
|
2
3
|
import {
|
|
3
4
|
BaseAdapter,
|
|
4
5
|
isBooleanAttr,
|
|
@@ -80,6 +81,22 @@ function resolveJsxChildrenProp(props) {
|
|
|
80
81
|
return [];
|
|
81
82
|
return prop.value.children;
|
|
82
83
|
}
|
|
84
|
+
function parsePureStringLiteral(source) {
|
|
85
|
+
const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
|
|
86
|
+
const stmt = sf.statements[0];
|
|
87
|
+
if (!stmt || !ts.isVariableStatement(stmt))
|
|
88
|
+
return null;
|
|
89
|
+
const decl = stmt.declarationList.declarations[0];
|
|
90
|
+
let init = decl?.initializer;
|
|
91
|
+
while (init && ts.isParenthesizedExpression(init))
|
|
92
|
+
init = init.expression;
|
|
93
|
+
if (!init)
|
|
94
|
+
return null;
|
|
95
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
96
|
+
return init.text;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
83
100
|
|
|
84
101
|
class MojoAdapter extends BaseAdapter {
|
|
85
102
|
name = "mojolicious";
|
|
@@ -94,6 +111,9 @@ class MojoAdapter extends BaseAdapter {
|
|
|
94
111
|
propsObjectName = null;
|
|
95
112
|
propsParams = [];
|
|
96
113
|
stringValueNames = new Set;
|
|
114
|
+
moduleStringConsts = new Map;
|
|
115
|
+
loopBoundNames = new Map;
|
|
116
|
+
nullableOptionalProps = new Set;
|
|
97
117
|
constructor(options = {}) {
|
|
98
118
|
super();
|
|
99
119
|
this.options = {
|
|
@@ -105,6 +125,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
105
125
|
this.componentName = ir.metadata.componentName;
|
|
106
126
|
this.propsObjectName = ir.metadata.propsObjectName ?? null;
|
|
107
127
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
128
|
+
this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
|
|
108
129
|
this.stringValueNames = new Set;
|
|
109
130
|
for (const s of ir.metadata.signals) {
|
|
110
131
|
if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
|
|
@@ -115,6 +136,8 @@ class MojoAdapter extends BaseAdapter {
|
|
|
115
136
|
if (isStringTypeInfo(p.type))
|
|
116
137
|
this.stringValueNames.add(p.name);
|
|
117
138
|
}
|
|
139
|
+
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
140
|
+
this.loopBoundNames.clear();
|
|
118
141
|
this.errors = [];
|
|
119
142
|
this.childrenCaptureCounter = 0;
|
|
120
143
|
if (!options?.siblingTemplatesRegistered) {
|
|
@@ -139,6 +162,27 @@ class MojoAdapter extends BaseAdapter {
|
|
|
139
162
|
extension: this.extension
|
|
140
163
|
};
|
|
141
164
|
}
|
|
165
|
+
collectModuleStringConsts(constants) {
|
|
166
|
+
const map = new Map;
|
|
167
|
+
for (const c of constants ?? []) {
|
|
168
|
+
if (!c.isModule)
|
|
169
|
+
continue;
|
|
170
|
+
if (c.value === undefined)
|
|
171
|
+
continue;
|
|
172
|
+
const literal = parsePureStringLiteral(c.value);
|
|
173
|
+
if (literal !== null)
|
|
174
|
+
map.set(c.name, literal);
|
|
175
|
+
}
|
|
176
|
+
return map;
|
|
177
|
+
}
|
|
178
|
+
resolveModuleStringConst(name) {
|
|
179
|
+
if (this.loopBoundNames.has(name))
|
|
180
|
+
return null;
|
|
181
|
+
const value = this.moduleStringConsts.get(name);
|
|
182
|
+
if (value === undefined)
|
|
183
|
+
return null;
|
|
184
|
+
return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
185
|
+
}
|
|
142
186
|
generateScriptRegistrations(ir, scriptBaseName) {
|
|
143
187
|
const hasInteractivity = this.hasClientInteractivity(ir);
|
|
144
188
|
if (!hasInteractivity)
|
|
@@ -405,6 +449,10 @@ ${whenTrue}
|
|
|
405
449
|
}
|
|
406
450
|
const param = loop.param;
|
|
407
451
|
const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
|
|
452
|
+
const loopBound = loop.iterationShape === "keys" ? [param] : [param, loop.index ?? "_i"];
|
|
453
|
+
for (const n of loopBound) {
|
|
454
|
+
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
|
|
455
|
+
}
|
|
408
456
|
const prevInLoop = this.inLoop;
|
|
409
457
|
this.inLoop = true;
|
|
410
458
|
const renderedChildren = this.renderChildren(loop.children);
|
|
@@ -438,6 +486,13 @@ ${renderedChildren}` : renderedChildren;
|
|
|
438
486
|
} else {
|
|
439
487
|
lines.push(children);
|
|
440
488
|
}
|
|
489
|
+
for (const n of loopBound) {
|
|
490
|
+
const c = (this.loopBoundNames.get(n) ?? 1) - 1;
|
|
491
|
+
if (c <= 0)
|
|
492
|
+
this.loopBoundNames.delete(n);
|
|
493
|
+
else
|
|
494
|
+
this.loopBoundNames.set(n, c);
|
|
495
|
+
}
|
|
441
496
|
lines.push(`% }`);
|
|
442
497
|
lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
|
|
443
498
|
return lines.join(`
|
|
@@ -529,6 +584,12 @@ ${children}`;
|
|
|
529
584
|
if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
|
|
530
585
|
return "";
|
|
531
586
|
}
|
|
587
|
+
const bareId = value.expr.trim();
|
|
588
|
+
if (!isBooleanAttr(name) && !value.presenceOrUndefined && this.nullableOptionalProps.has(bareId)) {
|
|
589
|
+
const perl2 = this.convertExpressionToPerl(value.expr);
|
|
590
|
+
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
|
|
591
|
+
return `<% if (defined ${perl2}) { %>${body}<% } %>`;
|
|
592
|
+
}
|
|
532
593
|
if (isBooleanAttr(name) || value.presenceOrUndefined) {
|
|
533
594
|
return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`;
|
|
534
595
|
}
|
|
@@ -549,6 +610,10 @@ ${children}`;
|
|
|
549
610
|
const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
|
|
550
611
|
return `<%== bf->spread_attrs({${entries.join(", ")}}) %>`;
|
|
551
612
|
}
|
|
613
|
+
const ternaryHashref = this.conditionalSpreadToPerl(trimmed);
|
|
614
|
+
if (ternaryHashref !== null) {
|
|
615
|
+
return `<%== bf->spread_attrs(${ternaryHashref}) %>`;
|
|
616
|
+
}
|
|
552
617
|
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
553
618
|
return `<%== bf->spread_attrs(${perlExpr}) %>`;
|
|
554
619
|
},
|
|
@@ -713,6 +778,54 @@ ${reason}` : "";
|
|
|
713
778
|
});
|
|
714
779
|
return true;
|
|
715
780
|
}
|
|
781
|
+
conditionalSpreadToPerl(expr) {
|
|
782
|
+
const sf = ts.createSourceFile("__spread.ts", `(${expr})`, ts.ScriptTarget.Latest, true);
|
|
783
|
+
if (sf.statements.length !== 1)
|
|
784
|
+
return null;
|
|
785
|
+
const stmt = sf.statements[0];
|
|
786
|
+
if (!ts.isExpressionStatement(stmt))
|
|
787
|
+
return null;
|
|
788
|
+
let node = stmt.expression;
|
|
789
|
+
while (ts.isParenthesizedExpression(node))
|
|
790
|
+
node = node.expression;
|
|
791
|
+
if (!ts.isConditionalExpression(node))
|
|
792
|
+
return null;
|
|
793
|
+
const unwrap = (e) => {
|
|
794
|
+
let n = e;
|
|
795
|
+
while (ts.isParenthesizedExpression(n))
|
|
796
|
+
n = n.expression;
|
|
797
|
+
return n;
|
|
798
|
+
};
|
|
799
|
+
const whenTrue = unwrap(node.whenTrue);
|
|
800
|
+
const whenFalse = unwrap(node.whenFalse);
|
|
801
|
+
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
const condPerl = this.convertExpressionToPerl(node.condition.getText(sf));
|
|
805
|
+
const truePerl = this.objectLiteralToPerlHashref(whenTrue, sf);
|
|
806
|
+
const falsePerl = this.objectLiteralToPerlHashref(whenFalse, sf);
|
|
807
|
+
if (truePerl === null || falsePerl === null)
|
|
808
|
+
return null;
|
|
809
|
+
return `${condPerl} ? ${truePerl} : ${falsePerl}`;
|
|
810
|
+
}
|
|
811
|
+
objectLiteralToPerlHashref(obj, sf) {
|
|
812
|
+
const entries = [];
|
|
813
|
+
for (const prop of obj.properties) {
|
|
814
|
+
if (!ts.isPropertyAssignment(prop))
|
|
815
|
+
return null;
|
|
816
|
+
let key;
|
|
817
|
+
if (ts.isIdentifier(prop.name)) {
|
|
818
|
+
key = prop.name.text;
|
|
819
|
+
} else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
820
|
+
key = prop.name.text;
|
|
821
|
+
} else {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
const valPerl = this.convertExpressionToPerl(prop.initializer.getText(sf));
|
|
825
|
+
entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
|
|
826
|
+
}
|
|
827
|
+
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
828
|
+
}
|
|
716
829
|
convertExpressionToPerl(expr) {
|
|
717
830
|
const trimmed = expr.trim();
|
|
718
831
|
if (trimmed === "")
|
|
@@ -1056,6 +1169,9 @@ class MojoTopLevelEmitter {
|
|
|
1056
1169
|
this.adapter = adapter;
|
|
1057
1170
|
}
|
|
1058
1171
|
identifier(name) {
|
|
1172
|
+
const inlined = this.adapter.resolveModuleStringConst(name);
|
|
1173
|
+
if (inlined !== null)
|
|
1174
|
+
return inlined;
|
|
1059
1175
|
return `$${name}`;
|
|
1060
1176
|
}
|
|
1061
1177
|
literal(value, literalType) {
|
|
@@ -58,8 +58,62 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
|
|
|
58
58
|
* true — selecting the string operator from the operand's type avoids that.
|
|
59
59
|
*/
|
|
60
60
|
private stringValueNames;
|
|
61
|
+
/**
|
|
62
|
+
* Module-scope pure string-literal constants (`const X = 'literal'` at
|
|
63
|
+
* file top-level), keyed by name → resolved literal value. Populated at
|
|
64
|
+
* `generate()` entry from `ir.metadata.localConstants`. When an identifier
|
|
65
|
+
* in an expression resolves to one of these, the adapter inlines the
|
|
66
|
+
* literal instead of emitting `$X` against a stash variable that is never
|
|
67
|
+
* bound (a module const isn't a prop, signal, or local — the value would
|
|
68
|
+
* render empty). Hono inlines it for free; this restores parity. Only
|
|
69
|
+
* module-scope pure string literals qualify (see `collectModuleStringConsts`).
|
|
70
|
+
*/
|
|
71
|
+
private moduleStringConsts;
|
|
72
|
+
/**
|
|
73
|
+
* Names currently bound by an enclosing loop body — the `my $<param>` and
|
|
74
|
+
* `my $<index>` bindings `renderLoop` introduces — ref-counted so nested
|
|
75
|
+
* loops compose. `resolveModuleStringConst` consults this so a loop
|
|
76
|
+
* variable whose name happens to match a module string const is NOT
|
|
77
|
+
* inlined as the const literal (mirrors the Go adapter's loop-param /
|
|
78
|
+
* loop-var shadowing guards). (#1749 review)
|
|
79
|
+
*/
|
|
80
|
+
private loopBoundNames;
|
|
81
|
+
/**
|
|
82
|
+
* Prop names whose value is `undef` in the template body when the caller
|
|
83
|
+
* omits them — so a bare-reference attribute should be dropped rather
|
|
84
|
+
* than rendered as `attr=""`. The actual population criterion (see
|
|
85
|
+
* `generate()`) is: NO destructure default (`defaultValue === undefined`)
|
|
86
|
+
* AND non-rest (`!isRest`) AND non-primitive type (`type.kind !==
|
|
87
|
+
* 'primitive'`). It deliberately does NOT consult `p.optional`: the
|
|
88
|
+
* analyzer derives `optional` from the presence of a default initializer,
|
|
89
|
+
* not the `?` token, so it's not the right witness here. Excluding
|
|
90
|
+
* concrete primitives (`string`/`number`/`boolean`) mirrors the Go
|
|
91
|
+
* adapter's scope, which guards only `interface{}` (nillable) fields.
|
|
92
|
+
* Used by `elementAttrEmitter.emitExpression` to guard such an attribute
|
|
93
|
+
* with a Perl `defined $x` check (`<textarea>` omits `rows`), matching
|
|
94
|
+
* Hono's nullish-attribute omission. Concrete/defaulted props are
|
|
95
|
+
* excluded and always emit unconditionally.
|
|
96
|
+
*/
|
|
97
|
+
private nullableOptionalProps;
|
|
61
98
|
constructor(options?: MojoAdapterOptions);
|
|
62
99
|
generate(ir: ComponentIR, options?: AdapterGenerateOptions): AdapterOutput;
|
|
100
|
+
/**
|
|
101
|
+
* Build the module pure-string-const map from the IR's localConstants.
|
|
102
|
+
* A const qualifies only when it is module-scope (`isModule`) and its
|
|
103
|
+
* initializer parses to a single string literal (`ts.StringLiteral` or
|
|
104
|
+
* `ts.NoSubstitutionTemplateLiteral`). Template literals with `${}`,
|
|
105
|
+
* numeric/object initializers, `Record<T,string>` maps, memos, and
|
|
106
|
+
* signals are all excluded — only a pure compile-time string can be
|
|
107
|
+
* inlined byte-for-byte.
|
|
108
|
+
*/
|
|
109
|
+
private collectModuleStringConsts;
|
|
110
|
+
/**
|
|
111
|
+
* Resolve an identifier to its inlined Perl single-quoted string literal
|
|
112
|
+
* when it names a module pure-string const, else `null` (the caller then
|
|
113
|
+
* falls back to its normal `$name` stash lowering). Returns the Perl
|
|
114
|
+
* literal form `'<escaped>'` ready to drop into an expression.
|
|
115
|
+
*/
|
|
116
|
+
resolveModuleStringConst(name: string): string | null;
|
|
63
117
|
private generateScriptRegistrations;
|
|
64
118
|
private hasClientInteractivity;
|
|
65
119
|
/**
|
|
@@ -165,6 +219,32 @@ export declare class MojoAdapter extends BaseAdapter implements IRNodeEmitter<Mo
|
|
|
165
219
|
* should drop the attribute / skip the emit).
|
|
166
220
|
*/
|
|
167
221
|
private refuseUnsupportedAttrExpression;
|
|
222
|
+
/**
|
|
223
|
+
* Lower a conditional inline-object spread expression
|
|
224
|
+
* `(COND ? { 'aria-describedby': describedBy } : {})`
|
|
225
|
+
* (either branch possibly `{}`) into a Perl inline ternary of
|
|
226
|
+
* hashrefs for `bf->spread_attrs`:
|
|
227
|
+
* `$describedBy ? { 'aria-describedby' => $describedBy } : {}`
|
|
228
|
+
*
|
|
229
|
+
* The condition is translated via `convertExpressionToPerl` (a bare
|
|
230
|
+
* prop ident becomes `$describedBy`; Perl truthiness handles the
|
|
231
|
+
* test). Object literals become Perl hashrefs with `=>`; string-
|
|
232
|
+
* literal keys are quoted, values resolve via `convertExpressionToPerl`.
|
|
233
|
+
*
|
|
234
|
+
* Returns null when the expression is NOT this shape, or when a part
|
|
235
|
+
* can't be faithfully lowered (non-static key, etc.) so the caller
|
|
236
|
+
* falls back to the standard `convertExpressionToPerl` path (which
|
|
237
|
+
* records BF101). Scoped strictly to ternary-of-object-literals so no
|
|
238
|
+
* other spread shape regresses.
|
|
239
|
+
*/
|
|
240
|
+
private conditionalSpreadToPerl;
|
|
241
|
+
/**
|
|
242
|
+
* Convert a static object literal into a Perl hashref string for a
|
|
243
|
+
* conditional spread. Only static string/identifier keys are allowed;
|
|
244
|
+
* values resolve via `convertExpressionToPerl`. Returns null for any
|
|
245
|
+
* computed/spread/dynamic key. Empty object → `{}`.
|
|
246
|
+
*/
|
|
247
|
+
private objectLiteralToPerlHashref;
|
|
168
248
|
private convertExpressionToPerl;
|
|
169
249
|
/**
|
|
170
250
|
* Render a full ParsedExpr tree to Perl for top-level (non-filter)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-adapter.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"mojo-adapter.d.ts","sourceRoot":"","sources":["../../src/adapter/mojo-adapter.ts"],"names":[],"mappings":"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"}
|
package/dist/build.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/adapter/mojo-adapter.ts
|
|
2
|
+
import ts from "typescript";
|
|
2
3
|
import {
|
|
3
4
|
BaseAdapter,
|
|
4
5
|
isBooleanAttr,
|
|
@@ -80,6 +81,22 @@ function resolveJsxChildrenProp(props) {
|
|
|
80
81
|
return [];
|
|
81
82
|
return prop.value.children;
|
|
82
83
|
}
|
|
84
|
+
function parsePureStringLiteral(source) {
|
|
85
|
+
const sf = ts.createSourceFile("__const.ts", `const __x = (${source});`, ts.ScriptTarget.Latest, false);
|
|
86
|
+
const stmt = sf.statements[0];
|
|
87
|
+
if (!stmt || !ts.isVariableStatement(stmt))
|
|
88
|
+
return null;
|
|
89
|
+
const decl = stmt.declarationList.declarations[0];
|
|
90
|
+
let init = decl?.initializer;
|
|
91
|
+
while (init && ts.isParenthesizedExpression(init))
|
|
92
|
+
init = init.expression;
|
|
93
|
+
if (!init)
|
|
94
|
+
return null;
|
|
95
|
+
if (ts.isStringLiteral(init) || ts.isNoSubstitutionTemplateLiteral(init)) {
|
|
96
|
+
return init.text;
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
83
100
|
|
|
84
101
|
class MojoAdapter extends BaseAdapter {
|
|
85
102
|
name = "mojolicious";
|
|
@@ -94,6 +111,9 @@ class MojoAdapter extends BaseAdapter {
|
|
|
94
111
|
propsObjectName = null;
|
|
95
112
|
propsParams = [];
|
|
96
113
|
stringValueNames = new Set;
|
|
114
|
+
moduleStringConsts = new Map;
|
|
115
|
+
loopBoundNames = new Map;
|
|
116
|
+
nullableOptionalProps = new Set;
|
|
97
117
|
constructor(options = {}) {
|
|
98
118
|
super();
|
|
99
119
|
this.options = {
|
|
@@ -105,6 +125,7 @@ class MojoAdapter extends BaseAdapter {
|
|
|
105
125
|
this.componentName = ir.metadata.componentName;
|
|
106
126
|
this.propsObjectName = ir.metadata.propsObjectName ?? null;
|
|
107
127
|
this.propsParams = ir.metadata.propsParams.map((p) => ({ name: p.name }));
|
|
128
|
+
this.nullableOptionalProps = new Set(ir.metadata.propsParams.filter((p) => p.defaultValue === undefined && !p.isRest && p.type?.kind !== "primitive").map((p) => p.name));
|
|
108
129
|
this.stringValueNames = new Set;
|
|
109
130
|
for (const s of ir.metadata.signals) {
|
|
110
131
|
if (isStringTypeInfo(s.type) || isBareStringLiteral(s.initialValue)) {
|
|
@@ -115,6 +136,8 @@ class MojoAdapter extends BaseAdapter {
|
|
|
115
136
|
if (isStringTypeInfo(p.type))
|
|
116
137
|
this.stringValueNames.add(p.name);
|
|
117
138
|
}
|
|
139
|
+
this.moduleStringConsts = this.collectModuleStringConsts(ir.metadata.localConstants);
|
|
140
|
+
this.loopBoundNames.clear();
|
|
118
141
|
this.errors = [];
|
|
119
142
|
this.childrenCaptureCounter = 0;
|
|
120
143
|
if (!options?.siblingTemplatesRegistered) {
|
|
@@ -139,6 +162,27 @@ class MojoAdapter extends BaseAdapter {
|
|
|
139
162
|
extension: this.extension
|
|
140
163
|
};
|
|
141
164
|
}
|
|
165
|
+
collectModuleStringConsts(constants) {
|
|
166
|
+
const map = new Map;
|
|
167
|
+
for (const c of constants ?? []) {
|
|
168
|
+
if (!c.isModule)
|
|
169
|
+
continue;
|
|
170
|
+
if (c.value === undefined)
|
|
171
|
+
continue;
|
|
172
|
+
const literal = parsePureStringLiteral(c.value);
|
|
173
|
+
if (literal !== null)
|
|
174
|
+
map.set(c.name, literal);
|
|
175
|
+
}
|
|
176
|
+
return map;
|
|
177
|
+
}
|
|
178
|
+
resolveModuleStringConst(name) {
|
|
179
|
+
if (this.loopBoundNames.has(name))
|
|
180
|
+
return null;
|
|
181
|
+
const value = this.moduleStringConsts.get(name);
|
|
182
|
+
if (value === undefined)
|
|
183
|
+
return null;
|
|
184
|
+
return `'${value.replace(/[\\']/g, (m) => `\\${m}`)}'`;
|
|
185
|
+
}
|
|
142
186
|
generateScriptRegistrations(ir, scriptBaseName) {
|
|
143
187
|
const hasInteractivity = this.hasClientInteractivity(ir);
|
|
144
188
|
if (!hasInteractivity)
|
|
@@ -405,6 +449,10 @@ ${whenTrue}
|
|
|
405
449
|
}
|
|
406
450
|
const param = loop.param;
|
|
407
451
|
const indexVar = loop.iterationShape === "keys" ? `$${param}` : loop.index ? `$${loop.index}` : "$_i";
|
|
452
|
+
const loopBound = loop.iterationShape === "keys" ? [param] : [param, loop.index ?? "_i"];
|
|
453
|
+
for (const n of loopBound) {
|
|
454
|
+
this.loopBoundNames.set(n, (this.loopBoundNames.get(n) ?? 0) + 1);
|
|
455
|
+
}
|
|
408
456
|
const prevInLoop = this.inLoop;
|
|
409
457
|
this.inLoop = true;
|
|
410
458
|
const renderedChildren = this.renderChildren(loop.children);
|
|
@@ -438,6 +486,13 @@ ${renderedChildren}` : renderedChildren;
|
|
|
438
486
|
} else {
|
|
439
487
|
lines.push(children);
|
|
440
488
|
}
|
|
489
|
+
for (const n of loopBound) {
|
|
490
|
+
const c = (this.loopBoundNames.get(n) ?? 1) - 1;
|
|
491
|
+
if (c <= 0)
|
|
492
|
+
this.loopBoundNames.delete(n);
|
|
493
|
+
else
|
|
494
|
+
this.loopBoundNames.set(n, c);
|
|
495
|
+
}
|
|
441
496
|
lines.push(`% }`);
|
|
442
497
|
lines.push(`<%== bf->comment("/loop:${loop.markerId}") %>`);
|
|
443
498
|
return lines.join(`
|
|
@@ -529,6 +584,12 @@ ${children}`;
|
|
|
529
584
|
if (this.refuseUnsupportedAttrExpression(value.expr, name)) {
|
|
530
585
|
return "";
|
|
531
586
|
}
|
|
587
|
+
const bareId = value.expr.trim();
|
|
588
|
+
if (!isBooleanAttr(name) && !value.presenceOrUndefined && this.nullableOptionalProps.has(bareId)) {
|
|
589
|
+
const perl2 = this.convertExpressionToPerl(value.expr);
|
|
590
|
+
const body = isBooleanResultExpr(value.expr) || isAriaBooleanAttr(name) ? `${name}="<%= bf->bool_str(${perl2}) %>"` : `${name}="<%= ${perl2} %>"`;
|
|
591
|
+
return `<% if (defined ${perl2}) { %>${body}<% } %>`;
|
|
592
|
+
}
|
|
532
593
|
if (isBooleanAttr(name) || value.presenceOrUndefined) {
|
|
533
594
|
return `<%= ${this.convertExpressionToPerl(value.expr)} ? '${name}' : '' %>`;
|
|
534
595
|
}
|
|
@@ -549,6 +610,10 @@ ${children}`;
|
|
|
549
610
|
const entries = this.propsParams.map((p) => `${JSON.stringify(p.name)} => $${p.name}`);
|
|
550
611
|
return `<%== bf->spread_attrs({${entries.join(", ")}}) %>`;
|
|
551
612
|
}
|
|
613
|
+
const ternaryHashref = this.conditionalSpreadToPerl(trimmed);
|
|
614
|
+
if (ternaryHashref !== null) {
|
|
615
|
+
return `<%== bf->spread_attrs(${ternaryHashref}) %>`;
|
|
616
|
+
}
|
|
552
617
|
const perlExpr = this.convertExpressionToPerl(value.expr);
|
|
553
618
|
return `<%== bf->spread_attrs(${perlExpr}) %>`;
|
|
554
619
|
},
|
|
@@ -713,6 +778,54 @@ ${reason}` : "";
|
|
|
713
778
|
});
|
|
714
779
|
return true;
|
|
715
780
|
}
|
|
781
|
+
conditionalSpreadToPerl(expr) {
|
|
782
|
+
const sf = ts.createSourceFile("__spread.ts", `(${expr})`, ts.ScriptTarget.Latest, true);
|
|
783
|
+
if (sf.statements.length !== 1)
|
|
784
|
+
return null;
|
|
785
|
+
const stmt = sf.statements[0];
|
|
786
|
+
if (!ts.isExpressionStatement(stmt))
|
|
787
|
+
return null;
|
|
788
|
+
let node = stmt.expression;
|
|
789
|
+
while (ts.isParenthesizedExpression(node))
|
|
790
|
+
node = node.expression;
|
|
791
|
+
if (!ts.isConditionalExpression(node))
|
|
792
|
+
return null;
|
|
793
|
+
const unwrap = (e) => {
|
|
794
|
+
let n = e;
|
|
795
|
+
while (ts.isParenthesizedExpression(n))
|
|
796
|
+
n = n.expression;
|
|
797
|
+
return n;
|
|
798
|
+
};
|
|
799
|
+
const whenTrue = unwrap(node.whenTrue);
|
|
800
|
+
const whenFalse = unwrap(node.whenFalse);
|
|
801
|
+
if (!ts.isObjectLiteralExpression(whenTrue) || !ts.isObjectLiteralExpression(whenFalse)) {
|
|
802
|
+
return null;
|
|
803
|
+
}
|
|
804
|
+
const condPerl = this.convertExpressionToPerl(node.condition.getText(sf));
|
|
805
|
+
const truePerl = this.objectLiteralToPerlHashref(whenTrue, sf);
|
|
806
|
+
const falsePerl = this.objectLiteralToPerlHashref(whenFalse, sf);
|
|
807
|
+
if (truePerl === null || falsePerl === null)
|
|
808
|
+
return null;
|
|
809
|
+
return `${condPerl} ? ${truePerl} : ${falsePerl}`;
|
|
810
|
+
}
|
|
811
|
+
objectLiteralToPerlHashref(obj, sf) {
|
|
812
|
+
const entries = [];
|
|
813
|
+
for (const prop of obj.properties) {
|
|
814
|
+
if (!ts.isPropertyAssignment(prop))
|
|
815
|
+
return null;
|
|
816
|
+
let key;
|
|
817
|
+
if (ts.isIdentifier(prop.name)) {
|
|
818
|
+
key = prop.name.text;
|
|
819
|
+
} else if (ts.isStringLiteral(prop.name) || ts.isNoSubstitutionTemplateLiteral(prop.name)) {
|
|
820
|
+
key = prop.name.text;
|
|
821
|
+
} else {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
const valPerl = this.convertExpressionToPerl(prop.initializer.getText(sf));
|
|
825
|
+
entries.push(`'${key.replace(/'/g, "\\'")}' => ${valPerl}`);
|
|
826
|
+
}
|
|
827
|
+
return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
|
|
828
|
+
}
|
|
716
829
|
convertExpressionToPerl(expr) {
|
|
717
830
|
const trimmed = expr.trim();
|
|
718
831
|
if (trimmed === "")
|
|
@@ -1056,6 +1169,9 @@ class MojoTopLevelEmitter {
|
|
|
1056
1169
|
this.adapter = adapter;
|
|
1057
1170
|
}
|
|
1058
1171
|
identifier(name) {
|
|
1172
|
+
const inlined = this.adapter.resolveModuleStringConst(name);
|
|
1173
|
+
if (inlined !== null)
|
|
1174
|
+
return inlined;
|
|
1059
1175
|
return `$${name}`;
|
|
1060
1176
|
}
|
|
1061
1177
|
literal(value, literalType) {
|