From 2dbe84c95da65504172a68ad974174b9c5bd48b7 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Thu, 19 Dec 2024 19:58:30 +0100 Subject: [PATCH 1/3] feat(parser): add support for parsing transformations --- src/core/template/templateParser.test.ts | 137 +++++++++++------- src/core/template/templateParser.ts | 48 ++++-- src/core/template/templateSchema.ts | 29 +++- .../components/Form/MarkdownBlock.svelte | 3 +- 4 files changed, 147 insertions(+), 70 deletions(-) diff --git a/src/core/template/templateParser.test.ts b/src/core/template/templateParser.test.ts index 7a00ac59..b33a1ba0 100644 --- a/src/core/template/templateParser.test.ts +++ b/src/core/template/templateParser.test.ts @@ -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}!"; @@ -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" }))); + }); }); diff --git a/src/core/template/templateParser.ts b/src/core/template/templateParser.ts index e47c400a..99225f38 100644 --- a/src/core/template/templateParser.ts +++ b/src/core/template/templateParser.ts @@ -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 { @@ -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 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 === @@ -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( ", ", @@ -160,7 +186,7 @@ function tokenToString(token: Token): string { function matchToken( onText: (value: string) => T, - onVariable: (variable: string) => T, + onVariable: (variable: string, transformation?: string) => T, onCommand: (command: FrontmatterCommand) => T, ) { return (token: Token): T => { @@ -168,7 +194,7 @@ function matchToken( 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: @@ -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 diff --git a/src/core/template/templateSchema.ts b/src/core/template/templateSchema.ts index a43eb8f1..a7209b69 100644 --- a/src/core/template/templateSchema.ts +++ b/src/core/template/templateSchema.ts @@ -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; + const TemplateVariableSchema = object({ _tag: literal("variable"), value: string(), + transformation: optional(transformations), }); const FrontmatterCommandSchema = object({ @@ -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; export type TemplateVariable = Output; diff --git a/src/views/components/Form/MarkdownBlock.svelte b/src/views/components/Form/MarkdownBlock.svelte index f7d1d614..423f26e5 100644 --- a/src/views/components/Form/MarkdownBlock.svelte +++ b/src/views/components/Form/MarkdownBlock.svelte @@ -1,7 +1,6 @@