From 5ac3497ca1e46badbea36e268c19164abdf669cf Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Tue, 19 Dec 2023 08:51:43 +0100 Subject: [PATCH 1/9] refactor: move input types to it's own file --- src/core/InputDefinitionSchema.ts | 142 ++++++++++++++++++++++++++ src/core/findInputDefinitionSchema.ts | 75 +++++++++----- src/core/formDefinition.test.ts | 2 +- src/core/formDefinition.ts | 35 ++++--- src/core/formDefinitionSchema.ts | 118 +-------------------- 5 files changed, 214 insertions(+), 158 deletions(-) create mode 100644 src/core/InputDefinitionSchema.ts diff --git a/src/core/InputDefinitionSchema.ts b/src/core/InputDefinitionSchema.ts new file mode 100644 index 00000000..57719097 --- /dev/null +++ b/src/core/InputDefinitionSchema.ts @@ -0,0 +1,142 @@ +import { trySchemas, ParsingFn, parseC } from "@std"; +import { AllFieldTypes } from "./formDefinition"; +import { + object, + number, + literal, + array, + string, + union, + optional, + minLength, + toTrimmed, + BaseSchema, + enumType, + Output, +} from "valibot"; + +/** + * Here are the definition for the input types. + * This types are the ones that represent the just the input. + * This is how they are stored in the settings/form definition. + * For schemas of the FieldDefinition, see formDefinitionSchema.ts + **/ + +export function nonEmptyString(name: string) { + return string(`${name} should be a string`, [ + toTrimmed(), + minLength(1, `${name} should not be empty`), + ]); +} + +const InputBasicTypeSchema = enumType([ + "text", + "number", + "date", + "time", + "datetime", + "textarea", + "toggle", + "email", + "tel", +]); +//=========== Schema definitions +export const SelectFromNotesSchema = object({ + type: literal("select"), + source: literal("notes"), + folder: nonEmptyString("folder name"), +}); +export const InputTagSchema = object({ + type: literal("tag"), + exclude: optional(string()), // This should be a regex string +}); +export const InputSliderSchema = object({ + type: literal("slider"), + min: number(), + max: number(), +}); +export const InputNoteFromFolderSchema = object({ + type: literal("note"), + folder: nonEmptyString("folder name"), +}); +export const InputFolderSchema = object({ + type: literal("folder"), + // TODO: allow exclude option +}); +export const InputDataviewSourceSchema = object({ + type: literal("dataview"), + query: nonEmptyString("dataview query"), +}); +export const InputBasicSchema = object({ type: InputBasicTypeSchema }); +export const InputSelectFixedSchema = object({ + type: literal("select"), + source: literal("fixed"), + options: array( + object({ + value: string([toTrimmed()]), + label: string(), + }), + ), +}); +const MultiSelectNotesSchema = object({ + type: literal("multiselect"), + source: literal("notes"), + folder: nonEmptyString("multi select source folder"), +}); +const MultiSelectFixedSchema = object({ + type: literal("multiselect"), + source: literal("fixed"), + multi_select_options: array(string()), +}); +const MultiSelectQuerySchema = object({ + type: literal("multiselect"), + source: literal("dataview"), + query: nonEmptyString("dataview query"), +}); +export const MultiselectSchema = union([ + MultiSelectNotesSchema, + MultiSelectFixedSchema, + MultiSelectQuerySchema, +]); +export const InputTypeSchema = union([ + InputBasicSchema, + InputNoteFromFolderSchema, + InputFolderSchema, + InputSliderSchema, + InputTagSchema, + SelectFromNotesSchema, + InputDataviewSourceSchema, + InputSelectFixedSchema, + MultiselectSchema, +]); +export const InputTypeToParserMap: Record< + AllFieldTypes, + ParsingFn +> = { + number: parseC(InputBasicSchema), + text: parseC(InputBasicSchema), + email: parseC(InputBasicSchema), + tel: parseC(InputBasicSchema), + date: parseC(InputBasicSchema), + time: parseC(InputBasicSchema), + datetime: parseC(InputBasicSchema), + textarea: parseC(InputBasicSchema), + toggle: parseC(InputBasicSchema), + note: parseC(InputNoteFromFolderSchema), + folder: parseC(InputFolderSchema), + slider: parseC(InputSliderSchema), + tag: parseC(InputTagSchema), + select: trySchemas([SelectFromNotesSchema, InputSelectFixedSchema]), + dataview: parseC(InputDataviewSourceSchema), + multiselect: parseC(MultiselectSchema), +}; + +//=========== Types derived from schemas +export type selectFromNotes = Output; +export type inputSlider = Output; +export type inputNoteFromFolder = Output; +export type inputDataviewSource = Output; +export type inputSelectFixed = Output; +export type basicInput = Output; +export type multiselect = Output; +export type inputType = Output; diff --git a/src/core/findInputDefinitionSchema.ts b/src/core/findInputDefinitionSchema.ts index 181828f9..f7b973df 100644 --- a/src/core/findInputDefinitionSchema.ts +++ b/src/core/findInputDefinitionSchema.ts @@ -1,49 +1,66 @@ import { A, NonEmptyArray, ParsingFn, parse, pipe } from "@std"; import * as E from "fp-ts/Either"; import { ValiError, BaseSchema } from "valibot"; -import { FieldMinimal, FieldMinimalSchema, InputTypeToParserMap } from "./formDefinitionSchema"; +import { FieldMinimal, FieldMinimalSchema } from "./formDefinitionSchema"; import { AllFieldTypes } from "./formDefinition"; +import { InputTypeToParserMap } from "./InputDefinitionSchema"; function stringifyIssues(error: ValiError): NonEmptyArray { - return error.issues.map((issue) => `${issue.path?.map((i) => i.key)}: ${issue.message} got ${issue.input}`) as NonEmptyArray; + return error.issues.map( + (issue) => + `${issue.path?.map((i) => i.key)}: ${issue.message} got ${ + issue.input + }`, + ) as NonEmptyArray; } export class InvalidInputTypeError { static readonly _tag = "InvalidInputTypeError" as const; - readonly path: string = 'input.type'; - constructor(readonly field: FieldMinimal, readonly inputType: unknown) { } + readonly path: string = "input.type"; + constructor( + readonly field: FieldMinimal, + readonly inputType: unknown, + ) {} toString(): string { return `InvalidInputTypeError: ${this.getFieldErrors()[0]}`; } getFieldErrors(): NonEmptyArray { - return [`"input.type" is invalid, got: ${JSON.stringify(this.inputType)}`] + return [ + `"input.type" is invalid, got: ${JSON.stringify(this.inputType)}`, + ]; } } export class InvalidInputError { static readonly _tag = "InvalidInputError" as const; readonly path: string; - constructor(readonly field: FieldMinimal, readonly error: ValiError) { - this.path = error.issues[0].path?.map((i) => i.key).join('.') ?? ''; + constructor( + readonly field: FieldMinimal, + readonly error: ValiError, + ) { + this.path = error.issues[0].path?.map((i) => i.key).join(".") ?? ""; } toString(): string { - return `InvalidInputError: ${stringifyIssues(this.error).join(', ')}`; + return `InvalidInputError: ${stringifyIssues(this.error).join(", ")}`; } getFieldErrors(): NonEmptyArray { - return stringifyIssues(this.error) + return stringifyIssues(this.error); } } export class InvalidFieldError { static readonly _tag = "InvalidFieldError" as const; readonly path: string; - constructor(public field: unknown, readonly error: ValiError) { - this.path = error.issues[0].path?.map((i) => i.key).join('.') ?? ''; + constructor( + public field: unknown, + readonly error: ValiError, + ) { + this.path = error.issues[0].path?.map((i) => i.key).join(".") ?? ""; } toString(): string { - return `InvalidFieldError: ${stringifyIssues(this.error).join(', ')}`; + return `InvalidFieldError: ${stringifyIssues(this.error).join(", ")}`; } getFieldErrors(): string[] { - return stringifyIssues(this.error) + return stringifyIssues(this.error); } static of(field: unknown) { return (error: ValiError) => new InvalidFieldError(field, error); @@ -51,7 +68,7 @@ export class InvalidFieldError { } function isValidInputType(input: unknown): input is AllFieldTypes { - return 'string' === typeof input && input in InputTypeToParserMap; + return "string" === typeof input && input in InputTypeToParserMap; } /** * Finds the corresponding schema to the provided unparsed field. @@ -63,15 +80,21 @@ function isValidInputType(input: unknown): input is AllFieldTypes { * @param fieldDefinition a field definition to find the input schema for * @returns a tuple of the basic field definition and the input schema */ -export function findInputDefinitionSchema(fieldDefinition: unknown): E.Either]> { +export function findInputDefinitionSchema( + fieldDefinition: unknown, +): E.Either< + InvalidFieldError | InvalidInputTypeError, + [FieldMinimal, ParsingFn] +> { return pipe( parse(FieldMinimalSchema, fieldDefinition), E.mapLeft(InvalidFieldError.of(fieldDefinition)), E.chainW((field) => { const type = field.input.type; - if (isValidInputType(type)) return E.right([field, InputTypeToParserMap[type]]); + if (isValidInputType(type)) + return E.right([field, InputTypeToParserMap[type]]); else return E.left(new InvalidInputTypeError(field, type)); - }) + }), ); } /** @@ -87,16 +110,18 @@ export function findFieldErrors(fields: unknown[]) { A.map((fieldUnparsed) => { return pipe( findInputDefinitionSchema(fieldUnparsed), - E.chainW(([field, parser]) => pipe( - parser(field.input), - E.bimap( - (error) => new InvalidInputError(field, error), - () => field - )), - )) + E.chainW(([field, parser]) => + pipe( + parser(field.input), + E.bimap( + (error) => new InvalidInputError(field, error), + () => field, + ), + ), + ), + ); }), // A.partition(E.isLeft), // Separated.right, ); - } diff --git a/src/core/formDefinition.test.ts b/src/core/formDefinition.test.ts index 514a3def..6526ac47 100644 --- a/src/core/formDefinition.test.ts +++ b/src/core/formDefinition.test.ts @@ -1,3 +1,4 @@ +import { MultiselectSchema } from "./InputDefinitionSchema"; import { isDataViewSource, isInputNoteFromFolder, @@ -5,7 +6,6 @@ import { isInputSlider, isSelectFromNotes, } from "./formDefinition"; -import { MultiselectSchema } from "./formDefinitionSchema"; import { parse } from "valibot"; describe("isDataViewSource", () => { diff --git a/src/core/formDefinition.ts b/src/core/formDefinition.ts index ba5167a3..cf99d3b5 100644 --- a/src/core/formDefinition.ts +++ b/src/core/formDefinition.ts @@ -1,13 +1,5 @@ import { type Output, is, safeParse } from "valibot"; import { - SelectFromNotesSchema, - InputSliderSchema, - InputNoteFromFolderSchema, - InputDataviewSourceSchema, - InputSelectFixedSchema, - InputBasicSchema, - MultiselectSchema, - InputTypeSchema, FieldDefinitionSchema, FormDefinitionLatestSchema, FieldListSchema, @@ -16,15 +8,24 @@ import { } from "./formDefinitionSchema"; import { A, O, pipe } from "@std"; import { Simplify } from "type-fest"; -//=========== Types derived from schemas -type selectFromNotes = Output; -type inputSlider = Output; -type inputNoteFromFolder = Output; -type inputDataviewSource = Output; -type inputSelectFixed = Output; -type basicInput = Output; -type multiselect = Output; -type inputType = Output; +import { + InputBasicSchema, + InputDataviewSourceSchema, + InputNoteFromFolderSchema, + InputSelectFixedSchema, + InputSliderSchema, + InputTypeSchema, + MultiselectSchema, + SelectFromNotesSchema, + basicInput, + inputDataviewSource, + inputNoteFromFolder, + inputSelectFixed, + inputSlider, + inputType, + multiselect, + selectFromNotes, +} from "./InputDefinitionSchema"; export const FieldTypeReadable: Record = { text: "Text", diff --git a/src/core/formDefinitionSchema.ts b/src/core/formDefinitionSchema.ts index f03087c0..68ca74ad 100644 --- a/src/core/formDefinitionSchema.ts +++ b/src/core/formDefinitionSchema.ts @@ -1,141 +1,29 @@ import * as E from "fp-ts/Either"; -import { pipe, parse, trySchemas, ParsingFn, parseC } from "@std"; +import { pipe, parse } from "@std"; import { object, - number, literal, type Output, is, array, string, - union, optional, - minLength, - toTrimmed, merge, unknown, ValiError, - BaseSchema, - enumType, passthrough, boolean, } from "valibot"; -import { AllFieldTypes, FormDefinition } from "./formDefinition"; +import { FormDefinition } from "./formDefinition"; import { findFieldErrors } from "./findInputDefinitionSchema"; import { ParsedTemplateSchema } from "./template/templateSchema"; +import { InputTypeSchema, nonEmptyString } from "./InputDefinitionSchema"; /** * Here are the core logic around the main domain of the plugin, * which is the form definition. * Here are the types, validators, rules etc. */ -function nonEmptyString(name: string) { - return string(`${name} should be a string`, [ - toTrimmed(), - minLength(1, `${name} should not be empty`), - ]); -} -const InputBasicTypeSchema = enumType([ - "text", - "number", - "date", - "time", - "datetime", - "textarea", - "toggle", - "email", - "tel", -]); -//=========== Schema definitions -export const SelectFromNotesSchema = object({ - type: literal("select"), - source: literal("notes"), - folder: nonEmptyString("folder name"), -}); -export const InputTagSchema = object({ - type: literal("tag"), - exclude: optional(string()), // This should be a regex string -}); -export const InputSliderSchema = object({ - type: literal("slider"), - min: number(), - max: number(), -}); -export const InputNoteFromFolderSchema = object({ - type: literal("note"), - folder: nonEmptyString("folder name"), -}); -export const InputFolderSchema = object({ - type: literal("folder"), - // TODO: allow exclude option -}); -export const InputDataviewSourceSchema = object({ - type: literal("dataview"), - query: nonEmptyString("dataview query"), -}); -export const InputBasicSchema = object({ type: InputBasicTypeSchema }); -export const InputSelectFixedSchema = object({ - type: literal("select"), - source: literal("fixed"), - options: array( - object({ - value: string([toTrimmed()]), - label: string(), - }), - ), -}); -const MultiSelectNotesSchema = object({ - type: literal("multiselect"), - source: literal("notes"), - folder: nonEmptyString("multi select source folder"), -}); -const MultiSelectFixedSchema = object({ - type: literal("multiselect"), - source: literal("fixed"), - multi_select_options: array(string()), -}); -const MultiSelectQuerySchema = object({ - type: literal("multiselect"), - source: literal("dataview"), - query: nonEmptyString("dataview query"), -}); -export const MultiselectSchema = union([ - MultiSelectNotesSchema, - MultiSelectFixedSchema, - MultiSelectQuerySchema, -]); -export const InputTypeSchema = union([ - InputBasicSchema, - InputNoteFromFolderSchema, - InputFolderSchema, - InputSliderSchema, - InputTagSchema, - SelectFromNotesSchema, - InputDataviewSourceSchema, - InputSelectFixedSchema, - MultiselectSchema, -]); -export const InputTypeToParserMap: Record< - AllFieldTypes, - ParsingFn -> = { - number: parseC(InputBasicSchema), - text: parseC(InputBasicSchema), - email: parseC(InputBasicSchema), - tel: parseC(InputBasicSchema), - date: parseC(InputBasicSchema), - time: parseC(InputBasicSchema), - datetime: parseC(InputBasicSchema), - textarea: parseC(InputBasicSchema), - toggle: parseC(InputBasicSchema), - note: parseC(InputNoteFromFolderSchema), - folder: parseC(InputFolderSchema), - slider: parseC(InputSliderSchema), - tag: parseC(InputTagSchema), - select: trySchemas([SelectFromNotesSchema, InputSelectFixedSchema]), - dataview: parseC(InputDataviewSourceSchema), - multiselect: parseC(MultiselectSchema), -}; export const FieldDefinitionSchema = object({ name: nonEmptyString("field name"), From 41616688a860152dab37fabc2a7e0d91866463bd Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Tue, 19 Dec 2023 09:46:14 +0100 Subject: [PATCH 2/9] wip: document block input type --- src/FormModal.ts | 207 +++++++++++++++++------------- src/core/InputDefinitionSchema.ts | 12 ++ src/core/formDefinition.ts | 3 +- src/exampleModalDefinition.ts | 18 ++- src/std/index.ts | 115 ++++++++++++----- src/views/FormBuilder.svelte | 4 +- 6 files changed, 225 insertions(+), 134 deletions(-) diff --git a/src/FormModal.ts b/src/FormModal.ts index 89830ac3..260b6b82 100644 --- a/src/FormModal.ts +++ b/src/FormModal.ts @@ -1,9 +1,8 @@ import { App, Modal, Platform, Setting } from "obsidian"; +import * as R from "fp-ts/Record"; import MultiSelect from "./views/components/MultiSelect.svelte"; -import FormResult, { - type ModalFormData, -} from "./core/FormResult"; -import { formDataFromFormDefaults } from './core/formDataFromFormDefaults'; +import FormResult, { type ModalFormData } from "./core/FormResult"; +import { formDataFromFormDefaults } from "./core/formDataFromFormDefaults"; import { exhaustiveGuard } from "./safety"; import { get_tfiles_from_folder } from "./utils/files"; import type { FormDefinition, FormOptions } from "./core/formDefinition"; @@ -19,12 +18,24 @@ import { log_error, log_notice } from "./utils/Log"; import { FieldValue, FormEngine, makeFormEngine } from "./store/formStore"; import { Writable } from "svelte/store"; import { FolderSuggest } from "./suggesters/suggestFolder"; +import { tryCatchK } from "fp-ts/Either"; export type SubmitFn = (formResult: FormResult) => void; +const notify = throttle( + (msg: string) => + log_notice("⚠️ The form has errors ⚠️", msg, "notice-warning"), + 2000, +); +const notifyError = (title: string) => + throttle( + (msg: string) => log_notice(`🚨 ${title} 🚨`, msg, "notice-error"), + 2000, + ); + export class FormModal extends Modal { svelteComponents: SvelteComponent[] = []; - initialFormValues: ModalFormData + initialFormValues: ModalFormData; subscriptions: (() => void)[] = []; formEngine: FormEngine; constructor( @@ -34,29 +45,21 @@ export class FormModal extends Modal { options?: FormOptions, ) { super(app); - this.initialFormValues = formDataFromFormDefaults(modalDefinition.fields, options?.values ?? {}) + this.initialFormValues = formDataFromFormDefaults( + modalDefinition.fields, + options?.values ?? {}, + ); this.formEngine = makeFormEngine((result) => { this.onSubmit(new FormResult(result, "ok")); this.close(); }, this.initialFormValues); - this.formEngine.subscribe(console.log) + // this.formEngine.subscribe(console.log); } - // onOpen2() { - // const { contentEl } = this; - // const component = new FormModalComponent({ - // target: contentEl, - // props: { - // onSubmit: this.onSubmit, - // formDefinition: this.modalDefinition, - // }, - // }); - // this.svelteComponents.push(component); - // } onOpen() { const { contentEl } = this; // This class is very important for scoped styles - contentEl.addClass('modal-form'); + contentEl.addClass("modal-form"); if (this.modalDefinition.customClassname) contentEl.addClass(this.modalDefinition.customClassname); contentEl.createEl("h1", { text: this.modalDefinition.title }); @@ -74,11 +77,10 @@ export class FormModal extends Modal { const subToErrors = ( input: HTMLInputElement | HTMLTextAreaElement, ) => { - const notify = throttle((msg: string) => log_notice('⚠️ The form has errors ⚠️', msg, 'notice-warning'), 2000) this.subscriptions.push( fieldStore.errors.subscribe((errs) => { - console.log('errors', errs) - errs.forEach(notify) + console.log("errors", errs); + errs.forEach(notify); input.setCustomValidity(errs.join("\n")); }), ); @@ -157,10 +159,7 @@ export class FormModal extends Modal { }); case "folder": return fieldBase.addText((element) => { - new FolderSuggest( - element.inputEl, - this.app, - ); + new FolderSuggest(element.inputEl, this.app); subToErrors(element.inputEl); element.onChange(fieldStore.value.set); }); @@ -181,22 +180,22 @@ export class FormModal extends Modal { source == "fixed" ? fieldInput.multi_select_options : source == "notes" - ? pipe( - get_tfiles_from_folder( - fieldInput.folder, - this.app, - ), - E.map(A.map((file) => file.basename)), - E.getOrElse((err) => { - log_error(err); - return [] as string[]; - }), - ) - : executeSandboxedDvQuery( - sandboxedDvQuery(fieldInput.query), - this.app, - ); - fieldStore.value.set(initialValue ?? []) + ? pipe( + get_tfiles_from_folder( + fieldInput.folder, + this.app, + ), + E.map(A.map((file) => file.basename)), + E.getOrElse((err) => { + log_error(err); + return [] as string[]; + }), + ) + : executeSandboxedDvQuery( + sandboxedDvQuery(fieldInput.query), + this.app, + ); + fieldStore.value.set(initialValue ?? []); this.svelteComponents.push( new MultiSelect({ target: fieldBase.controlEl, @@ -215,7 +214,7 @@ export class FormModal extends Modal { const options = Object.keys( this.app.metadataCache.getTags(), ).map((tag) => tag.slice(1)); // remove the # - fieldStore.value.set(initialValue ?? []) + fieldStore.value.set(initialValue ?? []); this.svelteComponents.push( new MultiSelect({ target: fieldBase.controlEl, @@ -238,59 +237,83 @@ export class FormModal extends Modal { subToErrors(element.inputEl); }); } - case "select": - { - const source = fieldInput.source; - switch (source) { - case "fixed": - return fieldBase.addDropdown((element) => { - fieldInput.options.forEach((option) => { - element.addOption( - option.value, - option.label, - ); - }); - fieldStore.value.set(element.getValue()); - element.onChange(fieldStore.value.set); + case "select": { + const source = fieldInput.source; + switch (source) { + case "fixed": + return fieldBase.addDropdown((element) => { + fieldInput.options.forEach((option) => { + element.addOption( + option.value, + option.label, + ); }); + fieldStore.value.set(element.getValue()); + element.onChange(fieldStore.value.set); + }); - case "notes": - return fieldBase.addDropdown((element) => { - const files = get_tfiles_from_folder( - fieldInput.folder, - this.app, - ); - pipe( - files, - E.map((files) => - files.reduce( - ( - acc: Record, - option, - ) => { - acc[option.basename] = - option.basename; - return acc; - }, - {}, - ), + case "notes": + return fieldBase.addDropdown((element) => { + const files = get_tfiles_from_folder( + fieldInput.folder, + this.app, + ); + pipe( + files, + E.map((files) => + files.reduce( + ( + acc: Record, + option, + ) => { + acc[option.basename] = + option.basename; + return acc; + }, + {}, ), - E.mapLeft((err) => { - log_error(err); - return err; - }), - E.map((options) => { - element.addOptions(options); - }), - ); - fieldStore.value.set(element.getValue()); - element.onChange(fieldStore.value.set); - }); - default: - exhaustiveGuard(source); - } + ), + E.mapLeft((err) => { + log_error(err); + return err; + }), + E.map((options) => { + element.addOptions(options); + }), + ); + fieldStore.value.set(element.getValue()); + element.onChange(fieldStore.value.set); + }); + default: + exhaustiveGuard(source); } break; + } + case "document_block": { + console.log("Hey, document block", fieldInput); + const functionBody = fieldInput.body; + const functionParsed = new Function( + "form", + functionBody, + ) as (form: Record) => string; + const domNode = fieldBase.infoEl.createDiv(); + const sub = this.formEngine.subscribe((form) => { + console.log("form in block", form); + pipe( + form.fields, + R.filterMap((field) => field.value), + tryCatchK(functionParsed, (err) => + JSON.stringify(err), + ), + E.match( + notifyError("Error in document block"), + (newText) => domNode.setText(newText), + ), + ); + }); + return this.subscriptions.push(sub); + } + default: return exhaustiveGuard(type); } @@ -318,6 +341,6 @@ export class FormModal extends Modal { this.svelteComponents.forEach((component) => component.$destroy()); this.subscriptions.forEach((subscription) => subscription()); contentEl.empty(); - this.initialFormValues = {} + this.initialFormValues = {}; } } diff --git a/src/core/InputDefinitionSchema.ts b/src/core/InputDefinitionSchema.ts index 57719097..d38b8ca1 100644 --- a/src/core/InputDefinitionSchema.ts +++ b/src/core/InputDefinitionSchema.ts @@ -98,6 +98,15 @@ export const MultiselectSchema = union([ MultiSelectFixedSchema, MultiSelectQuerySchema, ]); + +// This is a special type of input that lets you render a string +// based on the values of other fields. +const DocumentBlock = object({ + type: literal("document_block"), + body: string(), +}); + +// Codec for all the input types export const InputTypeSchema = union([ InputBasicSchema, InputNoteFromFolderSchema, @@ -108,7 +117,9 @@ export const InputTypeSchema = union([ InputDataviewSourceSchema, InputSelectFixedSchema, MultiselectSchema, + DocumentBlock, ]); + export const InputTypeToParserMap: Record< AllFieldTypes, ParsingFn @@ -129,6 +140,7 @@ export const InputTypeToParserMap: Record< select: trySchemas([SelectFromNotesSchema, InputSelectFixedSchema]), dataview: parseC(InputDataviewSourceSchema), multiselect: parseC(MultiselectSchema), + document_block: parseC(DocumentBlock), }; //=========== Types derived from schemas diff --git a/src/core/formDefinition.ts b/src/core/formDefinition.ts index cf99d3b5..4fad3a66 100644 --- a/src/core/formDefinition.ts +++ b/src/core/formDefinition.ts @@ -27,7 +27,7 @@ import { selectFromNotes, } from "./InputDefinitionSchema"; -export const FieldTypeReadable: Record = { +export const InputTypeReadable: Record = { text: "Text", number: "Number", tag: "Tags", @@ -44,6 +44,7 @@ export const FieldTypeReadable: Record = { select: "Select", dataview: "Dataview", multiselect: "Multiselect", + document_block: "Document block", } as const; export function isDataViewSource(input: unknown): input is inputDataviewSource { diff --git a/src/exampleModalDefinition.ts b/src/exampleModalDefinition.ts index a3e2a510..91784776 100644 --- a/src/exampleModalDefinition.ts +++ b/src/exampleModalDefinition.ts @@ -6,17 +6,18 @@ export const exampleModalDefinition: FormDefinition = { version: "1", fields: [ { - name: "Name", + name: "name", + label: "Name", description: "It is named how?", isRequired: true, - input: { type: "text", }, + input: { type: "text" }, }, { name: "age", label: "Age", description: "How old", isRequired: true, - input: { type: "number", }, + input: { type: "number" }, }, { name: "dateOfBirth", @@ -41,7 +42,8 @@ export const exampleModalDefinition: FormDefinition = { label: "Favorite book", description: "Pick one", input: { type: "note", folder: "Books" }, - }, { + }, + { name: "folder", label: "The destination folder", description: "It offers auto-completion to existing folders", @@ -145,5 +147,13 @@ export const exampleModalDefinition: FormDefinition = { description: "Tags input example", input: { type: "tag" }, }, + { + name: "document", + description: "Document block example", + input: { + type: "document_block", + body: "return `Hello ${form.name}!\n Your best friend is ${form.best_fried}`", + }, + }, ], }; diff --git a/src/std/index.ts b/src/std/index.ts index a95aa964..59eb1783 100644 --- a/src/std/index.ts +++ b/src/std/index.ts @@ -1,14 +1,45 @@ import { pipe as p, flow as f } from "fp-ts/function"; -import { partitionMap, findFirst, findFirstMap, partition, map as mapArr, filter, compact, filterMap } from "fp-ts/Array"; -import { map as mapO, getOrElse as getOrElseOpt, some, none, fromNullable as fromNullableOpt, fold as ofold } from 'fp-ts/Option' -import { isLeft, isRight, tryCatchK, map, getOrElse, fromNullable, right, left, mapLeft, Either, bimap, tryCatch, flatMap } from "fp-ts/Either"; +import { + partitionMap, + findFirst, + findFirstMap, + partition, + map as mapArr, + filter, + compact, + filterMap, +} from "fp-ts/Array"; +import { + map as mapO, + getOrElse as getOrElseOpt, + some, + none, + fromNullable as fromNullableOpt, + fold as ofold, +} from "fp-ts/Option"; +import { + isLeft, + isRight, + tryCatchK, + map, + getOrElse, + fromNullable, + right, + left, + mapLeft, + Either, + bimap, + tryCatch, + flatMap, + match, +} from "fp-ts/Either"; import { BaseSchema, Output, ValiError, parse as parseV } from "valibot"; import { Semigroup, concatAll } from "fp-ts/Semigroup"; import { NonEmptyArray, concatAll as concatAllNea } from "fp-ts/NonEmptyArray"; -export type { NonEmptyArray } from 'fp-ts/NonEmptyArray' -export type { Either, Left, Right } from 'fp-ts/Either' +export type { NonEmptyArray } from "fp-ts/NonEmptyArray"; +export type { Either, Left, Right } from "fp-ts/Either"; export const flow = f; -export const pipe = p +export const pipe = p; export const A = { partitionMap, partition, @@ -17,14 +48,14 @@ export const A = { findFirstMap, map: mapArr, filter, - filterMap -} + filterMap, +}; /** * Non empty array */ export const NEA = { - concatAll: concatAllNea -} + concatAll: concatAllNea, +}; export const E = { isLeft, @@ -39,68 +70,82 @@ export const E = { bimap, flatMap, fromNullable, -} + match, +}; export const O = { map: mapO, getOrElse: getOrElseOpt, - some, none, + some, + none, fold: ofold, fromNullable: fromNullableOpt, -} - +}; -export const parse = tryCatchK(parseV, (e: unknown) => e as ValiError) +export const parse = tryCatchK(parseV, (e: unknown) => e as ValiError); -type ParseOpts = Parameters[2] +type ParseOpts = Parameters[2]; /** - * + * * curried version of parse. * It takes first the schema and the options * and produces a function that takes the input * and returns the result of parsing the input with the schema and options. */ export function parseC(schema: S, options?: ParseOpts) { - return (input: unknown) => parse(schema, input, options) + return (input: unknown) => parse(schema, input, options); } -export type ParsingFn = (input: unknown) => Either> +export type ParsingFn = ( + input: unknown, +) => Either>; /** * Concatenates two parsing functions that return Either into one. * If the first function returns a Right, the second function is not called. */ -class _EFunSemigroup implements Semigroup> { - concat(f: ParsingFn, g: ParsingFn): (i: unknown) => Either { +class _EFunSemigroup + implements Semigroup> +{ + concat( + f: ParsingFn, + g: ParsingFn, + ): (i: unknown) => Either { return (i) => { - const fRes = f(i) - if (isRight(fRes)) return fRes - return g(i) - } + const fRes = f(i); + if (isRight(fRes)) return fRes; + return g(i); + }; } } -export const EFunSemigroup = new _EFunSemigroup() +export const EFunSemigroup = new _EFunSemigroup(); /** - * Takes an array of schemas and returns a function + * Takes an array of schemas and returns a function * that tries to parse the input with each schema. */ -export function trySchemas(schemas: NonEmptyArray, options?: ParseOpts) { - const [first, ...rest] = schemas +export function trySchemas( + schemas: NonEmptyArray, + options?: ParseOpts, +) { + const [first, ...rest] = schemas; return pipe( rest, A.map((schema) => parseC(schema, options)), concatAll(EFunSemigroup)(parseC(first, options)), - ) + ); } -export function throttle(cb: (...args: [...T]) => V, timeout?: number): (...args: [...T]) => V | undefined; +export function throttle( + cb: (...args: [...T]) => V, + timeout?: number, +): (...args: [...T]) => V | undefined; export function throttle(fn: (...args: unknown[]) => unknown, ms = 100) { let lastCall = 0; return function (...args: unknown[]) { const now = Date.now(); if (now - lastCall < ms) { - lastCall = now;//reset the last call time,so it needs cooldown time + lastCall = now; //reset the last call time,so it needs cooldown time return; } lastCall = now; @@ -110,7 +155,7 @@ export function throttle(fn: (...args: unknown[]) => unknown, ms = 100) { export function tap(msg: string) { return (x: T) => { - console.log(msg, x) - return x - } + console.log(msg, x); + return x; + }; } diff --git a/src/views/FormBuilder.svelte b/src/views/FormBuilder.svelte index d7ae4467..36e69cd0 100644 --- a/src/views/FormBuilder.svelte +++ b/src/views/FormBuilder.svelte @@ -3,8 +3,8 @@ type EditableFormDefinition, type FormDefinition, isValidFormDefinition, - FieldTypeReadable, validateFields, + InputTypeReadable, } from "src/core/formDefinition"; import { setIcon } from "obsidian"; import InputBuilderDataview from "./components/inputBuilderDataview.svelte"; @@ -309,7 +309,7 @@ bind:value={field.input.type} id={`type_${index}`} > - {#each Object.entries(FieldTypeReadable) as type} + {#each Object.entries(InputTypeReadable) as type} From 4d6ee27040215d5f74327303311b252899c2036c Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Tue, 19 Dec 2023 19:02:47 +0100 Subject: [PATCH 3/9] feat: safer parsing of functions --- src/std/index.test.ts | 54 +++++++++++++++++++++++++++++++++++-------- src/std/index.ts | 28 ++++++++++++++++++++++ 2 files changed, 72 insertions(+), 10 deletions(-) diff --git a/src/std/index.test.ts b/src/std/index.test.ts index f02f5390..f037717d 100644 --- a/src/std/index.test.ts +++ b/src/std/index.test.ts @@ -1,4 +1,4 @@ -import { E, pipe, trySchemas } from "./index"; +import { E, parseFunctionBody, pipe, trySchemas } from "./index"; import { string, number, array, boolean, object } from "valibot"; describe("trySchemas", () => { @@ -29,7 +29,7 @@ describe("trySchemas", () => { hobbies: ["reading", "swimming"], isEmployed: true, }; - console.log(' ====== 1 =====') + console.log(" ====== 1 ====="); const result = trySchemas([schema1, schema2, schema3])(input); expect(result).toEqual({ _tag: "Right", @@ -49,7 +49,7 @@ describe("trySchemas", () => { age: 25, isStudent: true, }; - console.log(' ====== 2 =====') + console.log(" ====== 2 ====="); const result = trySchemas([schema1, schema2, schema3])(input); expect(result).toEqual({ _tag: "Right", @@ -69,12 +69,12 @@ describe("trySchemas", () => { hobbies: ["reading", "swimming"], isEmployed: true, }; - console.log(' ====== 3 =====') + console.log(" ====== 3 ====="); const result = trySchemas([schema1, schema2, schema3])(input); if (E.isLeft(result)) { - expect(result.left.message).toEqual('Invalid type'); + expect(result.left.message).toEqual("Invalid type"); } else { - fail('expected a Left') + fail("expected a Left"); } }); @@ -85,7 +85,7 @@ describe("trySchemas", () => { hobbies: ["reading", "swimming"], isEmployed: true, }; - console.log(' ====== 4 =====') + console.log(" ====== 4 ====="); const result = trySchemas([schema1])(input); expect(result).toEqual(E.right(input)); }); @@ -101,8 +101,42 @@ describe("trySchemas", () => { pipe( result, E.bimap( - (x) => expect(x.message).toEqual('Invalid type'), - () => fail('expected a Left') - )) + (x) => expect(x.message).toEqual("Invalid type"), + () => fail("expected a Left"), + ), + ); + }); +}); + +describe("parseFunctionBody", () => { + it("should parse a function body", () => { + const input = "return x + 1;"; + const result = parseFunctionBody(input); + pipe( + result, + E.match( + (err) => fail("Expected a right"), + (result) => expect(result.toString()).toMatch(input), + ), + ); + }); + it("should fail to parse a function body when it is incorrect", () => { + const input = "{ return x + 1; "; + const result = parseFunctionBody(input); + expect(result).toEqual(E.left(new SyntaxError("Unexpected token ')'"))); + }); + it("should parse a function body with arguments", () => { + const input = "return x + 1;"; + const result = parseFunctionBody<[number], number>(input, "x"); + pipe( + result, + E.match( + () => fail("Expected a right"), + (result) => { + expect(result.toString()).toMatch(input); + expect(result(1)).toEqual(2); + }, + ), + ); }); }); diff --git a/src/std/index.ts b/src/std/index.ts index 59eb1783..1e152d79 100644 --- a/src/std/index.ts +++ b/src/std/index.ts @@ -159,3 +159,31 @@ export function tap(msg: string) { return x; }; } + +function ensureError(e: unknown): Error { + return e instanceof Error ? e : new Error(String(e)); +} + +/** + * Creates a function from a string that is supposed to be a function body. + * It ensures the "use strict" directive is present and returns the function. + * Because the parsing can fail, it returns an Either. + * The reason why the type arguments are reversed is because + * we often know what the function input types should be, but + * we can't trust the function body to return the correct type, so by default1t it will be unknown + */ +export function parseFunctionBody( + body: string, + ...args: string[] +) { + const fnBody = `"use strict"; +${body}`; + try { + return right(new Function(...args, fnBody)) as Either< + Error, + (...args: Args) => T + >; + } catch (e) { + return left(ensureError(e)); + } +} From a09f72f409d3bc3249534654b4887fe95596e376 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Wed, 20 Dec 2023 08:42:46 +0100 Subject: [PATCH 4/9] feat: safely parse and execute code blocks functions --- .prettierrc | 9 ++-- src/FormModal.ts | 127 ++++++++++++++++------------------------------- src/std/index.ts | 35 +++++-------- 3 files changed, 60 insertions(+), 111 deletions(-) diff --git a/.prettierrc b/.prettierrc index a0e6323b..664d4497 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,9 +1,8 @@ { - "plugins": [ - "prettier-plugin-svelte" - ], + "plugins": ["prettier-plugin-svelte"], "arrowParens": "always", "editorconfig": true, "svelteAllowShorthand": true, - "trailingComma": "all" -} \ No newline at end of file + "trailingComma": "all", + "printWidth": 120 +} diff --git a/src/FormModal.ts b/src/FormModal.ts index 260b6b82..06f63c94 100644 --- a/src/FormModal.ts +++ b/src/FormModal.ts @@ -9,29 +9,18 @@ import type { FormDefinition, FormOptions } from "./core/formDefinition"; import { FileSuggest } from "./suggesters/suggestFile"; import { DataviewSuggest } from "./suggesters/suggestFromDataview"; import { SvelteComponent } from "svelte"; -import { - executeSandboxedDvQuery, - sandboxedDvQuery, -} from "./suggesters/SafeDataviewQuery"; -import { A, E, pipe, throttle } from "@std"; +import { executeSandboxedDvQuery, sandboxedDvQuery } from "./suggesters/SafeDataviewQuery"; +import { A, E, flow, parseFunctionBody, pipe, throttle } from "@std"; import { log_error, log_notice } from "./utils/Log"; import { FieldValue, FormEngine, makeFormEngine } from "./store/formStore"; import { Writable } from "svelte/store"; import { FolderSuggest } from "./suggesters/suggestFolder"; -import { tryCatchK } from "fp-ts/Either"; export type SubmitFn = (formResult: FormResult) => void; -const notify = throttle( - (msg: string) => - log_notice("⚠️ The form has errors ⚠️", msg, "notice-warning"), - 2000, -); +const notify = throttle((msg: string) => log_notice("⚠️ The form has errors ⚠️", msg, "notice-warning"), 2000); const notifyError = (title: string) => - throttle( - (msg: string) => log_notice(`🚨 ${title} 🚨`, msg, "notice-error"), - 2000, - ); + throttle((msg: string) => log_notice(`🚨 ${title} 🚨`, msg, "notice-error"), 2000); export class FormModal extends Modal { svelteComponents: SvelteComponent[] = []; @@ -45,10 +34,7 @@ export class FormModal extends Modal { options?: FormOptions, ) { super(app); - this.initialFormValues = formDataFromFormDefaults( - modalDefinition.fields, - options?.values ?? {}, - ); + this.initialFormValues = formDataFromFormDefaults(modalDefinition.fields, options?.values ?? {}); this.formEngine = makeFormEngine((result) => { this.onSubmit(new FormResult(result, "ok")); this.close(); @@ -60,8 +46,7 @@ export class FormModal extends Modal { const { contentEl } = this; // This class is very important for scoped styles contentEl.addClass("modal-form"); - if (this.modalDefinition.customClassname) - contentEl.addClass(this.modalDefinition.customClassname); + if (this.modalDefinition.customClassname) contentEl.addClass(this.modalDefinition.customClassname); contentEl.createEl("h1", { text: this.modalDefinition.title }); this.modalDefinition.fields.forEach((definition) => { const fieldBase = new Setting(contentEl) @@ -74,12 +59,10 @@ export class FormModal extends Modal { const type = fieldInput.type; const initialValue = this.initialFormValues[definition.name]; const fieldStore = this.formEngine.addField(definition); - const subToErrors = ( - input: HTMLInputElement | HTMLTextAreaElement, - ) => { + const subToErrors = (input: HTMLInputElement | HTMLTextAreaElement) => { this.subscriptions.push( fieldStore.errors.subscribe((errs) => { - console.log("errors", errs); + errs.length > 0 ? console.log("errors", errs) : void 0; errs.forEach(notify); input.setCustomValidity(errs.join("\n")); }), @@ -95,8 +78,7 @@ export class FormModal extends Modal { textEl.setValue(initialValue); } textEl.inputEl.rows = 6; - if (Platform.isIosApp) - textEl.inputEl.style.width = "100%"; + if (Platform.isIosApp) textEl.inputEl.style.width = "100%"; else if (Platform.isDesktopApp) { textEl.inputEl.rows = 10; } @@ -111,8 +93,7 @@ export class FormModal extends Modal { text.inputEl.type = type; subToErrors(text.inputEl); text.onChange(fieldStore.value.set); - initialValue !== undefined && - text.setValue(String(initialValue)); + initialValue !== undefined && text.setValue(String(initialValue)); }); case "number": return fieldBase.addText((text) => { @@ -123,14 +104,12 @@ export class FormModal extends Modal { fieldStore.value.set(Number(val) + ""); } }); - initialValue !== undefined && - text.setValue(String(initialValue)); + initialValue !== undefined && text.setValue(String(initialValue)); }); case "datetime": return fieldBase.addText((text) => { text.inputEl.type = "datetime-local"; - initialValue !== undefined && - text.setValue(String(initialValue)); + initialValue !== undefined && text.setValue(String(initialValue)); subToErrors(text.inputEl); text.onChange(fieldStore.value.set); }); @@ -180,21 +159,15 @@ export class FormModal extends Modal { source == "fixed" ? fieldInput.multi_select_options : source == "notes" - ? pipe( - get_tfiles_from_folder( - fieldInput.folder, - this.app, - ), - E.map(A.map((file) => file.basename)), - E.getOrElse((err) => { - log_error(err); - return [] as string[]; - }), - ) - : executeSandboxedDvQuery( - sandboxedDvQuery(fieldInput.query), - this.app, - ); + ? pipe( + get_tfiles_from_folder(fieldInput.folder, this.app), + E.map(A.map((file) => file.basename)), + E.getOrElse((err) => { + log_error(err); + return [] as string[]; + }), + ) + : executeSandboxedDvQuery(sandboxedDvQuery(fieldInput.query), this.app); fieldStore.value.set(initialValue ?? []); this.svelteComponents.push( new MultiSelect({ @@ -211,9 +184,7 @@ export class FormModal extends Modal { return; } case "tag": { - const options = Object.keys( - this.app.metadataCache.getTags(), - ).map((tag) => tag.slice(1)); // remove the # + const options = Object.keys(this.app.metadataCache.getTags()).map((tag) => tag.slice(1)); // remove the # fieldStore.value.set(initialValue ?? []); this.svelteComponents.push( new MultiSelect({ @@ -243,10 +214,7 @@ export class FormModal extends Modal { case "fixed": return fieldBase.addDropdown((element) => { fieldInput.options.forEach((option) => { - element.addOption( - option.value, - option.label, - ); + element.addOption(option.value, option.label); }); fieldStore.value.set(element.getValue()); element.onChange(fieldStore.value.set); @@ -254,24 +222,14 @@ export class FormModal extends Modal { case "notes": return fieldBase.addDropdown((element) => { - const files = get_tfiles_from_folder( - fieldInput.folder, - this.app, - ); + const files = get_tfiles_from_folder(fieldInput.folder, this.app); pipe( files, E.map((files) => - files.reduce( - ( - acc: Record, - option, - ) => { - acc[option.basename] = - option.basename; - return acc; - }, - {}, - ), + files.reduce((acc: Record, option) => { + acc[option.basename] = option.basename; + return acc; + }, {}), ), E.mapLeft((err) => { log_error(err); @@ -290,23 +248,27 @@ export class FormModal extends Modal { break; } case "document_block": { - console.log("Hey, document block", fieldInput); const functionBody = fieldInput.body; - const functionParsed = new Function( - "form", + const functionParsed = parseFunctionBody<[Record], string>( functionBody, - ) as (form: Record) => string; + "form", + ); const domNode = fieldBase.infoEl.createDiv(); const sub = this.formEngine.subscribe((form) => { - console.log("form in block", form); pipe( - form.fields, - R.filterMap((field) => field.value), - tryCatchK(functionParsed, (err) => - JSON.stringify(err), + functionParsed, + E.chainW((fn) => + pipe( + form.fields, + R.filterMap((field) => field.value), + fn, + ), ), E.match( - notifyError("Error in document block"), + (error) => { + console.error(error); + notifyError("Error in document block")(String(error)); + }, (newText) => domNode.setText(newText), ), ); @@ -320,10 +282,7 @@ export class FormModal extends Modal { }); new Setting(contentEl).addButton((btn) => - btn - .setButtonText("Submit") - .setCta() - .onClick(this.formEngine.triggerSubmit), + btn.setButtonText("Submit").setCta().onClick(this.formEngine.triggerSubmit), ); const submitEnterCallback = (evt: KeyboardEvent) => { diff --git a/src/std/index.ts b/src/std/index.ts index 1e152d79..c37b558f 100644 --- a/src/std/index.ts +++ b/src/std/index.ts @@ -31,6 +31,9 @@ import { bimap, tryCatch, flatMap, + ap, + flap, + chainW, match, } from "fp-ts/Either"; import { BaseSchema, Output, ValiError, parse as parseV } from "valibot"; @@ -71,6 +74,9 @@ export const E = { flatMap, fromNullable, match, + ap, + flap, + chainW, }; export const O = { @@ -96,20 +102,13 @@ type ParseOpts = Parameters[2]; export function parseC(schema: S, options?: ParseOpts) { return (input: unknown) => parse(schema, input, options); } -export type ParsingFn = ( - input: unknown, -) => Either>; +export type ParsingFn = (input: unknown) => Either>; /** * Concatenates two parsing functions that return Either into one. * If the first function returns a Right, the second function is not called. */ -class _EFunSemigroup - implements Semigroup> -{ - concat( - f: ParsingFn, - g: ParsingFn, - ): (i: unknown) => Either { +class _EFunSemigroup implements Semigroup> { + concat(f: ParsingFn, g: ParsingFn): (i: unknown) => Either { return (i) => { const fRes = f(i); if (isRight(fRes)) return fRes; @@ -124,10 +123,7 @@ export const EFunSemigroup = new _EFunSemigroup(); * Takes an array of schemas and returns a function * that tries to parse the input with each schema. */ -export function trySchemas( - schemas: NonEmptyArray, - options?: ParseOpts, -) { +export function trySchemas(schemas: NonEmptyArray, options?: ParseOpts) { const [first, ...rest] = schemas; return pipe( rest, @@ -172,17 +168,12 @@ function ensureError(e: unknown): Error { * we often know what the function input types should be, but * we can't trust the function body to return the correct type, so by default1t it will be unknown */ -export function parseFunctionBody( - body: string, - ...args: string[] -) { +export function parseFunctionBody(body: string, ...args: string[]) { const fnBody = `"use strict"; ${body}`; try { - return right(new Function(...args, fnBody)) as Either< - Error, - (...args: Args) => T - >; + const fn = new Function(...args, fnBody) as (...args: Args) => T; + return right(tryCatchK(fn, ensureError)); } catch (e) { return left(ensureError(e)); } From 526b2de6cacb21604974c69d12d0f45142fd9963 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Wed, 20 Dec 2023 09:11:21 +0100 Subject: [PATCH 5/9] chore: fix tests --- src/std/index.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/std/index.test.ts b/src/std/index.test.ts index f037717d..bfa6ebbe 100644 --- a/src/std/index.test.ts +++ b/src/std/index.test.ts @@ -1,3 +1,4 @@ +import { right } from "fp-ts/Separated"; import { E, parseFunctionBody, pipe, trySchemas } from "./index"; import { string, number, array, boolean, object } from "valibot"; @@ -116,7 +117,7 @@ describe("parseFunctionBody", () => { result, E.match( (err) => fail("Expected a right"), - (result) => expect(result.toString()).toMatch(input), + (result) => expect(result).toBeInstanceOf(Function), ), ); }); @@ -125,7 +126,7 @@ describe("parseFunctionBody", () => { const result = parseFunctionBody(input); expect(result).toEqual(E.left(new SyntaxError("Unexpected token ')'"))); }); - it("should parse a function body with arguments", () => { + it("should parse a function body with arguments and be able to execute it", () => { const input = "return x + 1;"; const result = parseFunctionBody<[number], number>(input, "x"); pipe( @@ -133,8 +134,7 @@ describe("parseFunctionBody", () => { E.match( () => fail("Expected a right"), (result) => { - expect(result.toString()).toMatch(input); - expect(result(1)).toEqual(2); + expect(result(1)).toEqual(E.right(2)); }, ), ); From 1949e03b241d6b67ac10465669843102cf4bb001 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Wed, 20 Dec 2023 09:15:30 +0100 Subject: [PATCH 6/9] feat: text document block. Allows to render a text in the form using the current form values --- src/FormModal.ts | 2 +- src/std/index.test.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/FormModal.ts b/src/FormModal.ts index 06f63c94..4cdcd562 100644 --- a/src/FormModal.ts +++ b/src/FormModal.ts @@ -10,7 +10,7 @@ import { FileSuggest } from "./suggesters/suggestFile"; import { DataviewSuggest } from "./suggesters/suggestFromDataview"; import { SvelteComponent } from "svelte"; import { executeSandboxedDvQuery, sandboxedDvQuery } from "./suggesters/SafeDataviewQuery"; -import { A, E, flow, parseFunctionBody, pipe, throttle } from "@std"; +import { A, E, parseFunctionBody, pipe, throttle } from "@std"; import { log_error, log_notice } from "./utils/Log"; import { FieldValue, FormEngine, makeFormEngine } from "./store/formStore"; import { Writable } from "svelte/store"; diff --git a/src/std/index.test.ts b/src/std/index.test.ts index bfa6ebbe..6086fe66 100644 --- a/src/std/index.test.ts +++ b/src/std/index.test.ts @@ -1,4 +1,3 @@ -import { right } from "fp-ts/Separated"; import { E, parseFunctionBody, pipe, trySchemas } from "./index"; import { string, number, array, boolean, object } from "valibot"; From 533446b125d97a44b7a76a3a5dceeab5e9529a48 Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Fri, 22 Dec 2023 15:12:52 +0100 Subject: [PATCH 7/9] chore: docs --- src/std/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/std/index.ts b/src/std/index.ts index c37b558f..fc21cf31 100644 --- a/src/std/index.ts +++ b/src/std/index.ts @@ -167,6 +167,7 @@ function ensureError(e: unknown): Error { * The reason why the type arguments are reversed is because * we often know what the function input types should be, but * we can't trust the function body to return the correct type, so by default1t it will be unknown + * The returned function is also wrapped to never throw and return an Either instead */ export function parseFunctionBody(body: string, ...args: string[]) { const fnBody = `"use strict"; From ec1d44bae6e6e5f823a0d9f9715a7883d341358a Mon Sep 17 00:00:00 2001 From: Danielo Rodriguez Date: Sun, 24 Dec 2023 12:03:23 +0100 Subject: [PATCH 8/9] feat: document block input builder --- .../InputBuilderDocumentBlock.svelte | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/views/components/InputBuilderDocumentBlock.svelte diff --git a/src/views/components/InputBuilderDocumentBlock.svelte b/src/views/components/InputBuilderDocumentBlock.svelte new file mode 100644 index 00000000..134ee058 --- /dev/null +++ b/src/views/components/InputBuilderDocumentBlock.svelte @@ -0,0 +1,25 @@ + + + + + This is a document block input. It is not meant to be used as a normal + input, instead it is to render some instructions to the user. It is + expected to be a function body that returns a string. Within the + function body, you can access the form data using the form + variable. For example: +
{placeholder}
+