diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..fc3ee64
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+*
+!.gitignore
+!etc
+!examples
+!examples/**
+!templates
+!scripts
+!**/*.ts
+!**/*.md
+!**/*.svg
+!**/*.png
+!**/*.json
+!**/*.jsonc
+!**/*.sh
+!**/*.vto
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..fd41904
--- /dev/null
+++ b/README.md
@@ -0,0 +1,55 @@
+
+
+Turn data into generated code and text files.
+
+---
+
+`dgen` takes a template file, plus some data or executable code, and then generates a new file from it.
+
+It simplifies going from structured data to code – and can be used to generate and format TypeScript, HTML, Markdown, JSON, blog posts, or **any** other type of text file.
+
+This can be used as a command line utility, or as a module in your own codebase
+via its exported `codegen` function.
+
+## Examples
+
+
+
+
+Generate Markdown, Typescript, and much more. Check out the full [examples](examples/).
+
+## Setup
+
+[Install Deno](https://docs.deno.com/runtime/manual) and ensure it is in your
+path.
+
+Then, run: `deno install -frA --name=dgen mod.ts`
+
+This will install `dgen` as a command line utility.
+
+## Usage
+
+A template is required, and ideally a data file or some TypeScript code to
+return data.
+
+Templates must be [vento (.vto) files](https://github.com/oscarotero/vento).
+
+
+```sh
+# Use with input file, data, and output.
+dgen --in=myCodegenTemplate.vto --data=myCodegenData.json --out=myCodegenFile.ts
+
+# Use with input file, data, output, plus additional processor step.
+dgen --in=myCodegenTemplate.vto --data=myCodegenData.json --processor=myTransformationStep.ts --out=myCodegenFile.ts
+```
+
+### Command Line Arguments
+
+| Option | Description | Example Usage |
+| ------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
+| `--in` | Path to the template file (vento .vto template), required | `--in ` |
+| `--out` | Path to the output file, optional, will print to stdout if not provided | `--out ` |
+| `--data` | Path to the data file (JSON or JSONC), optional | `--data ` |
+| `--processor` | Path to the JS/TS processor file, optional | `--processor ` |
+| `--flags` | Additional flags to run alongside the codegen process, optional. Accepts 'fmt', 'check', 'print_info'. Set to 'none' to skip defaults. | `--flags fmt,check,print_info` |
+| `--help` | Print the help message | `--help` |
\ No newline at end of file
diff --git a/etc/dgen.svg b/etc/dgen.svg
new file mode 100644
index 0000000..cb7a478
--- /dev/null
+++ b/etc/dgen.svg
@@ -0,0 +1,20 @@
+
diff --git a/etc/md.png b/etc/md.png
new file mode 100644
index 0000000..0680197
Binary files /dev/null and b/etc/md.png differ
diff --git a/etc/ts.png b/etc/ts.png
new file mode 100644
index 0000000..b52573e
Binary files /dev/null and b/etc/ts.png differ
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..b2dea0a
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,10 @@
+## Examples
+
+A few examples are provided in this folder. See the [input](input/) and corresponding [output](output/) folders.
+
+Try them out by running **`./scripts/run-example-.sh` from the parent folder**.
+
+- `basic` - A basic example of using `dgen` with just a template files and the default data. It creates a TypeScript file.
+- `data` - An example of using `dgen` with a data file. It creates a TypeScript file and then executes it.
+- `markdown` - An example of using `dgen` with a data file and outputting Markdown.
+- `processor` - An example of using `dgen` with a processor file.
\ No newline at end of file
diff --git a/examples/input/basic.vto b/examples/input/basic.vto
new file mode 100644
index 0000000..defb46f
--- /dev/null
+++ b/examples/input/basic.vto
@@ -0,0 +1,4 @@
+// Example TypeScript template
+
+const str = "{{ hello() }}, my name is {{ name }}!"
+console.log(str);
diff --git a/examples/input/data.jsonc b/examples/input/data.jsonc
new file mode 100644
index 0000000..8da1a17
--- /dev/null
+++ b/examples/input/data.jsonc
@@ -0,0 +1,29 @@
+{
+ // This is a JSONC file, which means it's a JSON file with comments.
+ "players": [
+ {
+ "name": "art3mis",
+ "level": 10,
+ "isOnline": true,
+ "notes": "This ain't no game, kid!"
+ },
+ {
+ "name": "parzival",
+ "level": 5,
+ "isOnline": false,
+ "notes": "I'm not a gunter, I'm a gamer."
+ },
+ {
+ "name": "aech",
+ "level": 7,
+ "isOnline": true,
+ "notes": "It is on like Red Dawn!"
+ },
+ {
+ "name": "gunter",
+ "level": 2,
+ "isOnline": false,
+ "notes": "I support the OASIS."
+ }
+ ]
+}
\ No newline at end of file
diff --git a/examples/input/data.vto b/examples/input/data.vto
new file mode 100644
index 0000000..7253717
--- /dev/null
+++ b/examples/input/data.vto
@@ -0,0 +1,30 @@
+// Example Typescript code with a JSONC data file
+
+class Player {
+ name: string;
+ level: number;
+ isOnline: boolean;
+
+ constructor(name: string, level: number, isOnline: boolean) {
+ this.name = name;
+ this.level = level;
+ this.isOnline = isOnline;
+ }
+
+ toString() {
+ return `${this.name} (Lvl. ${this.level})`;
+ }
+}
+
+const players = [
+{{ for player of players }}
+ // {{ player.notes}}
+ new Player('{{ player.name }}', {{ player.level }}, {{ player.isOnline }}),
+{{ /for }}
+];
+
+console.log(`There are ${players.length} players in the game.`);
+
+const onlinePlayers = players.filter(player => player.isOnline).join(', ');
+
+console.log(`The following players are online: ${onlinePlayers}`);
\ No newline at end of file
diff --git a/examples/input/markdown.jsonc b/examples/input/markdown.jsonc
new file mode 100644
index 0000000..7aa80a2
--- /dev/null
+++ b/examples/input/markdown.jsonc
@@ -0,0 +1,14 @@
+{
+ "sections": {
+ "header": {
+ "title": "dgen markdown template",
+ "content": "This is the header."
+ },
+ "content": {
+ "content": "## Hello, World!\n\nThis is a simple example of a markdown template."
+ },
+ "footer": {
+ "content": "This is the footer."
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/input/markdown.vto b/examples/input/markdown.vto
new file mode 100644
index 0000000..ce36436
--- /dev/null
+++ b/examples/input/markdown.vto
@@ -0,0 +1,5 @@
+{{ for section of sections }}
+{{ if section.title }}# {{ section.title }}{{ /if }}
+{{ if section.subtitle }}## {{ section.subtitle }}{{ /if }}
+{{ if section.content }}{{ section.content }}{{ /if }}
+{{ /for }}
\ No newline at end of file
diff --git a/examples/input/processor.ts b/examples/input/processor.ts
new file mode 100644
index 0000000..39062c0
--- /dev/null
+++ b/examples/input/processor.ts
@@ -0,0 +1,12 @@
+export default async function codegen(
+ data: any,
+): Promise> {
+ return {
+ data: {
+ ...(data ? data : {}),
+ pi: Math.PI,
+ e: Math.E,
+ c: 299792458,
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/input/processor.vto b/examples/input/processor.vto
new file mode 100644
index 0000000..e5f0cfa
--- /dev/null
+++ b/examples/input/processor.vto
@@ -0,0 +1,7 @@
+# dgen processor example
+
+Here are some math constants:
+
+e: {{ e }}
+pi: {{ pi }}
+c: {{ c }} m/s
\ No newline at end of file
diff --git a/examples/output/basic.ts b/examples/output/basic.ts
new file mode 100644
index 0000000..2ad0c72
--- /dev/null
+++ b/examples/output/basic.ts
@@ -0,0 +1,4 @@
+// Example TypeScript template
+
+const str = "sup dawg, my name is Bobert Paulson!";
+console.log(str);
diff --git a/examples/output/data.ts b/examples/output/data.ts
new file mode 100644
index 0000000..a83ed98
--- /dev/null
+++ b/examples/output/data.ts
@@ -0,0 +1,37 @@
+// Example Typescript code with a JSONC data file
+
+class Player {
+ name: string;
+ level: number;
+ isOnline: boolean;
+
+ constructor(name: string, level: number, isOnline: boolean) {
+ this.name = name;
+ this.level = level;
+ this.isOnline = isOnline;
+ }
+
+ toString() {
+ return `${this.name} (Lvl. ${this.level})`;
+ }
+}
+
+const players = [
+ // This ain't no game, kid!
+ new Player("art3mis", 10, true),
+
+ // I'm not a gunter, I'm a gamer.
+ new Player("parzival", 5, false),
+
+ // It is on like Red Dawn!
+ new Player("aech", 7, true),
+
+ // I support the OASIS.
+ new Player("gunter", 2, false),
+];
+
+console.log(`There are ${players.length} players in the game.`);
+
+const onlinePlayers = players.filter((player) => player.isOnline).join(", ");
+
+console.log(`The following players are online: ${onlinePlayers}`);
diff --git a/examples/output/markdown.md b/examples/output/markdown.md
new file mode 100644
index 0000000..f02db69
--- /dev/null
+++ b/examples/output/markdown.md
@@ -0,0 +1,13 @@
+# dgen markdown template
+
+This is the header.
+
+
+
+## Hello, World!
+
+This is a simple example of a markdown template.
+
+
+
+This is the footer.
\ No newline at end of file
diff --git a/examples/output/processor.md b/examples/output/processor.md
new file mode 100644
index 0000000..1fe94a5
--- /dev/null
+++ b/examples/output/processor.md
@@ -0,0 +1,7 @@
+# dgen processor example
+
+Here are some math constants:
+
+e: 2.718281828459045
+pi: 3.141592653589793
+c: 299792458 m/s
\ No newline at end of file
diff --git a/mod.ts b/mod.ts
new file mode 100644
index 0000000..fde280b
--- /dev/null
+++ b/mod.ts
@@ -0,0 +1,305 @@
+import vento from "https://deno.land/x/vento@v0.12.1/mod.ts";
+import { Filter } from "https://deno.land/x/vento@v0.12.1/src/environment.ts";
+import { parse as parseJsonc } from "https://deno.land/std@0.212.0/jsonc/parse.ts";
+import { parseArgs } from "https://deno.land/std@0.212.0/cli/parse_args.ts";
+
+type CodegenArgs = {
+ /**
+ * Full path to the template file (vento .vto template)
+ */
+ templateVtoPath: string;
+
+ /**
+ * Full path to processor file [optional], which will pass data and
+ * additional filters to the template.
+ *
+ * This file should export a function (or async function) that
+ * takes an optional data Json object and returns an object with the final
+ * data and filters to pass to the template.
+ *
+ * Example:
+ * ```ts
+ * export default (dataJson: any) => ({
+ * data: {
+ * myVar: "yeet",
+ * hello() {
+ * return "hello world";
+ * },
+ * filters: {
+ * upper: (str: string) => str.toUpperCase(),
+ * }
+ * });
+ * `
+ */
+ processorTsPath?: string;
+
+ /**
+ * JSON string to pass to the template as data.
+ * This can be either a JSON or JSONC (JSON with comments) file.
+ */
+ dataJsonPath?: string;
+
+ /**
+ * Full path to the final output file, can be any file type (.ts, .json, .md, etc.)
+ */
+ outputPath?: string;
+
+ /**
+ * Filters to pass to the template, which are arbitrary functions that
+ * can transform any variable.
+ *
+ * Read about filters here:
+ * https://vento.js.org/syntax/pipes/
+ */
+ filters?: Record;
+
+ /**
+ * Arbitrary data to pass to the template
+ * Can be functions, objects, arrays, etc.
+ *
+ * Read more about using data in templates here:
+ * https://vento.js.org/syntax/print/
+ */
+ data?: Record;
+
+ /**
+ * Additional flags to run alongside the codegen process
+ * For example, formatting and checking the output is valid TypeScript
+ */
+ flags?: ('fmt' | 'check' | 'print_info')[]
+
+ /**
+ * Optional error handler
+ */
+ error?: (err: Error) => void;
+}
+
+const DEFAULT_ARGS: Partial = {
+ templateVtoPath: "template.vto",
+ filters: {
+ upper: (str: string) => str.toUpperCase(),
+ lower: (str: string) => str.toLowerCase(),
+ },
+ data: {
+ name: "Bobert Paulson",
+ hello() {
+ return "sup dawg";
+ }
+ },
+ flags: ['fmt', 'check', 'print_info'],
+};
+
+export const codegen = async (args: CodegenArgs): Promise => {
+ const startTime = performance.now();
+ const {
+ templateVtoPath,
+ processorTsPath,
+ dataJsonPath,
+ outputPath,
+ filters,
+ data,
+ flags,
+ error,
+ } = { ...DEFAULT_ARGS, ...args };
+
+ const env = vento();
+ const failures: string[] = [];
+
+ let processorData: Record | undefined = undefined;
+ let processorFilters: Record | undefined = undefined;
+
+ if (dataJsonPath) {
+ try {
+ const dataJson = await Deno.readTextFile(dataJsonPath);
+ const parsedData = parseJsonc(dataJson) as Record;
+
+ if (parsedData) {
+ processorData = parsedData;
+ }
+ }
+ catch (err) {
+ if (error) {
+ error(err);
+ }
+ console.error(err);
+ failures.push(`data (${dataJsonPath})`);
+ }
+ }
+
+ if (processorTsPath) {
+ try {
+ const processor = (await import(processorTsPath)).default;
+ const result = await processor(processorData);
+
+ processorData = result.data;
+ processorFilters = result.filters;
+ }
+ catch (err) {
+ if (error) {
+ error(err);
+ }
+ console.error(err);
+ failures.push(`processor (${processorTsPath})`)
+ }
+ }
+
+ if (processorFilters) {
+ env.filters = { ...processorFilters }
+ }
+ else {
+ env.filters = { ...filters };
+ }
+
+ let output: string = "";
+
+ try {
+ const template = await env.load(templateVtoPath);
+ const finalData = {
+ ...(processorData ? processorData : data),
+ }
+
+ const result = await template(finalData);
+
+ output = result.content.trim();
+
+ if (outputPath) {
+ await Deno.writeTextFile(outputPath, output);
+ }
+ }
+ catch (err) {
+ if (error) {
+ error(err);
+ }
+ console.error(err);
+ failures.push(`vento template (${templateVtoPath})`);
+ }
+
+ if (flags && flags.length && outputPath) {
+ if (flags.includes("fmt")) {
+ const p = new Deno.Command("deno", {
+ args: ["fmt", outputPath],
+ stdout: "piped",
+ stderr: "piped",
+ }).spawn();
+
+ const {
+ success,
+ stdout,
+ stderr,
+ } = await p.output();
+
+ // Write output to stderr
+ if (flags.includes('print_info')) {
+ await Deno.stderr.write(stdout);
+ await Deno.stderr.write(stderr);
+ }
+
+ if (!success) {
+ failures.push("deno fmt");
+ }
+ }
+
+ if (flags.includes("check")) {
+ const p = new Deno.Command("deno", {
+ args: ["check", outputPath],
+ stdout: "piped",
+ stderr: "piped",
+ }).spawn();
+
+ const {
+ success,
+ stdout,
+ stderr,
+ } = await p.output();
+
+ // Write output to stderr
+ if (flags.includes('print_info')) {
+ await Deno.stderr.write(stdout);
+ await Deno.stderr.write(stderr);
+ }
+
+ if (!success) {
+ failures.push("deno check");
+ }
+ }
+ }
+
+ if (flags && flags.includes('print_info')) {
+ const terminalWidth = Deno.consoleSize().columns;
+ const printDivider = (char: string) => {
+ const divider = char.repeat(Math.floor(terminalWidth * 0.67));
+ return `${divider}\n`;
+ }
+ const bold = (str: string) => `\x1b[1m${str}\x1b[22m`;
+ const colorize = (str: string, color: 'red' | 'green') => {
+ const colors = {
+ red: "\x1b[31m",
+ green: "\x1b[32m",
+ }
+ return `${colors[color]}${str}\x1b[0m`;
+ }
+
+ await Deno.stderr.write(new TextEncoder().encode(printDivider("=")));
+ const emoji = failures.length ? "⚠️" : "✅";
+ await Deno.stdout.write(new TextEncoder().encode(bold(colorize(`${emoji} Codegen finished ${failures.length ? "with errors" : "successfully"
+ } in ${Math.round(performance.now() - startTime)}ms\n`, failures.length ? 'red' : 'green'))));
+
+ if (failures.length) {
+ const failStr = `Failed steps: \n\t· ${bold(failures.join("\n\t· "))}\n`
+ await Deno.stderr.write(new TextEncoder().encode(failStr));
+ }
+ await Deno.stderr.write(new TextEncoder().encode(printDivider("=")));
+ }
+
+ if (failures.length && error) {
+ error(new Error(`Failed steps: ${failures
+ .map((f) => f.replace("deno ", ""))
+ .join(", ")}\n`));
+ }
+
+ return output;
+}
+
+if (import.meta.main) {
+ const cliArgs = parseArgs(Deno.args);
+
+ if (!cliArgs.in || cliArgs._.length || cliArgs.help) {
+ console.log(`Usage:
+ dgen --in --out --data --processor --flags fmt,check,print_info
+
+Options:
+ --in Path to the template file (vento .vto template), required
+ --out Path to the output file, optional, will print to stdout if not provided
+ --data Path to the data file (JSON or JSONC), optional
+ --processor Path to the JS/TS processor file, optional
+ --flags Additional flags to run alongside the codegen process, optional
+ --help Print this help message
+ `);
+ Deno.exit(1);
+ }
+
+ let errors = false;
+
+ const args: CodegenArgs = {
+ templateVtoPath: cliArgs.in,
+ processorTsPath: cliArgs.processor,
+ dataJsonPath: cliArgs.data,
+ outputPath: cliArgs.out,
+ flags: cliArgs.flags ? cliArgs.flags.split(",").map((f: string) => f.trim()).filter(Boolean) as ('fmt' | 'check' | 'print_info')[] || undefined : undefined,
+ error: (_err: Error) => {
+ errors = true;
+ }
+ }
+
+ // Filter out undefined values
+ // @ts-ignore Object keys are always strings
+ Object.keys(args).forEach((key) => args[key] === undefined && delete args[key]);
+
+ const result = await codegen(args);
+
+ if (!args.outputPath) {
+ console.log(result);
+ }
+
+ Deno.exit(errors ? 1 : 0);
+}
diff --git a/scripts/clean-examples.sh b/scripts/clean-examples.sh
new file mode 100755
index 0000000..140e6d4
--- /dev/null
+++ b/scripts/clean-examples.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+# This script cleans the output directory of the examples
+rm -r examples/output/*
\ No newline at end of file
diff --git a/scripts/dev.sh b/scripts/dev.sh
new file mode 100755
index 0000000..f35721b
--- /dev/null
+++ b/scripts/dev.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+# Use this to develop the project
+deno run -A --watch mod.ts
diff --git a/scripts/install.sh b/scripts/install.sh
new file mode 100755
index 0000000..99026f3
--- /dev/null
+++ b/scripts/install.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+# This script installs the project as a deno executable
+deno install -frA --name=dgen mod.ts
\ No newline at end of file
diff --git a/scripts/run-example-basic-alias.sh b/scripts/run-example-basic-alias.sh
new file mode 100755
index 0000000..91485de
--- /dev/null
+++ b/scripts/run-example-basic-alias.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+# This script runs the basic example from its default alias `dgen`, requiring the project to be installed
+dgen --in=examples/input/basic.vto --out=examples/output/basic.ts
+deno run examples/output/basic.ts
\ No newline at end of file
diff --git a/scripts/run-example-basic.sh b/scripts/run-example-basic.sh
new file mode 100755
index 0000000..285d625
--- /dev/null
+++ b/scripts/run-example-basic.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+# This script runs the basic example and outputs the result of executing the generated file
+deno run --allow-read --allow-write --allow-run mod.ts --in=examples/input/basic.vto --out=examples/output/basic.ts
+deno run examples/output/basic.ts
\ No newline at end of file
diff --git a/scripts/run-example-data.sh b/scripts/run-example-data.sh
new file mode 100755
index 0000000..0174cbe
--- /dev/null
+++ b/scripts/run-example-data.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+# This script runs the data example and outputs the result of executing the generated file
+deno run --allow-read --allow-write --allow-run mod.ts --in=examples/input/data.vto --data=examples/input/data.jsonc --out=examples/output/data.ts
+deno run examples/output/data.ts
\ No newline at end of file
diff --git a/scripts/run-example-markdown.sh b/scripts/run-example-markdown.sh
new file mode 100755
index 0000000..494bbba
--- /dev/null
+++ b/scripts/run-example-markdown.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+# This script runs the markdown example and outputs the result
+deno run --allow-read --allow-write --allow-run mod.ts --in=examples/input/markdown.vto --data=examples/input/markdown.jsonc --out=examples/output/markdown.md --flags=none
+cat examples/output/markdown.md
\ No newline at end of file
diff --git a/scripts/run-example-processor.sh b/scripts/run-example-processor.sh
new file mode 100755
index 0000000..d7298bc
--- /dev/null
+++ b/scripts/run-example-processor.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+# This script runs the processor example and outputs the result
+deno run --allow-read --allow-write --allow-run mod.ts --in=examples/input/processor.vto --processor=./examples/input/processor.ts --out=examples/output/processor.md --flags=none
+cat examples/output/processor.md
\ No newline at end of file