@relevate/katachi 0.1.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/CONTRIBUTING.md +60 -0
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/bin/katachi.mjs +30 -0
- package/dist/api/index.d.ts +54 -0
- package/dist/api/index.js +45 -0
- package/dist/api/jsx.d.ts +26 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +77 -0
- package/dist/core/ast.d.ts +115 -0
- package/dist/core/ast.js +51 -0
- package/dist/core/build.d.ts +15 -0
- package/dist/core/build.js +107 -0
- package/dist/core/compiler.d.ts +9 -0
- package/dist/core/compiler.js +9 -0
- package/dist/core/example-fixtures.d.ts +5 -0
- package/dist/core/example-fixtures.js +54 -0
- package/dist/core/parser.d.ts +5 -0
- package/dist/core/parser.js +637 -0
- package/dist/core/types.d.ts +65 -0
- package/dist/core/types.js +1 -0
- package/dist/core/verify.d.ts +25 -0
- package/dist/core/verify.js +270 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/targets/askama.d.ts +11 -0
- package/dist/targets/askama.js +122 -0
- package/dist/targets/index.d.ts +5 -0
- package/dist/targets/index.js +60 -0
- package/dist/targets/react.d.ts +4 -0
- package/dist/targets/react.js +28 -0
- package/dist/targets/shared.d.ts +36 -0
- package/dist/targets/shared.js +278 -0
- package/dist/targets/static-jsx.d.ts +4 -0
- package/dist/targets/static-jsx.js +28 -0
- package/dist/verify-examples.d.ts +1 -0
- package/dist/verify-examples.js +14 -0
- package/docs/architecture.md +122 -0
- package/docs/getting-started.md +154 -0
- package/docs/syntax.md +236 -0
- package/docs/targets.md +53 -0
- package/examples/basic/README.md +67 -0
- package/examples/basic/components/badge-chip.html +3 -0
- package/examples/basic/components/comparison-table.html +24 -0
- package/examples/basic/components/glyph.html +6 -0
- package/examples/basic/components/hover-note.html +6 -0
- package/examples/basic/components/media-frame.html +15 -0
- package/examples/basic/components/notice-panel.html +24 -0
- package/examples/basic/components/resource-tile.html +24 -0
- package/examples/basic/components/stack-shell.html +3 -0
- package/examples/basic/dist/askama/badge-chip.rs +18 -0
- package/examples/basic/dist/askama/comparison-table.rs +47 -0
- package/examples/basic/dist/askama/glyph.rs +21 -0
- package/examples/basic/dist/askama/hover-note.rs +19 -0
- package/examples/basic/dist/askama/includes/badge-chip.html +5 -0
- package/examples/basic/dist/askama/includes/comparison-table.html +34 -0
- package/examples/basic/dist/askama/includes/glyph.html +6 -0
- package/examples/basic/dist/askama/includes/hover-note.html +6 -0
- package/examples/basic/dist/askama/includes/media-frame.html +23 -0
- package/examples/basic/dist/askama/includes/notice-panel.html +34 -0
- package/examples/basic/dist/askama/includes/resource-tile.html +42 -0
- package/examples/basic/dist/askama/includes/stack-shell.html +5 -0
- package/examples/basic/dist/askama/media-frame.rs +37 -0
- package/examples/basic/dist/askama/notice-panel.rs +49 -0
- package/examples/basic/dist/askama/resource-tile.rs +59 -0
- package/examples/basic/dist/askama/stack-shell.rs +17 -0
- package/examples/basic/dist/jsx-static/badge-chip.tsx +18 -0
- package/examples/basic/dist/jsx-static/comparison-table.tsx +53 -0
- package/examples/basic/dist/jsx-static/glyph.tsx +21 -0
- package/examples/basic/dist/jsx-static/hover-note.tsx +19 -0
- package/examples/basic/dist/jsx-static/media-frame.tsx +41 -0
- package/examples/basic/dist/jsx-static/notice-panel.tsx +53 -0
- package/examples/basic/dist/jsx-static/resource-tile.tsx +63 -0
- package/examples/basic/dist/jsx-static/stack-shell.tsx +17 -0
- package/examples/basic/dist/react/badge-chip.tsx +18 -0
- package/examples/basic/dist/react/comparison-table.tsx +53 -0
- package/examples/basic/dist/react/glyph.tsx +21 -0
- package/examples/basic/dist/react/hover-note.tsx +19 -0
- package/examples/basic/dist/react/media-frame.tsx +41 -0
- package/examples/basic/dist/react/notice-panel.tsx +53 -0
- package/examples/basic/dist/react/resource-tile.tsx +63 -0
- package/examples/basic/dist/react/stack-shell.tsx +17 -0
- package/examples/basic/src/templates/badge-chip.template.tsx +18 -0
- package/examples/basic/src/templates/comparison-table.template.tsx +35 -0
- package/examples/basic/src/templates/glyph.template.tsx +17 -0
- package/examples/basic/src/templates/hover-note.template.tsx +17 -0
- package/examples/basic/src/templates/media-frame.template.tsx +25 -0
- package/examples/basic/src/templates/notice-panel.template.tsx +40 -0
- package/examples/basic/src/templates/resource-tile.template.tsx +51 -0
- package/examples/basic/src/templates/stack-shell.template.tsx +13 -0
- package/examples/basic/tsconfig.json +10 -0
- package/package.json +69 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes a string for insertion into Rust string literals used by Askama wrappers.
|
|
3
|
+
*/
|
|
4
|
+
function escapeDoubleQuotes(value) {
|
|
5
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Chooses a stable quote style for generated HTML attributes so mixed template
|
|
9
|
+
* syntax stays readable and editor highlighting does not break.
|
|
10
|
+
*/
|
|
11
|
+
function wrapHtmlAttribute(value) {
|
|
12
|
+
if (!value.includes("'")) {
|
|
13
|
+
return `'${value}'`;
|
|
14
|
+
}
|
|
15
|
+
if (!value.includes('"')) {
|
|
16
|
+
return `"${value}"`;
|
|
17
|
+
}
|
|
18
|
+
return `'${value.replace(/'/g, "'")}'`;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Best-effort translation of Rust-ish template expressions into TSX-friendly syntax.
|
|
22
|
+
*
|
|
23
|
+
* This exists to support incremental Askama migrations. It is not the ideal
|
|
24
|
+
* final authoring model, but it keeps existing ports workable.
|
|
25
|
+
*/
|
|
26
|
+
function translateRustExprToTsx(source) {
|
|
27
|
+
let result = source.trim();
|
|
28
|
+
result = result.replace(/([A-Za-z0-9_().]+)\.clone\(\)\.unwrap\(\)/g, "$1");
|
|
29
|
+
result = result.replace(/([A-Za-z0-9_().]+)\.unwrap\(\)/g, "$1");
|
|
30
|
+
result = result.replace(/([A-Za-z0-9_().]+)\.is_some\(\)/g, "($1 != null)");
|
|
31
|
+
result = result.replace(/([A-Za-z0-9_().]+)\.is_none\(\)/g, "($1 == null)");
|
|
32
|
+
result = result.replace(/!\s*([A-Za-z0-9_().]+)\.is_empty\(\)/g, "($1.length > 0)");
|
|
33
|
+
result = result.replace(/([A-Za-z0-9_().]+)\.is_empty\(\)/g, "($1.length === 0)");
|
|
34
|
+
result = result.replace(/([A-Za-z0-9_().]+)\.len\(\)/g, "$1.length");
|
|
35
|
+
result = result.replace(/\s===\s/g, " === ");
|
|
36
|
+
result = result.replace(/\s!==\s/g, " !== ");
|
|
37
|
+
result = result.replace(/\s==\s/g, " === ");
|
|
38
|
+
result = result.replace(/\s!=\s/g, " !== ");
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Rewrites JS/TS equality operators into Askama-compatible syntax.
|
|
43
|
+
*/
|
|
44
|
+
function translateTsxExprToAskama(source) {
|
|
45
|
+
return source.replace(/\s===\s/g, " == ").replace(/\s!==\s/g, " != ");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Emits a portable expression into TSX/React-family output.
|
|
49
|
+
*/
|
|
50
|
+
export function emitTsxExpr(expr) {
|
|
51
|
+
switch (expr.kind) {
|
|
52
|
+
case "var":
|
|
53
|
+
return expr.name;
|
|
54
|
+
case "string":
|
|
55
|
+
return JSON.stringify(expr.value);
|
|
56
|
+
case "bool":
|
|
57
|
+
return expr.value ? "true" : "false";
|
|
58
|
+
case "number":
|
|
59
|
+
return String(expr.value);
|
|
60
|
+
case "intrinsic": {
|
|
61
|
+
const [arg] = expr.args;
|
|
62
|
+
const emittedArg = arg ? emitTsxExpr(arg) : "undefined";
|
|
63
|
+
switch (expr.name) {
|
|
64
|
+
case "len":
|
|
65
|
+
return `(${emittedArg}?.length ?? 0)`;
|
|
66
|
+
case "isEmpty":
|
|
67
|
+
return `((${emittedArg}?.length ?? 0) === 0)`;
|
|
68
|
+
case "isSome":
|
|
69
|
+
return `(${emittedArg} != null)`;
|
|
70
|
+
case "isNone":
|
|
71
|
+
return `(${emittedArg} == null)`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
case "raw":
|
|
75
|
+
return translateRustExprToTsx(expr.source);
|
|
76
|
+
case "eq":
|
|
77
|
+
return `(${emitTsxExpr(expr.left)} === ${emitTsxExpr(expr.right)})`;
|
|
78
|
+
case "neq":
|
|
79
|
+
return `(${emitTsxExpr(expr.left)} !== ${emitTsxExpr(expr.right)})`;
|
|
80
|
+
case "and":
|
|
81
|
+
return `(${emitTsxExpr(expr.left)} && ${emitTsxExpr(expr.right)})`;
|
|
82
|
+
case "or":
|
|
83
|
+
return `(${emitTsxExpr(expr.left)} || ${emitTsxExpr(expr.right)})`;
|
|
84
|
+
case "not":
|
|
85
|
+
return `!(${emitTsxExpr(expr.expr)})`;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Emits a portable expression into Askama syntax.
|
|
90
|
+
*/
|
|
91
|
+
export function emitAskamaExpr(expr) {
|
|
92
|
+
switch (expr.kind) {
|
|
93
|
+
case "var":
|
|
94
|
+
return expr.name;
|
|
95
|
+
case "string":
|
|
96
|
+
return `"${escapeDoubleQuotes(expr.value)}"`;
|
|
97
|
+
case "bool":
|
|
98
|
+
return expr.value ? "true" : "false";
|
|
99
|
+
case "number":
|
|
100
|
+
return String(expr.value);
|
|
101
|
+
case "intrinsic": {
|
|
102
|
+
const [arg] = expr.args;
|
|
103
|
+
const emittedArg = arg ? emitAskamaExpr(arg) : "";
|
|
104
|
+
switch (expr.name) {
|
|
105
|
+
case "len":
|
|
106
|
+
return `${emittedArg}.len()`;
|
|
107
|
+
case "isEmpty":
|
|
108
|
+
return `${emittedArg}.is_empty()`;
|
|
109
|
+
case "isSome":
|
|
110
|
+
return `${emittedArg}.is_some()`;
|
|
111
|
+
case "isNone":
|
|
112
|
+
return `${emittedArg}.is_none()`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
case "raw":
|
|
116
|
+
return translateTsxExprToAskama(expr.source);
|
|
117
|
+
case "eq":
|
|
118
|
+
return `${emitAskamaExpr(expr.left)} == ${emitAskamaExpr(expr.right)}`;
|
|
119
|
+
case "neq":
|
|
120
|
+
return `${emitAskamaExpr(expr.left)} != ${emitAskamaExpr(expr.right)}`;
|
|
121
|
+
case "and":
|
|
122
|
+
return `${emitAskamaExpr(expr.left)} && ${emitAskamaExpr(expr.right)}`;
|
|
123
|
+
case "or":
|
|
124
|
+
return `${emitAskamaExpr(expr.left)} || ${emitAskamaExpr(expr.right)}`;
|
|
125
|
+
case "not":
|
|
126
|
+
return `!(${emitAskamaExpr(expr.expr)})`;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Shared JSX/TSX tree emitter used by both React and static JSX targets.
|
|
131
|
+
*/
|
|
132
|
+
export function emitTsxNode(node, emitAttr, indent = 0) {
|
|
133
|
+
const pad = " ".repeat(indent);
|
|
134
|
+
switch (node.kind) {
|
|
135
|
+
case "text":
|
|
136
|
+
return `${pad}${node.value}`;
|
|
137
|
+
case "slot":
|
|
138
|
+
return `${pad}{${node.name}}`;
|
|
139
|
+
case "print":
|
|
140
|
+
return `${pad}{${emitTsxExpr(node.expr)}}`;
|
|
141
|
+
case "if": {
|
|
142
|
+
const thenPart = node.then
|
|
143
|
+
.map((child) => emitTsxNode(child, emitAttr, indent + 2))
|
|
144
|
+
.join("\n");
|
|
145
|
+
const elsePart = (node.else ?? [])
|
|
146
|
+
.map((child) => emitTsxNode(child, emitAttr, indent + 2))
|
|
147
|
+
.join("\n");
|
|
148
|
+
if (elsePart) {
|
|
149
|
+
return `${pad}{${emitTsxExpr(node.test)} ? (\n${pad} <>\n${thenPart}\n${pad} </>\n${pad}) : (\n${pad} <>\n${elsePart}\n${pad} </>\n${pad})}`;
|
|
150
|
+
}
|
|
151
|
+
return `${pad}{${emitTsxExpr(node.test)} && (\n${pad} <>\n${thenPart}\n${pad} </>\n${pad})}`;
|
|
152
|
+
}
|
|
153
|
+
case "for": {
|
|
154
|
+
const eachExpr = emitTsxExpr(node.each);
|
|
155
|
+
const iteratorArgs = node.indexName
|
|
156
|
+
? `${node.item}, ${node.indexName}`
|
|
157
|
+
: `${node.item}, __index`;
|
|
158
|
+
const body = node.children
|
|
159
|
+
.map((child) => emitTsxNode(child, emitAttr, indent + 2))
|
|
160
|
+
.join("\n");
|
|
161
|
+
return `${pad}{(${eachExpr} ?? []).map((${iteratorArgs}) => (\n${pad} <>\n${body}\n${pad} </>\n${pad}))}`;
|
|
162
|
+
}
|
|
163
|
+
case "element": {
|
|
164
|
+
const attrEntries = Object.entries(node.attrs ?? {});
|
|
165
|
+
const multilineOpen = `${pad}<${node.tag}\n${attrEntries
|
|
166
|
+
.map(([name, value]) => `${pad} ${emitAttr(name, value)}`)
|
|
167
|
+
.join("\n")}\n${pad}>`;
|
|
168
|
+
const children = (node.children ?? []).map((child) => emitTsxNode(child, emitAttr, indent + 1));
|
|
169
|
+
if (children.length === 0) {
|
|
170
|
+
if (attrEntries.length === 0) {
|
|
171
|
+
return `${pad}<${node.tag} />`;
|
|
172
|
+
}
|
|
173
|
+
return `${pad}<${node.tag}\n${attrEntries
|
|
174
|
+
.map(([name, value]) => `${pad} ${emitAttr(name, value)}`)
|
|
175
|
+
.join("\n")}\n${pad}/>`;
|
|
176
|
+
}
|
|
177
|
+
if (attrEntries.length === 0) {
|
|
178
|
+
return `${pad}<${node.tag}>\n${children.join("\n")}\n${pad}</${node.tag}>`;
|
|
179
|
+
}
|
|
180
|
+
return `${multilineOpen}\n${children.join("\n")}\n${pad}</${node.tag}>`;
|
|
181
|
+
}
|
|
182
|
+
case "component": {
|
|
183
|
+
const propEntries = Object.entries(node.props ?? {});
|
|
184
|
+
const multilineOpen = `${pad}<${node.name}\n${propEntries
|
|
185
|
+
.map(([name, value]) => `${pad} ${emitAttr(name, value)}`)
|
|
186
|
+
.join("\n")}\n${pad}>`;
|
|
187
|
+
const children = (node.children ?? []).map((child) => emitTsxNode(child, emitAttr, indent + 1));
|
|
188
|
+
if (children.length === 0) {
|
|
189
|
+
if (propEntries.length === 0) {
|
|
190
|
+
return `${pad}<${node.name} />`;
|
|
191
|
+
}
|
|
192
|
+
return `${pad}<${node.name}\n${propEntries
|
|
193
|
+
.map(([name, value]) => `${pad} ${emitAttr(name, value)}`)
|
|
194
|
+
.join("\n")}\n${pad}/>`;
|
|
195
|
+
}
|
|
196
|
+
if (propEntries.length === 0) {
|
|
197
|
+
return `${pad}<${node.name}>\n${children.join("\n")}\n${pad}</${node.name}>`;
|
|
198
|
+
}
|
|
199
|
+
return `${multilineOpen}\n${children.join("\n")}\n${pad}</${node.name}>`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function toPascalCase(value) {
|
|
204
|
+
return value
|
|
205
|
+
.replace(/(^|[-_ ]+)([a-zA-Z0-9])/g, (_match, _sep, char) => char.toUpperCase())
|
|
206
|
+
.replace(/[^a-zA-Z0-9]/g, "");
|
|
207
|
+
}
|
|
208
|
+
export function toCamelCase(value) {
|
|
209
|
+
const pascal = toPascalCase(value);
|
|
210
|
+
return pascal.length === 0 ? pascal : pascal[0].toLowerCase() + pascal.slice(1);
|
|
211
|
+
}
|
|
212
|
+
export function toRustType(type) {
|
|
213
|
+
switch (type) {
|
|
214
|
+
case "string":
|
|
215
|
+
return "&'a str";
|
|
216
|
+
case "bool":
|
|
217
|
+
return "bool";
|
|
218
|
+
case "number":
|
|
219
|
+
return "i64";
|
|
220
|
+
case "children":
|
|
221
|
+
return "&'a str";
|
|
222
|
+
case "string[]":
|
|
223
|
+
return "&'a [&'a str]";
|
|
224
|
+
case "string[][]":
|
|
225
|
+
return "&'a [&'a [&'a str]]";
|
|
226
|
+
default:
|
|
227
|
+
return "&'a str";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
export function toTsType(type) {
|
|
231
|
+
switch (type) {
|
|
232
|
+
case "string":
|
|
233
|
+
return "string";
|
|
234
|
+
case "bool":
|
|
235
|
+
return "boolean";
|
|
236
|
+
case "number":
|
|
237
|
+
return "number";
|
|
238
|
+
case "children":
|
|
239
|
+
return "ReactNode";
|
|
240
|
+
default:
|
|
241
|
+
return type;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Builds import lines for nested template components in TSX-family targets.
|
|
246
|
+
*/
|
|
247
|
+
export function buildTsxImportLines(template) {
|
|
248
|
+
return (template.imports ?? [])
|
|
249
|
+
.map((entry) => template.componentRegistry?.[entry.localName]?.reactImport
|
|
250
|
+
? `import ${entry.localName} from "${template.componentRegistry[entry.localName].reactImport}";`
|
|
251
|
+
: null)
|
|
252
|
+
.filter((line) => Boolean(line))
|
|
253
|
+
.join("\n");
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Wraps an emitted TSX template body in a component module.
|
|
257
|
+
*/
|
|
258
|
+
export function buildTsxComponentSource(template, body) {
|
|
259
|
+
const props = template.props ?? [];
|
|
260
|
+
const propsTypeName = `${template.name}Props`;
|
|
261
|
+
const propLines = props.map((prop) => ` ${prop.name}${prop.optional ? "?" : ""}: ${toTsType(prop.type)};`);
|
|
262
|
+
const destructuredProps = props.map((prop) => prop.name).join(", ");
|
|
263
|
+
const componentImports = buildTsxImportLines(template);
|
|
264
|
+
return `import type { ReactNode } from "react";
|
|
265
|
+
${componentImports ? `${componentImports}\n` : ""}
|
|
266
|
+
|
|
267
|
+
export type ${propsTypeName} = {
|
|
268
|
+
${propLines.join("\n")}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
export default function ${template.name}({ ${destructuredProps} }: ${propsTypeName}) {
|
|
272
|
+
return (
|
|
273
|
+
${body}
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
export { escapeDoubleQuotes, wrapHtmlAttribute };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { buildTsxComponentSource, emitTsxExpr, emitTsxNode } from "./shared.js";
|
|
2
|
+
/**
|
|
3
|
+
* Emits TSX meant to read more statically by inlining class string interpolation.
|
|
4
|
+
*/
|
|
5
|
+
function emitStaticJsxAttr(name, value) {
|
|
6
|
+
const attrName = name === "class" ? "className" : name;
|
|
7
|
+
switch (value.kind) {
|
|
8
|
+
case "text":
|
|
9
|
+
return `${attrName}=${JSON.stringify(value.value)}`;
|
|
10
|
+
case "expr":
|
|
11
|
+
return `${attrName}={${emitTsxExpr(value.expr)}}`;
|
|
12
|
+
case "classList": {
|
|
13
|
+
const segments = value.items.map((item) => {
|
|
14
|
+
if (item.kind === "static") {
|
|
15
|
+
return item.value;
|
|
16
|
+
}
|
|
17
|
+
return `\${${emitTsxExpr(item.test)} ? ${JSON.stringify(item.value)} : ""}`;
|
|
18
|
+
});
|
|
19
|
+
return `${attrName}={\`${segments.join(" ").trim()}\`}`;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function emitStaticJsx(node, indent = 0) {
|
|
24
|
+
return emitTsxNode(node, emitStaticJsxAttr, indent);
|
|
25
|
+
}
|
|
26
|
+
export function emitStaticJsxComponent(template) {
|
|
27
|
+
return buildTsxComponentSource(template, emitStaticJsx(template.template, 2));
|
|
28
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { buildProject } from "./core/build.js";
|
|
2
|
+
import { verifyAskamaFixtures } from "./core/verify.js";
|
|
3
|
+
import { basicExampleRoot, exampleFixtures } from "./core/example-fixtures.js";
|
|
4
|
+
/**
|
|
5
|
+
* Verifies a small public Askama fixture set that ships with the repository.
|
|
6
|
+
* This is intended for OSS consumers and repo contributors as the public
|
|
7
|
+
* end-to-end smoke test project.
|
|
8
|
+
*/
|
|
9
|
+
buildProject({
|
|
10
|
+
projectRoot: basicExampleRoot,
|
|
11
|
+
});
|
|
12
|
+
verifyAskamaFixtures({
|
|
13
|
+
fixtures: exampleFixtures,
|
|
14
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
This document is for contributors working on Katachi itself.
|
|
4
|
+
|
|
5
|
+
Katachi has four major layers:
|
|
6
|
+
|
|
7
|
+
1. Authoring input
|
|
8
|
+
2. Parser
|
|
9
|
+
3. Portable AST
|
|
10
|
+
4. Target emitters
|
|
11
|
+
|
|
12
|
+
## Flow
|
|
13
|
+
|
|
14
|
+
```txt
|
|
15
|
+
src/templates/**/*.template.tsx
|
|
16
|
+
-> core/parser.ts
|
|
17
|
+
-> core/ast.ts node model
|
|
18
|
+
-> target emitters
|
|
19
|
+
-> dist/*
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Canonical source
|
|
23
|
+
|
|
24
|
+
The canonical source is restricted TSX, not React semantics and not Askama syntax.
|
|
25
|
+
|
|
26
|
+
That distinction matters:
|
|
27
|
+
|
|
28
|
+
- authoring uses TSX because it is ergonomic
|
|
29
|
+
- the compiler owns the meaning of that TSX subset
|
|
30
|
+
- outputs are generated for each target separately
|
|
31
|
+
|
|
32
|
+
## AST
|
|
33
|
+
|
|
34
|
+
[src/core/ast.ts](../src/core/ast.ts) defines the portable template model.
|
|
35
|
+
[src/core/types.ts](../src/core/types.ts) defines the shared compiler interfaces around that model.
|
|
36
|
+
|
|
37
|
+
Key node kinds:
|
|
38
|
+
|
|
39
|
+
- `text`
|
|
40
|
+
- `slot`
|
|
41
|
+
- `print`
|
|
42
|
+
- `if`
|
|
43
|
+
- `for`
|
|
44
|
+
- `element`
|
|
45
|
+
- `component`
|
|
46
|
+
|
|
47
|
+
Key attribute kinds:
|
|
48
|
+
|
|
49
|
+
- `text`
|
|
50
|
+
- `expr`
|
|
51
|
+
- `classList`
|
|
52
|
+
|
|
53
|
+
This AST is the real source of truth once parsing is complete.
|
|
54
|
+
|
|
55
|
+
## Parser
|
|
56
|
+
|
|
57
|
+
[src/core/parser.ts](../src/core/parser.ts) lowers restricted TSX into AST nodes.
|
|
58
|
+
|
|
59
|
+
Responsibilities:
|
|
60
|
+
|
|
61
|
+
- parse elements and attributes
|
|
62
|
+
- normalize `className` to internal `class`
|
|
63
|
+
- convert `If` and `For`
|
|
64
|
+
- convert `{children}` to slot nodes
|
|
65
|
+
- convert `{safe(value)}` to safe print nodes
|
|
66
|
+
- resolve imported template components later during build
|
|
67
|
+
|
|
68
|
+
The parser is currently handwritten and string-based. That is fine for a prototype, but a stronger long-term direction is to parse real TSX via Babel, SWC, or the TypeScript compiler and then lower from that AST.
|
|
69
|
+
|
|
70
|
+
## Targets
|
|
71
|
+
|
|
72
|
+
Each output format gets its own emitter module:
|
|
73
|
+
|
|
74
|
+
- [src/targets/react.ts](../src/targets/react.ts)
|
|
75
|
+
- [src/targets/static-jsx.ts](../src/targets/static-jsx.ts)
|
|
76
|
+
- [src/targets/askama.ts](../src/targets/askama.ts)
|
|
77
|
+
|
|
78
|
+
Shared cross-target emitter helpers live in:
|
|
79
|
+
|
|
80
|
+
- [src/targets/shared.ts](../src/targets/shared.ts)
|
|
81
|
+
|
|
82
|
+
The build registry is:
|
|
83
|
+
|
|
84
|
+
- [src/targets/index.ts](../src/targets/index.ts)
|
|
85
|
+
|
|
86
|
+
## Build entrypoint
|
|
87
|
+
|
|
88
|
+
[src/core/build.ts](../src/core/build.ts) does project-level orchestration:
|
|
89
|
+
|
|
90
|
+
- scan template files
|
|
91
|
+
- parse all templates
|
|
92
|
+
- resolve template imports
|
|
93
|
+
- build per-template component registries
|
|
94
|
+
- invoke each configured output target
|
|
95
|
+
- write files into `dist/`
|
|
96
|
+
|
|
97
|
+
## Verification
|
|
98
|
+
|
|
99
|
+
[src/core/verify.ts](../src/core/verify.ts) compares generated Askama partials against expected Askama fixtures.
|
|
100
|
+
|
|
101
|
+
That comparison uses normalization so it can separate:
|
|
102
|
+
|
|
103
|
+
- exact match
|
|
104
|
+
- format-only differences
|
|
105
|
+
- functional differences
|
|
106
|
+
|
|
107
|
+
This is currently the main regression safety net for Askama output.
|
|
108
|
+
|
|
109
|
+
## Design principles
|
|
110
|
+
|
|
111
|
+
- one canonical semantic representation
|
|
112
|
+
- targets own their own emission strategy
|
|
113
|
+
- authoring should be ergonomic
|
|
114
|
+
- compiler internals should stay target-agnostic where possible
|
|
115
|
+
- generated output should become more target-idiomatic over time
|
|
116
|
+
|
|
117
|
+
## Near-term architecture improvements
|
|
118
|
+
|
|
119
|
+
- replace the handwritten parser with a real TSX AST pipeline
|
|
120
|
+
- formalize the target interface further
|
|
121
|
+
- add a reusable programmatic API separate from the build script
|
|
122
|
+
- decide whether to keep a single-package repo or split `core` and `cli` packages later
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
This guide is for using Katachi in your own project, not for working on the
|
|
4
|
+
Katachi repository itself.
|
|
5
|
+
|
|
6
|
+
## 1. Install Katachi
|
|
7
|
+
|
|
8
|
+
With pnpm:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pnpm add -D @relevate/katachi
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
With npm:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install --save-dev @relevate/katachi
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
If you only want to try it without installing it first:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm dlx @relevate/katachi build
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
or:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npx @relevate/katachi build
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 2. Update `tsconfig.json`
|
|
33
|
+
|
|
34
|
+
Katachi templates use TSX syntax, but they are not normal React components.
|
|
35
|
+
Tell TypeScript to load Katachi's JSX typing layer:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"compilerOptions": {
|
|
40
|
+
"jsx": "preserve",
|
|
41
|
+
"types": ["node", "@relevate/katachi/jsx"]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If you already use a `types` array, append `@relevate/katachi/jsx` instead of
|
|
47
|
+
replacing your existing entries.
|
|
48
|
+
|
|
49
|
+
## 3. Create a template
|
|
50
|
+
|
|
51
|
+
By default, Katachi reads from:
|
|
52
|
+
|
|
53
|
+
```txt
|
|
54
|
+
src/templates/**/*.template.tsx
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { If, type TemplateNode } from "@relevate/katachi";
|
|
61
|
+
|
|
62
|
+
export type Props = {
|
|
63
|
+
tone: "calm" | "urgent";
|
|
64
|
+
title: string;
|
|
65
|
+
children?: TemplateNode;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export default function NoticePanel({ tone, title, children }: Props) {
|
|
69
|
+
return (
|
|
70
|
+
<aside
|
|
71
|
+
className={[
|
|
72
|
+
"rounded-3xl border px-5 py-4",
|
|
73
|
+
tone == "calm" && "border-sky-200 bg-sky-50/80",
|
|
74
|
+
tone == "urgent" && "border-rose-200 bg-rose-50/80",
|
|
75
|
+
]}
|
|
76
|
+
>
|
|
77
|
+
<h3>{title}</h3>
|
|
78
|
+
<If test={tone == "urgent"}>
|
|
79
|
+
<p>Action recommended</p>
|
|
80
|
+
</If>
|
|
81
|
+
{children}
|
|
82
|
+
</aside>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 4. Build outputs
|
|
88
|
+
|
|
89
|
+
From your project root:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pnpm exec katachi build
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Without installing locally:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pnpm dlx @relevate/katachi build
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
With npm:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
npx @relevate/katachi build
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
You can also point Katachi at custom paths:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pnpm exec katachi build --templates ./katachi/templates --dist ./generated
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## 5. Consume the generated files
|
|
114
|
+
|
|
115
|
+
By default, Katachi writes:
|
|
116
|
+
|
|
117
|
+
- `dist/react/**/*.tsx`
|
|
118
|
+
- `dist/jsx-static/**/*.tsx`
|
|
119
|
+
- `dist/askama/**/*.rs`
|
|
120
|
+
- `dist/askama/includes/**/*.html`
|
|
121
|
+
|
|
122
|
+
Typical usage:
|
|
123
|
+
|
|
124
|
+
- use `dist/react` in your editor or React app
|
|
125
|
+
- use `dist/askama` and `dist/askama/includes` in your Rust/Askama app
|
|
126
|
+
|
|
127
|
+
If you are evaluating Katachi for a shared component library, this is the
|
|
128
|
+
normal model: author once, then consume the generated output from each target
|
|
129
|
+
environment.
|
|
130
|
+
|
|
131
|
+
## Nested components
|
|
132
|
+
|
|
133
|
+
Import other Katachi templates with the `.template` path:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import Glyph from "./glyph.template";
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Then use them as normal capitalized TSX components:
|
|
140
|
+
|
|
141
|
+
```tsx
|
|
142
|
+
<Glyph name="spark" size="16" tone="calm" className="h-4 w-4" />
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Katachi resolves those imports into:
|
|
146
|
+
|
|
147
|
+
- TSX imports for React output
|
|
148
|
+
- Askama include wiring for Askama output
|
|
149
|
+
|
|
150
|
+
## Next steps
|
|
151
|
+
|
|
152
|
+
- See [syntax.md](./syntax.md) for the supported syntax.
|
|
153
|
+
- See [targets.md](./targets.md) for output details.
|
|
154
|
+
- See [../examples/basic/README.md](../examples/basic/README.md) for a fuller consumer-style example.
|