Skip to content

Commit

Permalink
feat(parser): add support for parsing transformations
Browse files Browse the repository at this point in the history
  • Loading branch information
danielo515 committed Dec 19, 2024
1 parent 3ae4202 commit 2dbe84c
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 70 deletions.
137 changes: 83 additions & 54 deletions src/core/template/templateParser.test.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,112 @@
import { parseTemplate, anythingUntilOpenOrEOF, executeTemplate } from "./templateParser";
import * as S from 'parser-ts/string'
import * as E from "fp-ts/Either";
import { pipe, tap } from "@std";
import * as E from "fp-ts/Either";
import { stringifyYaml } from "obsidian";
import * as S from "parser-ts/string";
import { anythingUntilOpenOrEOF, executeTemplate, parseTemplate } from "./templateParser";

const inspect = (val: unknown) => {
console.dir(val, { depth: 10 });
return val;
}
};
const logError = E.mapLeft(console.log);

describe("parseTemplate", () => {
it.skip("test", () => {
pipe(
// stupid prettier
S.run("al{nam{{e}}")(anythingUntilOpenOrEOF),
inspect)
inspect,
);
});
it("should parse a single identifier template", () => {
const template = "{{name}}";
const result = parseTemplate(template);
expect(result).toEqual(E.of([{ _tag: "variable", value: "name" }]));

});
it("templates can start with an identifier", () => {
const template = "{{name}} is a name";
const result = parseTemplate(template);
expect(result).toEqual(E.of([
{ _tag: "variable", value: "name" },
{ _tag: "text", value: " is a name" },
]));
expect(result).toEqual(
E.of([
{ _tag: "variable", value: "name" },
{ _tag: "text", value: " is a name" },
]),
);
});
it("should parse a valid template", () => {
const template = "Hello, {{name}}!";
const result = parseTemplate(template);
logError(result);
expect(result).toEqual(E.of(
[
expect(result).toEqual(
E.of([
{ _tag: "text", value: "Hello, " },
{ _tag: "variable", value: "name" },
{ _tag: "text", value: "!" },
],
));
]),
);
});
it("should parse a valid template with several variables", () => {
const template = "Hello, {{name}}! You are {{age}} years old.";
const result = parseTemplate(template);
logError(result);
expect(result).toEqual(E.of(
[
expect(result).toEqual(
E.of([
{ _tag: "text", value: "Hello, " },
{ _tag: "variable", value: "name" },
{ _tag: "text", value: "! You are " },
{ _tag: "variable", value: "age" },
{ _tag: "text", value: " years old." },
],
));
]),
);
});

it("should parse a variables with transformations", () => {
const template = "Hello, {{name|uppercase}}! You are {{age|stringify}} years old.";
const result = parseTemplate(template);
logError(result);
expect(result).toEqual(
E.of([
{ _tag: "text", value: "Hello, " },
{ _tag: "variable", value: "name", transformation: "upper" },
{ _tag: "text", value: "! You are " },
{ _tag: "variable", value: "age", transformation: "stringify" },
{ _tag: "text", value: " years old." },
]),
);
});

it("should silently ignore invalid transformations", () => {
const template = "Hello, {{name|invalid}}! You are {{age|stringify}} years old.";
const result = parseTemplate(template);
logError(result);
expect(result).toEqual(
E.of([
{ _tag: "text", value: "Hello, " },
{ _tag: "variable", value: "name" },
{ _tag: "text", value: "! You are " },
{ _tag: "variable", value: "age", transformation: "stringify" },
{ _tag: "text", value: " years old." },
]),
);
});

it("should allow single braces in a template", () => {
const template = "This is code {bla}";
const result = parseTemplate(template);
logError(result);
expect(result).toEqual(E.of(
[
{ _tag: "text", value: "This is code {bla}" },
],
));
})
expect(result).toEqual(E.of([{ _tag: "text", value: "This is code {bla}" }]));
});
it("should allow single braces in a template even if it has variables", () => {
const template = "This is code {bla} {{name}}";
const result = parseTemplate(template);
logError(result);
expect(result).toEqual(E.of(
[
expect(result).toEqual(
E.of([
{ _tag: "text", value: "This is code {bla} " },
{ _tag: "variable", value: "name" },
],
));
})
]),
);
});

