@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
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for taking an interest in Katachi.
|
|
4
|
+
|
|
5
|
+
## Before you start
|
|
6
|
+
|
|
7
|
+
Katachi is still early. The current priority is keeping the compiler small, understandable, and honest about its supported surface.
|
|
8
|
+
|
|
9
|
+
Please open an issue or discussion before starting large feature work, especially for:
|
|
10
|
+
|
|
11
|
+
- new targets
|
|
12
|
+
- new authoring syntax
|
|
13
|
+
- parser rewrites
|
|
14
|
+
- runtime behavior changes
|
|
15
|
+
|
|
16
|
+
## Local setup
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm install
|
|
20
|
+
pnpm typecheck
|
|
21
|
+
pnpm test
|
|
22
|
+
pnpm build
|
|
23
|
+
pnpm verify:examples
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Contribution guidelines
|
|
27
|
+
|
|
28
|
+
- Keep changes focused.
|
|
29
|
+
- Prefer extending the typed compiler model over adding special cases.
|
|
30
|
+
- Add tests for parser, emitter, or build behavior when you change them.
|
|
31
|
+
- Update docs when the supported authoring surface changes.
|
|
32
|
+
- Do not broaden the supported TSX subset without documenting it.
|
|
33
|
+
|
|
34
|
+
## Project shape
|
|
35
|
+
|
|
36
|
+
The most important internal layers are:
|
|
37
|
+
|
|
38
|
+
- `src/core/parser.ts`
|
|
39
|
+
- `src/core/ast.ts`
|
|
40
|
+
- `src/core/types.ts`
|
|
41
|
+
- `src/targets/*`
|
|
42
|
+
- `tests/*.test.ts`
|
|
43
|
+
|
|
44
|
+
If you add a new target:
|
|
45
|
+
|
|
46
|
+
1. create a target module in `src/targets/`
|
|
47
|
+
2. register it in `src/targets/index.ts`
|
|
48
|
+
3. add tests for the generated output
|
|
49
|
+
|
|
50
|
+
## Scope
|
|
51
|
+
|
|
52
|
+
Katachi is intentionally constrained.
|
|
53
|
+
|
|
54
|
+
Non-goals include:
|
|
55
|
+
|
|
56
|
+
- full React semantics
|
|
57
|
+
- arbitrary JavaScript execution in templates
|
|
58
|
+
- full Askama parity in the authoring language
|
|
59
|
+
|
|
60
|
+
If a proposal pushes Katachi toward being a general-purpose framework compiler, it should be justified very carefully.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Relevate
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Katachi
|
|
2
|
+
|
|
3
|
+
Katachi lets you author template-like components once in restricted TSX and
|
|
4
|
+
compile them to multiple outputs.
|
|
5
|
+
|
|
6
|
+
Today it can emit:
|
|
7
|
+
|
|
8
|
+
- React TSX components
|
|
9
|
+
- static-oriented TSX components
|
|
10
|
+
- Askama Rust wrapper files
|
|
11
|
+
- Askama include partials
|
|
12
|
+
|
|
13
|
+
Katachi is still early, but it is already usable if you need one component
|
|
14
|
+
source that can target both React-style environments and Askama.
|
|
15
|
+
|
|
16
|
+
## Getting Started
|
|
17
|
+
|
|
18
|
+
Try it once without installing it:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm dlx @relevate/katachi build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
or:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx @relevate/katachi build
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
If you want Katachi in your project, install it:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm add -D @relevate/katachi
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
or:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npm install --save-dev @relevate/katachi
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Then add Katachi's JSX typing layer to `tsconfig.json`:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"compilerOptions": {
|
|
47
|
+
"jsx": "preserve",
|
|
48
|
+
"types": ["node", "@relevate/katachi/jsx"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If you already use a `types` array, append `@relevate/katachi/jsx` instead of
|
|
54
|
+
replacing your existing entries.
|
|
55
|
+
|
|
56
|
+
By default, Katachi reads templates from:
|
|
57
|
+
|
|
58
|
+
```txt
|
|
59
|
+
src/templates/**/*.template.tsx
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Build from your project root:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pnpm exec katachi build
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
or without installing it first:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pnpm dlx @relevate/katachi build
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
By default, Katachi writes:
|
|
75
|
+
|
|
76
|
+
- `dist/react`
|
|
77
|
+
- `dist/jsx-static`
|
|
78
|
+
- `dist/askama`
|
|
79
|
+
- `dist/askama/includes`
|
|
80
|
+
|
|
81
|
+
If you want custom paths:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pnpm exec katachi build --templates ./katachi/templates --dist ./generated
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## First Template
|
|
88
|
+
|
|
89
|
+
Start with a normal `.template.tsx` file:
|
|
90
|
+
|
|
91
|
+
```tsx
|
|
92
|
+
import { If, type TemplateNode } from "@relevate/katachi";
|
|
93
|
+
|
|
94
|
+
export type Props = {
|
|
95
|
+
tone: "calm" | "urgent";
|
|
96
|
+
title: string;
|
|
97
|
+
children?: TemplateNode;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default function NoticePanel({ tone, title, children }: Props) {
|
|
101
|
+
return (
|
|
102
|
+
<aside
|
|
103
|
+
className={[
|
|
104
|
+
"rounded-3xl border px-5 py-4",
|
|
105
|
+
tone == "calm" && "border-sky-200 bg-sky-50/80",
|
|
106
|
+
tone == "urgent" && "border-rose-200 bg-rose-50/80",
|
|
107
|
+
]}
|
|
108
|
+
>
|
|
109
|
+
<h3>{title}</h3>
|
|
110
|
+
<If test={tone == "urgent"}>
|
|
111
|
+
<p>Action recommended</p>
|
|
112
|
+
</If>
|
|
113
|
+
{children}
|
|
114
|
+
</aside>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Build it with:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pnpm exec katachi build
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
That generates target-specific files under `dist/`.
|
|
126
|
+
|
|
127
|
+
## What Katachi Generates
|
|
128
|
+
|
|
129
|
+
By default, build output goes to:
|
|
130
|
+
|
|
131
|
+
- `dist/react/**/*.tsx`
|
|
132
|
+
- `dist/jsx-static/**/*.tsx`
|
|
133
|
+
- `dist/askama/**/*.rs`
|
|
134
|
+
- `dist/askama/includes/**/*.html`
|
|
135
|
+
|
|
136
|
+
Nested templates preserve their relative directory layout.
|
|
137
|
+
|
|
138
|
+
## What Katachi Supports Today
|
|
139
|
+
|
|
140
|
+
- template authoring in `src/templates/**/*.template.tsx`
|
|
141
|
+
- imports between templates
|
|
142
|
+
- dynamic `class` and `className` arrays
|
|
143
|
+
- `If`
|
|
144
|
+
- `For`
|
|
145
|
+
- `safe(...)`
|
|
146
|
+
- nested components
|
|
147
|
+
- React output
|
|
148
|
+
- static-oriented TSX output
|
|
149
|
+
- Askama output
|
|
150
|
+
|
|
151
|
+
## Why Katachi Exists
|
|
152
|
+
|
|
153
|
+
At Relevate, our docs system is built in Rust and renders components with
|
|
154
|
+
Askama. We also wanted a live editor that could use the same component
|
|
155
|
+
structure and styling without maintaining a separate hand-written React
|
|
156
|
+
component library.
|
|
157
|
+
|
|
158
|
+
Katachi exists to make that possible: one authoring format, multiple outputs.
|
|
159
|
+
|
|
160
|
+
## What Katachi Is Not
|
|
161
|
+
|
|
162
|
+
- not a full React compiler
|
|
163
|
+
- not a full Askama replacement
|
|
164
|
+
- not arbitrary JavaScript execution in templates
|
|
165
|
+
- not a general-purpose frontend framework
|
|
166
|
+
|
|
167
|
+
## Documentation
|
|
168
|
+
|
|
169
|
+
- [Getting started](./docs/getting-started.md)
|
|
170
|
+
- [Template syntax](./docs/syntax.md)
|
|
171
|
+
- [Target outputs](./docs/targets.md)
|
|
172
|
+
- [Architecture](./docs/architecture.md)
|
|
173
|
+
- [Consumer example](./examples/basic/README.md)
|
|
174
|
+
|
|
175
|
+
## Current Limitations
|
|
176
|
+
|
|
177
|
+
- The TSX input is not arbitrary React.
|
|
178
|
+
- Generated React output is valid React, but the input syntax is compiler-owned.
|
|
179
|
+
- The repository-level smoke tests build the public example project under `examples/basic`.
|
|
180
|
+
|
|
181
|
+
## Contributing
|
|
182
|
+
|
|
183
|
+
If you are working on Katachi itself rather than using it in another project:
|
|
184
|
+
|
|
185
|
+
- `pnpm build`
|
|
186
|
+
- `pnpm verify:examples`
|
|
187
|
+
- `pnpm test`
|
|
188
|
+
- `pnpm typecheck`
|
|
189
|
+
|
|
190
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md) and [docs/architecture.md](./docs/architecture.md).
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
MIT. See [LICENSE](./LICENSE).
|
package/bin/katachi.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { dirname, resolve } from "node:path";
|
|
7
|
+
|
|
8
|
+
const binDir = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const packageRoot = resolve(binDir, "..");
|
|
10
|
+
const builtCliPath = resolve(packageRoot, "dist/cli/index.js");
|
|
11
|
+
const sourceCliPath = resolve(packageRoot, "src/cli/index.ts");
|
|
12
|
+
|
|
13
|
+
const hasBuiltCli = existsSync(builtCliPath);
|
|
14
|
+
|
|
15
|
+
const result = spawnSync(
|
|
16
|
+
process.execPath,
|
|
17
|
+
hasBuiltCli
|
|
18
|
+
? [builtCliPath, ...process.argv.slice(2)]
|
|
19
|
+
: ["--import", "tsx/esm", sourceCliPath, ...process.argv.slice(2)],
|
|
20
|
+
{
|
|
21
|
+
cwd: process.cwd(),
|
|
22
|
+
stdio: "inherit",
|
|
23
|
+
},
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
if (typeof result.status === "number") {
|
|
27
|
+
process.exit(result.status);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.exit(1);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public API helpers exposed to Katachi template files.
|
|
3
|
+
*
|
|
4
|
+
* These exports exist primarily for editor support and package consumers. The
|
|
5
|
+
* Katachi compiler parses template source text directly, so these helpers are
|
|
6
|
+
* not expected to execute in normal builds.
|
|
7
|
+
*/
|
|
8
|
+
export type ClassValue = string | number | boolean | null | undefined | ClassValue[];
|
|
9
|
+
export type TemplateNode = string | number | boolean | null | undefined | TemplateNode[];
|
|
10
|
+
export type IfProps = {
|
|
11
|
+
test: unknown;
|
|
12
|
+
children?: TemplateNode;
|
|
13
|
+
};
|
|
14
|
+
export type ForProps<T = unknown> = {
|
|
15
|
+
each: readonly T[] | T[] | null | undefined;
|
|
16
|
+
as: string;
|
|
17
|
+
index?: string;
|
|
18
|
+
children?: TemplateNode;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Placeholder runtime export for template files. The compiler reads source
|
|
22
|
+
* templates directly and never evaluates this function during normal use.
|
|
23
|
+
*/
|
|
24
|
+
export declare function If(_props: IfProps): TemplateNode;
|
|
25
|
+
/**
|
|
26
|
+
* Placeholder runtime export for template files. The compiler reads source
|
|
27
|
+
* templates directly and never evaluates this function during normal use.
|
|
28
|
+
*/
|
|
29
|
+
export declare function For<T>(_props: ForProps<T>): TemplateNode;
|
|
30
|
+
/**
|
|
31
|
+
* Marks a printed value as safe in Katachi templates. This is a no-op at the
|
|
32
|
+
* API layer because escaping is handled by target emitters.
|
|
33
|
+
*/
|
|
34
|
+
export declare function safe<T>(value: T): T;
|
|
35
|
+
/**
|
|
36
|
+
* Portable length helper for Katachi templates.
|
|
37
|
+
*/
|
|
38
|
+
export declare function len(value: {
|
|
39
|
+
length: number;
|
|
40
|
+
} | string | readonly unknown[] | null | undefined): number;
|
|
41
|
+
/**
|
|
42
|
+
* Portable emptiness helper for Katachi templates.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isEmpty(value: {
|
|
45
|
+
length: number;
|
|
46
|
+
} | string | readonly unknown[] | null | undefined): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* Portable presence helper for Katachi templates.
|
|
49
|
+
*/
|
|
50
|
+
export declare function isSome<T>(value: T | null | undefined): value is T;
|
|
51
|
+
/**
|
|
52
|
+
* Portable absence helper for Katachi templates.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isNone<T>(value: T | null | undefined): value is null | undefined;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Placeholder runtime export for template files. The compiler reads source
|
|
3
|
+
* templates directly and never evaluates this function during normal use.
|
|
4
|
+
*/
|
|
5
|
+
export function If(_props) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Placeholder runtime export for template files. The compiler reads source
|
|
10
|
+
* templates directly and never evaluates this function during normal use.
|
|
11
|
+
*/
|
|
12
|
+
export function For(_props) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Marks a printed value as safe in Katachi templates. This is a no-op at the
|
|
17
|
+
* API layer because escaping is handled by target emitters.
|
|
18
|
+
*/
|
|
19
|
+
export function safe(value) {
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Portable length helper for Katachi templates.
|
|
24
|
+
*/
|
|
25
|
+
export function len(value) {
|
|
26
|
+
return value?.length ?? 0;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Portable emptiness helper for Katachi templates.
|
|
30
|
+
*/
|
|
31
|
+
export function isEmpty(value) {
|
|
32
|
+
return (value?.length ?? 0) === 0;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Portable presence helper for Katachi templates.
|
|
36
|
+
*/
|
|
37
|
+
export function isSome(value) {
|
|
38
|
+
return value != null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Portable absence helper for Katachi templates.
|
|
42
|
+
*/
|
|
43
|
+
export function isNone(value) {
|
|
44
|
+
return value == null;
|
|
45
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ClassValue, TemplateNode } from "./index.js";
|
|
2
|
+
|
|
3
|
+
declare global {
|
|
4
|
+
namespace JSX {
|
|
5
|
+
/**
|
|
6
|
+
* Authoring templates do not produce real JSX runtime elements. The parser
|
|
7
|
+
* reads the source text and lowers it into Katachi AST nodes instead.
|
|
8
|
+
*/
|
|
9
|
+
type Element = TemplateNode;
|
|
10
|
+
|
|
11
|
+
interface ElementChildrenAttribute {
|
|
12
|
+
children: {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface IntrinsicElements {
|
|
16
|
+
[elemName: string]: {
|
|
17
|
+
children?: TemplateNode;
|
|
18
|
+
class?: ClassValue;
|
|
19
|
+
className?: ClassValue;
|
|
20
|
+
[attrName: string]: unknown;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
2
|
+
import { buildProject } from "../core/build.js";
|
|
3
|
+
import { verifyAskamaFixtures } from "../core/verify.js";
|
|
4
|
+
import { basicExampleRoot, createExampleFixtures, exampleFixtures } from "../core/example-fixtures.js";
|
|
5
|
+
function printHelp() {
|
|
6
|
+
console.log(`Katachi
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
katachi build [--project <dir>] [--templates <dir>] [--dist <dir>]
|
|
10
|
+
katachi verify:examples
|
|
11
|
+
katachi help
|
|
12
|
+
|
|
13
|
+
Defaults:
|
|
14
|
+
--project current working directory
|
|
15
|
+
--templates <project>/src/templates
|
|
16
|
+
--dist <project>/dist`);
|
|
17
|
+
}
|
|
18
|
+
function parseArgs(argv) {
|
|
19
|
+
const [commandArg, ...rest] = argv;
|
|
20
|
+
const command = (commandArg ?? "build");
|
|
21
|
+
if (!["build", "verify:askama", "verify:examples", "help"].includes(command)) {
|
|
22
|
+
throw new Error(`Unknown command: ${command}`);
|
|
23
|
+
}
|
|
24
|
+
const options = { command };
|
|
25
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
26
|
+
const current = rest[index];
|
|
27
|
+
const next = rest[index + 1];
|
|
28
|
+
if (current === "--project" && next) {
|
|
29
|
+
options.projectRoot = resolve(next);
|
|
30
|
+
index += 1;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (current === "--templates" && next) {
|
|
34
|
+
options.templatesDir = resolve(next);
|
|
35
|
+
index += 1;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (current === "--dist" && next) {
|
|
39
|
+
options.distDir = resolve(next);
|
|
40
|
+
index += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Unknown or incomplete option: ${current}`);
|
|
44
|
+
}
|
|
45
|
+
return options;
|
|
46
|
+
}
|
|
47
|
+
function run() {
|
|
48
|
+
const options = parseArgs(process.argv.slice(2));
|
|
49
|
+
if (options.command === "help") {
|
|
50
|
+
printHelp();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (options.command === "build") {
|
|
54
|
+
buildProject({
|
|
55
|
+
projectRoot: options.projectRoot,
|
|
56
|
+
templatesDir: options.templatesDir,
|
|
57
|
+
distDir: options.distDir,
|
|
58
|
+
});
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (options.command === "verify:examples" || options.command === "verify:askama") {
|
|
62
|
+
const projectRoot = options.projectRoot ?? basicExampleRoot;
|
|
63
|
+
const distDir = options.distDir ?? resolve(projectRoot, "dist");
|
|
64
|
+
buildProject({
|
|
65
|
+
projectRoot,
|
|
66
|
+
templatesDir: options.templatesDir,
|
|
67
|
+
distDir,
|
|
68
|
+
});
|
|
69
|
+
verifyAskamaFixtures({
|
|
70
|
+
fixtures: projectRoot === basicExampleRoot && distDir === resolve(basicExampleRoot, "dist")
|
|
71
|
+
? exampleFixtures
|
|
72
|
+
: createExampleFixtures(projectRoot, distDir),
|
|
73
|
+
});
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
run();
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical portable AST used by Katachi after parsing authoring input.
|
|
3
|
+
*
|
|
4
|
+
* Targets emit from this model instead of parsing templates themselves.
|
|
5
|
+
*/
|
|
6
|
+
export type Expr = {
|
|
7
|
+
kind: "var";
|
|
8
|
+
name: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "string";
|
|
11
|
+
value: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "bool";
|
|
14
|
+
value: boolean;
|
|
15
|
+
} | {
|
|
16
|
+
kind: "number";
|
|
17
|
+
value: number;
|
|
18
|
+
} | {
|
|
19
|
+
kind: "intrinsic";
|
|
20
|
+
name: "len" | "isEmpty" | "isSome" | "isNone";
|
|
21
|
+
args: Expr[];
|
|
22
|
+
} | {
|
|
23
|
+
kind: "raw";
|
|
24
|
+
source: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "eq";
|
|
27
|
+
left: Expr;
|
|
28
|
+
right: Expr;
|
|
29
|
+
} | {
|
|
30
|
+
kind: "neq";
|
|
31
|
+
left: Expr;
|
|
32
|
+
right: Expr;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "and";
|
|
35
|
+
left: Expr;
|
|
36
|
+
right: Expr;
|
|
37
|
+
} | {
|
|
38
|
+
kind: "or";
|
|
39
|
+
left: Expr;
|
|
40
|
+
right: Expr;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "not";
|
|
43
|
+
expr: Expr;
|
|
44
|
+
};
|
|
45
|
+
export type ClassItem = {
|
|
46
|
+
kind: "static";
|
|
47
|
+
value: string;
|
|
48
|
+
} | {
|
|
49
|
+
kind: "when";
|
|
50
|
+
test: Expr;
|
|
51
|
+
value: string;
|
|
52
|
+
};
|
|
53
|
+
export type AttrValue = {
|
|
54
|
+
kind: "text";
|
|
55
|
+
value: string;
|
|
56
|
+
} | {
|
|
57
|
+
kind: "expr";
|
|
58
|
+
expr: Expr;
|
|
59
|
+
} | {
|
|
60
|
+
kind: "classList";
|
|
61
|
+
items: ClassItem[];
|
|
62
|
+
};
|
|
63
|
+
export type Node = {
|
|
64
|
+
kind: "text";
|
|
65
|
+
value: string;
|
|
66
|
+
} | {
|
|
67
|
+
kind: "slot";
|
|
68
|
+
name: string;
|
|
69
|
+
} | {
|
|
70
|
+
kind: "print";
|
|
71
|
+
expr: Expr;
|
|
72
|
+
safe?: boolean;
|
|
73
|
+
} | {
|
|
74
|
+
kind: "if";
|
|
75
|
+
test: Expr;
|
|
76
|
+
then: Node[];
|
|
77
|
+
else?: Node[];
|
|
78
|
+
} | {
|
|
79
|
+
kind: "for";
|
|
80
|
+
item: string;
|
|
81
|
+
each: Expr;
|
|
82
|
+
children: Node[];
|
|
83
|
+
indexName?: string | null;
|
|
84
|
+
} | {
|
|
85
|
+
kind: "element";
|
|
86
|
+
tag: string;
|
|
87
|
+
attrs?: Record<string, AttrValue>;
|
|
88
|
+
children?: Node[];
|
|
89
|
+
} | {
|
|
90
|
+
kind: "component";
|
|
91
|
+
name: string;
|
|
92
|
+
props?: Record<string, AttrValue>;
|
|
93
|
+
children?: Node[];
|
|
94
|
+
};
|
|
95
|
+
export declare const v: (name: string) => Expr;
|
|
96
|
+
export declare const s: (value: string) => Expr;
|
|
97
|
+
export declare const b: (value: boolean) => Expr;
|
|
98
|
+
export declare const n: (value: number) => Expr;
|
|
99
|
+
export declare const intrinsic: (name: "len" | "isEmpty" | "isSome" | "isNone", ...args: Expr[]) => Expr;
|
|
100
|
+
export declare const raw: (source: string) => Expr;
|
|
101
|
+
export declare const eq: (left: Expr, right: Expr) => Expr;
|
|
102
|
+
export declare const neq: (left: Expr, right: Expr) => Expr;
|
|
103
|
+
export declare const and: (left: Expr, right: Expr) => Expr;
|
|
104
|
+
export declare const or: (left: Expr, right: Expr) => Expr;
|
|
105
|
+
export declare const not: (expr: Expr) => Expr;
|
|
106
|
+
export declare const textAttr: (value: string) => AttrValue;
|
|
107
|
+
export declare const exprAttr: (expr: Expr) => AttrValue;
|
|
108
|
+
export declare const classList: (...items: ClassItem[]) => AttrValue;
|
|
109
|
+
export declare const textNode: (value: string) => Node;
|
|
110
|
+
export declare const slotNode: (name: string) => Node;
|
|
111
|
+
export declare const printNode: (expr: Expr, safe?: boolean) => Node;
|
|
112
|
+
export declare const ifNode: (test: Expr, thenNodes: Node[], elseNodes?: Node[]) => Node;
|
|
113
|
+
export declare const forNode: (item: string, each: Expr, children?: Node[], indexName?: string | null) => Node;
|
|
114
|
+
export declare const elementNode: (tag: string, attrs?: Record<string, AttrValue>, children?: Node[]) => Node;
|
|
115
|
+
export declare const componentNode: (name: string, props?: Record<string, AttrValue>, children?: Node[]) => Node;
|
package/dist/core/ast.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical portable AST used by Katachi after parsing authoring input.
|
|
3
|
+
*
|
|
4
|
+
* Targets emit from this model instead of parsing templates themselves.
|
|
5
|
+
*/
|
|
6
|
+
export const v = (name) => ({ kind: "var", name });
|
|
7
|
+
export const s = (value) => ({ kind: "string", value });
|
|
8
|
+
export const b = (value) => ({ kind: "bool", value });
|
|
9
|
+
export const n = (value) => ({ kind: "number", value });
|
|
10
|
+
export const intrinsic = (name, ...args) => ({
|
|
11
|
+
kind: "intrinsic",
|
|
12
|
+
name,
|
|
13
|
+
args,
|
|
14
|
+
});
|
|
15
|
+
export const raw = (source) => ({ kind: "raw", source });
|
|
16
|
+
export const eq = (left, right) => ({ kind: "eq", left, right });
|
|
17
|
+
export const neq = (left, right) => ({ kind: "neq", left, right });
|
|
18
|
+
export const and = (left, right) => ({ kind: "and", left, right });
|
|
19
|
+
export const or = (left, right) => ({ kind: "or", left, right });
|
|
20
|
+
export const not = (expr) => ({ kind: "not", expr });
|
|
21
|
+
export const textAttr = (value) => ({ kind: "text", value });
|
|
22
|
+
export const exprAttr = (expr) => ({ kind: "expr", expr });
|
|
23
|
+
export const classList = (...items) => ({ kind: "classList", items });
|
|
24
|
+
export const textNode = (value) => ({ kind: "text", value });
|
|
25
|
+
export const slotNode = (name) => ({ kind: "slot", name });
|
|
26
|
+
export const printNode = (expr, safe = false) => ({ kind: "print", expr, safe });
|
|
27
|
+
export const ifNode = (test, thenNodes, elseNodes = []) => ({
|
|
28
|
+
kind: "if",
|
|
29
|
+
test,
|
|
30
|
+
then: thenNodes,
|
|
31
|
+
else: elseNodes,
|
|
32
|
+
});
|
|
33
|
+
export const forNode = (item, each, children = [], indexName = null) => ({
|
|
34
|
+
kind: "for",
|
|
35
|
+
item,
|
|
36
|
+
each,
|
|
37
|
+
children,
|
|
38
|
+
indexName,
|
|
39
|
+
});
|
|
40
|
+
export const elementNode = (tag, attrs = {}, children = []) => ({
|
|
41
|
+
kind: "element",
|
|
42
|
+
tag,
|
|
43
|
+
attrs,
|
|
44
|
+
children,
|
|
45
|
+
});
|
|
46
|
+
export const componentNode = (name, props = {}, children = []) => ({
|
|
47
|
+
kind: "component",
|
|
48
|
+
name,
|
|
49
|
+
props,
|
|
50
|
+
children,
|
|
51
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { BuildTemplate } from "./types.js";
|
|
2
|
+
export interface BuildProjectOptions {
|
|
3
|
+
projectRoot?: string;
|
|
4
|
+
distDir?: string;
|
|
5
|
+
templatesDir?: string;
|
|
6
|
+
logger?: Pick<Console, "log">;
|
|
7
|
+
}
|
|
8
|
+
export interface BuildProjectResult {
|
|
9
|
+
templates: BuildTemplate[];
|
|
10
|
+
writtenFiles: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Builds the current project and writes all configured outputs to `dist/`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildProject(options?: BuildProjectOptions): BuildProjectResult;
|