@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.
Files changed (92) hide show
  1. package/CONTRIBUTING.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +194 -0
  4. package/bin/katachi.mjs +30 -0
  5. package/dist/api/index.d.ts +54 -0
  6. package/dist/api/index.js +45 -0
  7. package/dist/api/jsx.d.ts +26 -0
  8. package/dist/cli/index.d.ts +1 -0
  9. package/dist/cli/index.js +77 -0
  10. package/dist/core/ast.d.ts +115 -0
  11. package/dist/core/ast.js +51 -0
  12. package/dist/core/build.d.ts +15 -0
  13. package/dist/core/build.js +107 -0
  14. package/dist/core/compiler.d.ts +9 -0
  15. package/dist/core/compiler.js +9 -0
  16. package/dist/core/example-fixtures.d.ts +5 -0
  17. package/dist/core/example-fixtures.js +54 -0
  18. package/dist/core/parser.d.ts +5 -0
  19. package/dist/core/parser.js +637 -0
  20. package/dist/core/types.d.ts +65 -0
  21. package/dist/core/types.js +1 -0
  22. package/dist/core/verify.d.ts +25 -0
  23. package/dist/core/verify.js +270 -0
  24. package/dist/index.d.ts +7 -0
  25. package/dist/index.js +5 -0
  26. package/dist/targets/askama.d.ts +11 -0
  27. package/dist/targets/askama.js +122 -0
  28. package/dist/targets/index.d.ts +5 -0
  29. package/dist/targets/index.js +60 -0
  30. package/dist/targets/react.d.ts +4 -0
  31. package/dist/targets/react.js +28 -0
  32. package/dist/targets/shared.d.ts +36 -0
  33. package/dist/targets/shared.js +278 -0
  34. package/dist/targets/static-jsx.d.ts +4 -0
  35. package/dist/targets/static-jsx.js +28 -0
  36. package/dist/verify-examples.d.ts +1 -0
  37. package/dist/verify-examples.js +14 -0
  38. package/docs/architecture.md +122 -0
  39. package/docs/getting-started.md +154 -0
  40. package/docs/syntax.md +236 -0
  41. package/docs/targets.md +53 -0
  42. package/examples/basic/README.md +67 -0
  43. package/examples/basic/components/badge-chip.html +3 -0
  44. package/examples/basic/components/comparison-table.html +24 -0
  45. package/examples/basic/components/glyph.html +6 -0
  46. package/examples/basic/components/hover-note.html +6 -0
  47. package/examples/basic/components/media-frame.html +15 -0
  48. package/examples/basic/components/notice-panel.html +24 -0
  49. package/examples/basic/components/resource-tile.html +24 -0
  50. package/examples/basic/components/stack-shell.html +3 -0
  51. package/examples/basic/dist/askama/badge-chip.rs +18 -0
  52. package/examples/basic/dist/askama/comparison-table.rs +47 -0
  53. package/examples/basic/dist/askama/glyph.rs +21 -0
  54. package/examples/basic/dist/askama/hover-note.rs +19 -0
  55. package/examples/basic/dist/askama/includes/badge-chip.html +5 -0
  56. package/examples/basic/dist/askama/includes/comparison-table.html +34 -0
  57. package/examples/basic/dist/askama/includes/glyph.html +6 -0
  58. package/examples/basic/dist/askama/includes/hover-note.html +6 -0
  59. package/examples/basic/dist/askama/includes/media-frame.html +23 -0
  60. package/examples/basic/dist/askama/includes/notice-panel.html +34 -0
  61. package/examples/basic/dist/askama/includes/resource-tile.html +42 -0
  62. package/examples/basic/dist/askama/includes/stack-shell.html +5 -0
  63. package/examples/basic/dist/askama/media-frame.rs +37 -0
  64. package/examples/basic/dist/askama/notice-panel.rs +49 -0
  65. package/examples/basic/dist/askama/resource-tile.rs +59 -0
  66. package/examples/basic/dist/askama/stack-shell.rs +17 -0
  67. package/examples/basic/dist/jsx-static/badge-chip.tsx +18 -0
  68. package/examples/basic/dist/jsx-static/comparison-table.tsx +53 -0
  69. package/examples/basic/dist/jsx-static/glyph.tsx +21 -0
  70. package/examples/basic/dist/jsx-static/hover-note.tsx +19 -0
  71. package/examples/basic/dist/jsx-static/media-frame.tsx +41 -0
  72. package/examples/basic/dist/jsx-static/notice-panel.tsx +53 -0
  73. package/examples/basic/dist/jsx-static/resource-tile.tsx +63 -0
  74. package/examples/basic/dist/jsx-static/stack-shell.tsx +17 -0
  75. package/examples/basic/dist/react/badge-chip.tsx +18 -0
  76. package/examples/basic/dist/react/comparison-table.tsx +53 -0
  77. package/examples/basic/dist/react/glyph.tsx +21 -0
  78. package/examples/basic/dist/react/hover-note.tsx +19 -0
  79. package/examples/basic/dist/react/media-frame.tsx +41 -0
  80. package/examples/basic/dist/react/notice-panel.tsx +53 -0
  81. package/examples/basic/dist/react/resource-tile.tsx +63 -0
  82. package/examples/basic/dist/react/stack-shell.tsx +17 -0
  83. package/examples/basic/src/templates/badge-chip.template.tsx +18 -0
  84. package/examples/basic/src/templates/comparison-table.template.tsx +35 -0
  85. package/examples/basic/src/templates/glyph.template.tsx +17 -0
  86. package/examples/basic/src/templates/hover-note.template.tsx +17 -0
  87. package/examples/basic/src/templates/media-frame.template.tsx +25 -0
  88. package/examples/basic/src/templates/notice-panel.template.tsx +40 -0
  89. package/examples/basic/src/templates/resource-tile.template.tsx +51 -0
  90. package/examples/basic/src/templates/stack-shell.template.tsx +13 -0
  91. package/examples/basic/tsconfig.json +10 -0
  92. 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,4 @@
1
+ import type { Node } from "../core/ast.js";
2
+ import type { BuildTemplate } from "../core/types.js";
3
+ export declare function emitStaticJsx(node: Node, indent?: number): string;
4
+ export declare function emitStaticJsxComponent(template: BuildTemplate): string;
@@ -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.