it.skip("should return a parse error for an invalid template", () => {
const template = "Hey, {{name}!";
Expand All @@ -96,59 +125,59 @@ describe("parseTemplate", () => {
const template = "Hey, {{ name }}!";
const result = parseTemplate(template);
logError(result);
expect(result).toEqual(E.of(
[
expect(result).toEqual(
E.of([
{ _tag: "text", value: "Hey, " },
{ _tag: "variable", value: "name" },
{ _tag: "text", value: "!" },
],
));
})
]),
);
});
it("should parse a frontmatter command", () => {
const template = "{#frontmatter#}";
const result = parseTemplate(template);
expect(result).toEqual(E.of([{ _tag: "frontmatter-command", pick: [], omit: [] }]));
})
});

it("should parse a frontmatter command that includes spaces", () => {
const template = "{# frontmatter #}";
const result = parseTemplate(template);
expect(result).toEqual(E.of([{ _tag: "frontmatter-command", pick: [], omit: [] }]));
})
});
it("should parse a frontmatter command with pick values", () => {
const template = "{# frontmatter pick: name,age #}";
const result = parseTemplate(template);
expect(result).toEqual(E.of([{ _tag: "frontmatter-command", pick: ['name', 'age'], omit: [] }]));
})
expect(result).toEqual(
E.of([{ _tag: "frontmatter-command", pick: ["name", "age"], omit: [] }]),
);
});

it("should parse a frontmatter command with pick values that can be separated by spaces", () => {
const template = "{# frontmatter pick: name, age #}";
const result = parseTemplate(template);
expect(result).toEqual(E.of([{ _tag: "frontmatter-command", pick: ['name', 'age'], omit: [] }]));
})
expect(result).toEqual(
E.of([{ _tag: "frontmatter-command", pick: ["name", "age"], omit: [] }]),
);
});

it("Should properly execute a template with a frontmatter command", () => {
const template = "{# frontmatter #}";
const parsed = parseTemplate(template);
const result = pipe(
parsed,
E.map((parsedTemplate) =>
executeTemplate(parsedTemplate, { name: 'John', age: 18 })
),
E.map(tap('executed')),
)
expect(result).toEqual(E.of(stringifyYaml({ name: 'John', age: 18 })))
})
E.map((parsedTemplate) => executeTemplate(parsedTemplate, { name: "John", age: 18 })),
E.map(tap("executed")),
);
expect(result).toEqual(E.of(stringifyYaml({ name: "John", age: 18 })));
});
it("Should properly execute a template with a frontmatter command that specifies a pick", () => {
const template = "{# frontmatter pick: name #}";
const parsed = parseTemplate(template);
const result = pipe(
parsed,
E.map((parsedTemplate) =>
executeTemplate(parsedTemplate, { name: 'John', age: 18 })
),
E.map(tap('executed')),
)
expect(result).toEqual(E.of(stringifyYaml({ name: 'John' })))
})
E.map((parsedTemplate) => executeTemplate(parsedTemplate, { name: "John", age: 18 })),
E.map(tap("executed")),
);
expect(result).toEqual(E.of(stringifyYaml({ name: "John" })));
});
});
48 changes: 37 additions & 11 deletions src/core/template/templateParser.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
import { Either, O, pipe } from "@std";
import { Either, O, parse, pipe } from "@std";
import * as A from "fp-ts/Array";
import * as E from "fp-ts/Either";
import * as R from "fp-ts/Record";
import { absurd, identity } from "fp-ts/function";
import { absurd, constUndefined, identity } from "fp-ts/function";
import * as St from "fp-ts/string";
import { stringifyYaml } from "obsidian";
import * as P from "parser-ts/Parser";
import * as C from "parser-ts/char";
import * as S from "parser-ts/string";
import { ModalFormData } from "../FormResult";
import type { FrontmatterCommand, TemplateText, TemplateVariable } from "./templateSchema";
import {
transformations,
type FrontmatterCommand,
type TemplateText,
type TemplateVariable,
type Transformations,
} from "./templateSchema";
type Token = TemplateText | TemplateVariable | FrontmatterCommand;
export type ParsedTemplate = Token[];

function TemplateText(value: string): TemplateText {
return { _tag: "text", value };
}
function TemplateVariable(value: string): TemplateVariable {
return { _tag: "variable", value };
function TemplateVariable(value: string, transformation?: Transformations): TemplateVariable {
return { _tag: "variable", value, transformation };
}

function FrontmatterCommand(pick: string[] = [], omit: string[] = []): FrontmatterCommand {
Expand All @@ -36,10 +42,30 @@ const EofStr = pipe(
const open = S.fold([S.string("{{"), S.spaces]);
const close = P.expected(S.fold([S.spaces, S.string("}}")]), 'closing variable tag: "}}"');
const identifier = S.many1(C.alphanum);
const transformation = pipe(
// dam prettier
S.fold([S.string("|"), S.spaces]),
P.apSecond(identifier),
P.map((x) => {
return pipe(
parse(transformations, x),
E.fold(constUndefined, (x) => x),
);
}),
);

const templateIdentifier: TokenParser = pipe(
identifier,
identifier, // First, we parse the variable name
// chain takes a function that accepts the result of the previous parser and returns a new parser
P.chain((value) =>
pipe(
// Within this pipe we build a parser of Parser<string, TemplateVariable> also using the value of the previous parser
P.optional(transformation),
P.map((trans) => TemplateVariable(value, O.toUndefined(trans))),
),
),
// finally we wrap the resulting parser in between the open and close strings
P.between(open, close),
P.map(TemplateVariable),
);

// === Command Parser ===
Expand Down Expand Up @@ -148,7 +174,7 @@ function tokenToString(token: Token): string {
case "text":
return token.value;
case "variable":
return `{{${token.value}}}`;
return `{{${token.value}${token.transformation ? `|${token.transformation}` : ""}}}`;
case "frontmatter-command":
return `{{# frontmatter pick: ${token.pick.join(", ")}, omit: ${token.omit.join(
", ",
Expand All @@ -160,15 +186,15 @@ function tokenToString(token: Token): string {

function matchToken<T>(
onText: (value: string) => T,
onVariable: (variable: string) => T,
onVariable: (variable: string, transformation?: string) => T,
onCommand: (command: FrontmatterCommand) => T,
) {
return (token: Token): T => {
switch (token._tag) {
case "text":
return onText(token.value);
case "variable":
return onVariable(token.value);
return onVariable(token.value, token.transformation);
case "frontmatter-command":
return onCommand(token);
default:
Expand Down Expand Up @@ -213,7 +239,7 @@ export function executeTemplate(parsedTemplate: ParsedTemplate, formData: ModalF
A.filterMap(
matchToken(
O.some,
(key) => O.fromNullable(formData[key]),
(key, transformation) => O.fromNullable(formData[key]),
(command) =>
pipe(
//prettier
Expand Down
29 changes: 26 additions & 3 deletions src/core/template/templateSchema.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,35 @@
import { Output, array, literal, object, string, union } from "valibot";
import {
Output,
array,
enumType,
literal,
object,
optional,
string,
transform,
union,
} from "valibot";

const TemplateTextSchema = object({
_tag: literal("text"),
value: string(),
});

const upper = transform(enumType(["upper", "uppercase"]), (_) => "upper" as const);

export const transformations = union([
upper,
literal("lower"),
literal("trim"),
literal("stringify"),
]);

export type Transformations = Output<typeof transformations>;

const TemplateVariableSchema = object({
_tag: literal("variable"),
value: string(),
transformation: optional(transformations),
});

const FrontmatterCommandSchema = object({
Expand All @@ -16,8 +38,9 @@ const FrontmatterCommandSchema = object({
omit: array(string()),
});

export const ParsedTemplateSchema = array(union([TemplateTextSchema, TemplateVariableSchema, FrontmatterCommandSchema]))

export const ParsedTemplateSchema = array(
union([TemplateTextSchema, TemplateVariableSchema, FrontmatterCommandSchema]),
);

export type TemplateText = Output<typeof TemplateTextSchema>;
export type TemplateVariable = Output<typeof TemplateVariableSchema>;
Expand Down
3 changes: 1 addition & 2 deletions src/views/components/Form/MarkdownBlock.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<script lang="ts">
import { parseFunctionBody, pipe } from "@std";
import { parseFunctionBody, pipe, TE } from "@std";
import * as R from "fp-ts/Record";
import * as TE from "fp-ts/TaskEither";
import { App, Component, MarkdownRenderer } from "obsidian";
import { input } from "src/core";
import { FieldValue, FormEngine } from "src/store/formEngine";
Expand Down

0 comments on commit 2dbe84c

Please sign in to comment.