diff --git a/src/helpers/case.ts b/src/helpers/case.ts new file mode 100644 index 0000000..af8160c --- /dev/null +++ b/src/helpers/case.ts @@ -0,0 +1,16 @@ +import { HandlebarsHelper, HelperConstructorBlock } from "./helper"; + +export const caseHelper: HelperConstructorBlock = ctx => { + return new HandlebarsHelper("case", (type, rawV1): string => { + const v1 = new String(rawV1).toString(); + + switch (type) { + case "upper": + return v1.toUpperCase(); + case "lower": + return v1.toLowerCase(); + default: + throw new Error(`Invalid case type used with case: ${type}`); + } + }); +}; diff --git a/src/helpers/compare.ts b/src/helpers/compare.ts new file mode 100644 index 0000000..ce5f764 --- /dev/null +++ b/src/helpers/compare.ts @@ -0,0 +1,42 @@ +import { HandlebarsHelper, HelperConstructorBlock } from "./helper"; + +export const compareHelper: HelperConstructorBlock = ctx => { + return new HandlebarsHelper("compare", (v1, operator, v2): boolean => { + switch (operator) { + case "==": + case "eq": + case "equals": + return (v1 == v2); + case "===": + case "seq": + case "strictly-equals": + return (v1 === v2); + case "!=": + case "ne": + case "not-equals": + return (v1 != v2); + case "!==": + case "sne": + case "strictly-not-equals": + return (v1 !== v2); + case "<": + case "lt": + case "less-than": + return (v1 < v2); + case "<=": + case "lte": + case "less-than-equals": + return (v1 <= v2); + case ">": + case "gt": + case "greater-than": + return (v1 > v2); + case ">=": + case "gte": + case "greater-than-equals": + return (v1 >= v2); + default: + throw new Error(`Invalid comparison operator used with compare: ${operator}`); + } + }); +}; diff --git a/src/helpers/condition.ts b/src/helpers/condition.ts new file mode 100644 index 0000000..7c95aa8 --- /dev/null +++ b/src/helpers/condition.ts @@ -0,0 +1,19 @@ +import { HandlebarsHelper, HelperConstructorBlock } from "./helper"; + +export const conditionHelper: HelperConstructorBlock = ctx => { + return new HandlebarsHelper("condition", (v1, operator, v2): boolean => { + switch (operator) { + case "!": + case "not": + return !v1; + case "&&": + case "and": + return (v1 && v2); + case "||": + case "or": + return (v1 || v2); + default: + throw new Error(`Invalid operator used with condition: ${operator}`); + } + }); +}; diff --git a/src/helpers/context.ts b/src/helpers/context.ts new file mode 100644 index 0000000..dd64e6b --- /dev/null +++ b/src/helpers/context.ts @@ -0,0 +1,9 @@ +import { DateAndTimeUtils } from "../utils/dateAndTime"; + +export class HelperContext { + public dateAndTimeUtils: DateAndTimeUtils; + + constructor(dateUtils: DateAndTimeUtils) { + this.dateAndTimeUtils = dateUtils; + } +} diff --git a/src/helpers/custom_datetime.ts b/src/helpers/custom_datetime.ts new file mode 100644 index 0000000..eb06f09 --- /dev/null +++ b/src/helpers/custom_datetime.ts @@ -0,0 +1,7 @@ +import { HandlebarsHelper, HelperConstructorBlock } from "./helper"; + +export const customDatetimeHelper: HelperConstructorBlock = ctx => { + return new HandlebarsHelper("custom_datetime", (options) => { + return ctx.dateAndTimeUtils.getCurrentTime(options.fn(this)); + }); +}; diff --git a/src/helpers/datetime.ts b/src/helpers/datetime.ts new file mode 100644 index 0000000..a27c9d2 --- /dev/null +++ b/src/helpers/datetime.ts @@ -0,0 +1,94 @@ +import { HandlebarsHelper, HelperConstructorBlock } from "./helper"; +import { AttributeValueType, AttributeDefinition, AttributeParser } from "./utils/attributes"; +import * as moment from "moment"; + +const FORMAT = "format"; +const SET_DATE = "set_date"; +const SET_TIME = "set_time"; +const DELTA_YEARS = "delta_years"; +const DELTA_MONTHS = "delta_months"; +const DELTA_DAYS = "delta_days"; +const DELTA_HOURS = "delta_hours"; +const DELTA_MINUTES = "delta_minutes"; +const DELTA_SECONDS = "delta_seconds"; + +export const datetimeHelper: HelperConstructorBlock = (ctx) => { + const schema: AttributeDefinition[] = [ + { + name: FORMAT, + valueType: AttributeValueType.String, + defaultValue: ctx.dateAndTimeUtils.getDateTimeFormat() + }, + { + name: SET_DATE, + valueType: AttributeValueType.String, + defaultValue: "" + }, + { + name: SET_TIME, + valueType: AttributeValueType.String, + defaultValue: "" + }, + { + name: DELTA_YEARS, + valueType: AttributeValueType.Number, + defaultValue: 0 + }, + { + name: DELTA_MONTHS, + valueType: AttributeValueType.Number, + defaultValue: 0 + }, + { + name: DELTA_DAYS, + valueType: AttributeValueType.Number, + defaultValue: 0 + }, + { + name: DELTA_HOURS, + valueType: AttributeValueType.Number, + defaultValue: 0 + }, + { + name: DELTA_MINUTES, + valueType: AttributeValueType.Number, + defaultValue: 0 + }, + { + name: DELTA_SECONDS, + valueType: AttributeValueType.Number, + defaultValue: 0 + } + ]; + + return new HandlebarsHelper("datetime", function (options) { + const parser = new AttributeParser(schema); + const attrs = parser.parse(options.hash); + + const now = moment(new Date().getTime()); + + if (attrs[SET_DATE]) { + const parsedDate = ctx.dateAndTimeUtils.parseDate(attrs[SET_DATE] as string, ctx.dateAndTimeUtils.getDateFormat()); + now.set("date", parsedDate.date); + now.set("month", parsedDate.month); + now.set("year", parsedDate.year); + } + + if (attrs[SET_TIME]) { + const parsedTime = ctx.dateAndTimeUtils.parseTime(attrs[SET_TIME] as string, ctx.dateAndTimeUtils.getTimeFormat()); + now.set("hours", parsedTime.hours); + now.set("minutes", parsedTime.minutes); + now.set("seconds", 0); + now.set("milliseconds", 0); + } + + now.add(attrs[DELTA_YEARS] as number, "years"); + now.add(attrs[DELTA_MONTHS] as number, "months"); + now.add(attrs[DELTA_DAYS] as number, "days"); + now.add(attrs[DELTA_HOURS] as number, "hours"); + now.add(attrs[DELTA_MINUTES] as number, "minutes"); + now.add(attrs[DELTA_SECONDS] as number, "seconds"); + + return ctx.dateAndTimeUtils.formatMsToLocal(now.toDate().getTime(), attrs[FORMAT] as string); + }); +}; diff --git a/src/helpers/helper.ts b/src/helpers/helper.ts new file mode 100644 index 0000000..2f9d26e --- /dev/null +++ b/src/helpers/helper.ts @@ -0,0 +1,21 @@ +import * as Handlebars from "handlebars/dist/handlebars"; +import { HelperContext } from "./context"; + +export type HelperConstructorBlock = (ctx: HelperContext) => HandlebarsHelper; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type HelperImpl = (...args: Array) => any; + +export class HandlebarsHelper { + private tag: string; + private impl: HelperImpl; + + constructor(tag: string, impl: HelperImpl) { + this.tag = tag; + this.impl = impl; + } + + public register(): void { + Handlebars.registerHelper(this.tag, this.impl); + } +} diff --git a/src/helpers/index.ts b/src/helpers/index.ts new file mode 100644 index 0000000..84b97f7 --- /dev/null +++ b/src/helpers/index.ts @@ -0,0 +1,30 @@ +import { DateAndTimeUtils } from "../utils/dateAndTime"; +import { HelperConstructorBlock } from "./helper"; +import { HelperContext } from "./context"; + +import { customDatetimeHelper } from "./custom_datetime"; +import { compareHelper } from "./compare"; +import { mathHelper } from "./math"; +import { conditionHelper } from "./condition"; +import { repeatHelper } from "./repeat"; +import { datetimeHelper } from "./datetime"; +import { caseHelper } from "./case"; + +export class HelperFactory { + private static helpers: HelperConstructorBlock[] = [ + customDatetimeHelper, + compareHelper, + mathHelper, + conditionHelper, + repeatHelper, + datetimeHelper, + caseHelper, + ]; + + static registerHelpers(dateAndTimeUtils: DateAndTimeUtils): void { + const context = new HelperContext(dateAndTimeUtils); + for (const helper of this.helpers) { + helper(context).register(); + } + } +} diff --git a/src/helpers/math.ts b/src/helpers/math.ts new file mode 100644 index 0000000..e01f290 --- /dev/null +++ b/src/helpers/math.ts @@ -0,0 +1,30 @@ +import { HandlebarsHelper, HelperConstructorBlock } from "./helper"; + +export const mathHelper: HelperConstructorBlock = ctx => { + return new HandlebarsHelper("math", (rawV1, operator, rawV2): number => { + const v1 = Number.parseFloat(rawV1); + const v2 = Number.parseFloat(rawV2); + + if (Number.isNaN(v1) || Number.isNaN(v2)) { + throw new Error(`Can't convert "${rawV1}" and "${rawV2}" to numbers while using math`); + } + + switch (operator) { + case "+": + return v1 + v2; + case "-": + return v1 - v2; + case "*": + return v1 * v2; + case "/": + return v1 / v2; + case "**": + return v1 ** v2; + case "%": + if (!v2) throw new Error("% operator used with 0"); + return v1 % v2; + default: + throw new Error(`Invalid operator used with math: ${operator}`); + } + }); +}; diff --git a/src/helpers/repeat.ts b/src/helpers/repeat.ts new file mode 100644 index 0000000..881c51a --- /dev/null +++ b/src/helpers/repeat.ts @@ -0,0 +1,17 @@ +import { HandlebarsHelper, HelperConstructorBlock } from "./helper"; + +export const repeatHelper: HelperConstructorBlock = (ctx) => { + return new HandlebarsHelper("repeat", function (rawNum, options) { + const num = Number.parseInt(rawNum); + + if (Number.isNaN(num)) { + throw new Error(`Can't convert "${rawNum}" to number while using repeat`); + } + + let ret = ""; + for (let i = 0; i < rawNum; i++) { + ret += options.fn({ ...this, "repeat_index": i }); + } + return ret; + }); +}; diff --git a/src/helpers/utils/attributes.ts b/src/helpers/utils/attributes.ts new file mode 100644 index 0000000..0b8676e --- /dev/null +++ b/src/helpers/utils/attributes.ts @@ -0,0 +1,57 @@ +export enum AttributeValueType { + Number = "number", + String = "string", + Boolean = "boolean", +} + +export interface AttributeDefinition { + name: string; + valueType: AttributeValueType; + defaultValue: unknown +} + +interface RawAttributes { + [attr: string]: unknown +} + +export interface ParsedAttributes { + [attr: string]: string | number | boolean +} + +export class AttributeParser { + constructor(private schema: AttributeDefinition[]) {} + + private parseAttribute(attr: AttributeDefinition, rawValue: unknown) { + switch (attr.valueType) { + case AttributeValueType.Boolean: + return !!rawValue; + case AttributeValueType.Number: { + const v = typeof rawValue === "string" ? Number.parseFloat(rawValue) : rawValue; + if (typeof v !== "number" || Number.isNaN(v)) { + throw new Error(`Can't convert "${rawValue}" to number while parsing ${attr.name}.`); + } + return v; + } + case AttributeValueType.String: + return new String(rawValue).toString(); + } + } + + parse(rawAttributes: RawAttributes): ParsedAttributes { + const parsedAttributes = {}; + + if (!(typeof rawAttributes === "object")) { + throw new Error("There was an error parsing attributes.") + } + + for (const attr of this.schema) { + if (attr.name in rawAttributes) { + parsedAttributes[attr.name] = this.parseAttribute(attr, rawAttributes[attr.name]); + } else { + parsedAttributes[attr.name] = attr.defaultValue; + } + } + + return parsedAttributes; + } +} diff --git a/src/parser.ts b/src/parser.ts index 5081209..9231aa7 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -7,6 +7,7 @@ import { Note } from "./utils/templates"; import { getVariableFromDefinition } from "./variables/parser"; import { CustomVariable } from "./variables/types/base"; import { setTemplateVariablesView } from "./views/templateVariables"; +import { HelperFactory } from "./helpers"; // Can't use import for this library because the types in the library // are declared incorrectly which result in typescript errors. @@ -38,10 +39,6 @@ export class Parser { } private getDefaultContext() { - Handlebars.registerHelper("custom_datetime", (options) => { - return this.utils.getCurrentTime(options.fn(this)); - }); - return { date: this.utils.getCurrentTime(this.utils.getDateFormat()), time: this.utils.getCurrentTime(this.utils.getTimeFormat()), @@ -231,6 +228,8 @@ export class Parser { template.body = this.preProcessTemplateBody(template.body); try { + HelperFactory.registerHelpers(this.utils); + const processedTemplate = frontmatter(template.body); const templateVariables = processedTemplate.attributes; diff --git a/src/utils/dateAndTime.ts b/src/utils/dateAndTime.ts index e5da39e..60ea427 100644 --- a/src/utils/dateAndTime.ts +++ b/src/utils/dateAndTime.ts @@ -1,5 +1,19 @@ import * as moment from "moment"; +// These are meant to parse the date and time formats +// supported by Joplin. It doesn't support seconds or +// milliseconds. +interface ParsedDate { + date: number; + month: number; + year: number; +} + +interface ParsedTime { + hours: number; + minutes: number; +} + export class DateAndTimeUtils { private locale: string; private dateFormat: string; @@ -43,4 +57,31 @@ export class DateAndTimeUtils { const diff = day >= startIndex ? day - startIndex : 6 - day; return new Date().setDate(currentDate.getDate() - diff); } + + public parseDate(input: string, format: string): ParsedDate { + const date = moment(input, format, true); + + if (!date.isValid()) { + throw new Error(`Was not able to parse ${input} according to format ${format}`); + } + + return { + date: date.date(), + month: date.month(), + year: date.year(), + }; + } + + public parseTime(input: string, format: string): ParsedTime { + const time = moment(input, format, true); + + if (!time.isValid()) { + throw new Error(`Was not able to parse ${input} according to format ${format}`); + } + + return { + hours: time.hours(), + minutes: time.minutes(), + }; + } } diff --git a/tests/helpers/utils/attributes.spec.ts b/tests/helpers/utils/attributes.spec.ts new file mode 100644 index 0000000..69281fb --- /dev/null +++ b/tests/helpers/utils/attributes.spec.ts @@ -0,0 +1,163 @@ +import { AttributeParser, AttributeDefinition, AttributeValueType } from "@templates/helpers/utils/attributes"; + +describe("Attribute parser", () => { + test("should be able to set default values", () => { + const schema: AttributeDefinition[] = [ + { + name: "v1", + valueType: AttributeValueType.String, + defaultValue: "s1" + }, + { + name: "v2", + valueType: AttributeValueType.String, + defaultValue: "s2" + }, + { + name: "v3", + valueType: AttributeValueType.Number, + defaultValue: 3 + }, + { + name: "v4", + valueType: AttributeValueType.Boolean, + defaultValue: false + } + ]; + + const attrs = new AttributeParser(schema).parse({ + v5: "test1", + v2: "test2" + }); + + expect(attrs.v1).toEqual("s1"); + expect(attrs.v2).toEqual("test2"); + expect(attrs.v3).toEqual(3); + expect(attrs.v4).toEqual(false); + }); + + test("should be able to parse string values", () => { + const schema: AttributeDefinition[] = [ + { + name: "v1", + valueType: AttributeValueType.String, + defaultValue: "s1" + }, + { + name: "v2", + valueType: AttributeValueType.String, + defaultValue: "s2" + }, + { + name: "v3", + valueType: AttributeValueType.String, + defaultValue: "s3" + }, + { + name: "v4", + valueType: AttributeValueType.String, + defaultValue: "s4" + } + ]; + + const attrs = new AttributeParser(schema).parse({ + v1: "test1", + v2: 123456789, + v3: false, + }); + + expect(attrs.v1).toEqual("test1"); + expect(attrs.v2).toEqual("123456789"); + expect(attrs.v3).toEqual("false"); + expect(attrs.v4).toEqual("s4"); + }); + + test("should be able to parse number values", () => { + const schema: AttributeDefinition[] = [ + { + name: "v1", + valueType: AttributeValueType.Number, + defaultValue: 1 + }, + { + name: "v2", + valueType: AttributeValueType.Number, + defaultValue: 2 + }, + { + name: "v3", + valueType: AttributeValueType.Number, + defaultValue: 3 + }, + ]; + + const attrs = new AttributeParser(schema).parse({ + v1: "123456789", + v2: 987654321, + }); + + expect(attrs.v1).toEqual(123456789); + expect(attrs.v2).toEqual(987654321); + expect(attrs.v3).toEqual(3); + }); + + test("should throw error when can't parse number values", () => { + const schema: AttributeDefinition[] = [ + { + name: "v1", + valueType: AttributeValueType.Number, + defaultValue: 1 + }, + ]; + + const parser = new AttributeParser(schema); + + expect(() => parser.parse({ v1: "abcd" })).toThrow(); + expect(() => parser.parse({ v1: false })).toThrow(); + expect(() => parser.parse({ v1: "123" })).not.toThrow(); + expect(() => parser.parse({ v1: 123 })).not.toThrow(); + }); + + test("should be able to parse boolean values", () => { + const schema: AttributeDefinition[] = [ + { + name: "v1", + valueType: AttributeValueType.Boolean, + defaultValue: true + }, + { + name: "v2", + valueType: AttributeValueType.Boolean, + defaultValue: true + }, + { + name: "v3", + valueType: AttributeValueType.Boolean, + defaultValue: false + }, + { + name: "v4", + valueType: AttributeValueType.Boolean, + defaultValue: false + }, + { + name: "v5", + valueType: AttributeValueType.Boolean, + defaultValue: false + }, + ]; + + const attrs = new AttributeParser(schema).parse({ + v1: "", + v2: 0, + v3: 5, + v4: false, + }); + + expect(attrs.v1).toEqual(false); + expect(attrs.v2).toEqual(false); + expect(attrs.v3).toEqual(true); + expect(attrs.v4).toEqual(false); + expect(attrs.v5).toEqual(false); + }); +}); diff --git a/tests/parser.spec.ts b/tests/parser.spec.ts index 0c7c7ac..c1f0cc6 100644 --- a/tests/parser.spec.ts +++ b/tests/parser.spec.ts @@ -478,4 +478,508 @@ describe("Template parser", () => { some_time: 17.25 `); }); + + // Math helper. + test("should support math helper", async () => { + const template = { + id: "note-id", + title: "Some Template", + body: dedent` + --- + num1: text + num2: number + + --- + + {{ math num1 "+" num2 }} + {{ math num2 "**" 2 }} + {{ math 2 "-" num1 }} + {{ math num1 "/" 2 }} + {{ math num2 "*" num1 }} + {{ math (math num1 "+" num2) "%" 3 }} + {{ math num2 "/" (math num1 "-" num1) }} + ` + }; + testVariableTypes({ + num1: TextCustomVariable, + num2: NumberCustomVariable, + }); + + handleVariableDialog("ok", { + num1: "11", + num2: "4" + }); + const parsedTemplate = await parser.parseTemplate(template); + expect(parsedTemplate.folder).toBeNull(); + expect(parsedTemplate.tags.length).toEqual(0); + expect(parsedTemplate.title).toEqual("Some Template"); + expect(parsedTemplate.body).toEqual(dedent` + 15 + 16 + -9 + 5.5 + 44 + 0 + Infinity + `); + }); + + test("should show error with invalid usage of math helpers", async () => { + const invalidTemplates = []; + invalidTemplates.push(dedent` + --- + num1: text + + --- + + {{ math num1 "+" num1 }} + `); + + invalidTemplates.push(dedent` + --- + num1: boolean + + --- + + {{ math num1 "+" num1 }} + `); + + invalidTemplates.push(dedent` + {{ math 2 "%" 0 }} + `); + testVariableTypes({ + num1: CustomVariable, + }); + + handleVariableDialog("ok", { + num1: "true", + }); + + let errorMessagesShown = 0; + jest.spyOn(joplin.views.dialogs, "showMessageBox").mockImplementation(async () => { + errorMessagesShown++; + return 0; + }); + + for (const body of invalidTemplates) { + await parser.parseTemplate({ + id: "some-id", + title: "some template", + body, + }); + } + + expect(errorMessagesShown).toEqual(invalidTemplates.length); + }); + + // Repeat helper. + test("should support repeat helper", async () => { + const template = { + id: "note-id", + title: "Some Template", + body: dedent` + --- + num1: number + var1: text + + --- + + {{#repeat num1 }} + {{ var1 }} + + {{#if (compare repeat_index "==" 0)}} + Test + {{else}} + {{#repeat 2}} + Hi {{ repeat_index }} + {{/repeat}} + {{/if}} + + {{/repeat}}eof + ` + }; + testVariableTypes({ + num1: NumberCustomVariable, + var1: TextCustomVariable, + }); + + handleVariableDialog("ok", { + num1: "3", + var1: "v" + }); + const parsedTemplate = await parser.parseTemplate(template); + expect(parsedTemplate.folder).toBeNull(); + expect(parsedTemplate.tags.length).toEqual(0); + expect(parsedTemplate.title).toEqual("Some Template"); + expect(parsedTemplate.body).toEqual(dedent` + v + + Test + + v + + Hi 0 + Hi 1 + + v + + Hi 0 + Hi 1 + + eof + `); + }); + + test("should show error with invalid usage of repeat helper", async () => { + const invalidTemplates = []; + invalidTemplates.push(dedent` + --- + var1: text + + --- + + {{#repeat var1 }} + Hi + {{/repeat}} + `); + + invalidTemplates.push(dedent` + --- + var1: text + + --- + + {{#repeat (compare var1 "==" var1) }} + Hi + {{/repeat}} + `); + testVariableTypes({ + var1: TextCustomVariable, + }); + + handleVariableDialog("ok", { + var1: "abc", + }); + + let errorMessagesShown = 0; + jest.spyOn(joplin.views.dialogs, "showMessageBox").mockImplementation(async () => { + errorMessagesShown++; + return 0; + }); + + for (const body of invalidTemplates) { + await parser.parseTemplate({ + id: "some-id", + title: "some template", + body, + }); + } + + expect(errorMessagesShown).toEqual(invalidTemplates.length); + }); + + // Case helper. + test("should support case helper", async () => { + const template = { + id: "note-id", + title: "Some Template", + body: dedent` + --- + var1: text + + --- + + {{ case "upper" var1 }} + {{ case "lower" var1 }} + {{ case "upper" (condition false "!") }} + ` + }; + testVariableTypes({ + var1: TextCustomVariable, + }); + + handleVariableDialog("ok", { + var1: "Variable" + }); + const parsedTemplate = await parser.parseTemplate(template); + expect(parsedTemplate.folder).toBeNull(); + expect(parsedTemplate.tags.length).toEqual(0); + expect(parsedTemplate.title).toEqual("Some Template"); + expect(parsedTemplate.body).toEqual(dedent` + VARIABLE + variable + TRUE + `); + }); + + test("should show error with invalid usage of case helper", async () => { + const invalidTemplates = []; + invalidTemplates.push(dedent` + --- + var1: text + + --- + + {{ case "random" var1 }} + `); + + testVariableTypes({ + var1: TextCustomVariable, + }); + + handleVariableDialog("ok", { + var1: "abc", + }); + + let errorMessagesShown = 0; + jest.spyOn(joplin.views.dialogs, "showMessageBox").mockImplementation(async () => { + errorMessagesShown++; + return 0; + }); + + for (const body of invalidTemplates) { + await parser.parseTemplate({ + id: "some-id", + title: "some template", + body, + }); + } + + expect(errorMessagesShown).toEqual(invalidTemplates.length); + }); + + // Compare helper. + test("should support compare helper", async () => { + const template = { + id: "note-id", + title: "Some Template", + body: dedent` + --- + var1: text + var2: number + var3: number + var4: boolean + + --- + + {{ compare "val1" "===" var1 }} + {{ compare 0 "==" "" }} + {{ compare "val1" "!=" var1 }} + {{ compare 0 "!==" "" }} + {{ compare var2 ">" var3 }} + {{ compare (math var2 "*" 2) ">" var3 }} + {{ compare (math var2 "*" 2) ">=" var3 }} + {{ compare var2 "<" var3 }} + {{ compare var2 "<=" (math var3 "/" 2) }} + {{ compare (compare var4 "!=" true) "==" var4 }} + ` + }; + testVariableTypes({ + var1: TextCustomVariable, + var2: NumberCustomVariable, + var3: NumberCustomVariable, + var4: BooleanCustomVariable, + }); + + handleVariableDialog("ok", { + var1: "val1", + var2: "20", + var3: "40", + var4: "false" + }); + const parsedTemplate = await parser.parseTemplate(template); + expect(parsedTemplate.folder).toBeNull(); + expect(parsedTemplate.tags.length).toEqual(0); + expect(parsedTemplate.title).toEqual("Some Template"); + expect(parsedTemplate.body).toEqual(dedent` + true + true + false + true + false + false + true + true + true + false + `); + }); + + test("should show error with invalid usage of compare helper", async () => { + const invalidTemplates = []; + invalidTemplates.push(dedent` + --- + var1: text + + --- + + {{ compare var1 "random" 0 }} + `); + + testVariableTypes({ + var1: TextCustomVariable, + }); + + handleVariableDialog("ok", { + var1: "abc", + }); + + let errorMessagesShown = 0; + jest.spyOn(joplin.views.dialogs, "showMessageBox").mockImplementation(async () => { + errorMessagesShown++; + return 0; + }); + + for (const body of invalidTemplates) { + await parser.parseTemplate({ + id: "some-id", + title: "some template", + body, + }); + } + + expect(errorMessagesShown).toEqual(invalidTemplates.length); + }); + + // Condition helper. + test("should support condition helper", async () => { + const template = { + id: "note-id", + title: "Some Template", + body: dedent` + --- + var1: number + var2: number + var3: boolean + + --- + + {{ condition true "&&" false }} + {{ condition true "&&" true }} + {{ condition true "||" false }} + {{ condition false "!" }} + {{ condition true "&&" (compare var3 "==" true) }} + {{ condition true "&&" (condition (compare var1 "!=" var2) "!") }} + ` + }; + testVariableTypes({ + var1: NumberCustomVariable, + var2: NumberCustomVariable, + var3: BooleanCustomVariable, + }); + + handleVariableDialog("ok", { + var1: "20", + var2: "40", + var3: "true", + }); + const parsedTemplate = await parser.parseTemplate(template); + expect(parsedTemplate.folder).toBeNull(); + expect(parsedTemplate.tags.length).toEqual(0); + expect(parsedTemplate.title).toEqual("Some Template"); + expect(parsedTemplate.body).toEqual(dedent` + false + true + true + true + true + false + `); + }); + + test("should show error with invalid usage of condition helper", async () => { + const invalidTemplates = []; + invalidTemplates.push(dedent` + {{ condition false "~~" }} + `); + + let errorMessagesShown = 0; + jest.spyOn(joplin.views.dialogs, "showMessageBox").mockImplementation(async () => { + errorMessagesShown++; + return 0; + }); + + for (const body of invalidTemplates) { + await parser.parseTemplate({ + id: "some-id", + title: "some template", + body, + }); + } + + expect(errorMessagesShown).toEqual(invalidTemplates.length); + }); + + // Datetime helper. + test("should support datetime helper", async () => { + const template = { + id: "note-id", + title: "Some Template", + body: dedent` + --- + var1: number + + --- + + {{ datetime }} + {{ datetime format="[]YYYY[-]MM[-]DD[ ]HH[:]mm[:]ss" }} + {{ datetime delta_years="2" delta_months=-1 }} + {{ datetime delta_days=(math -1 "*" var1) }} + {{ datetime delta_days=1 delta_hours=(math -24 "*" var1) delta_minutes="56" }} + {{ datetime delta_seconds=8 format="HH:mm:ss" }} + {{ datetime set_date=bowm delta_days="1" format="DD/MM/YYYY, dddd" }} + {{ datetime set_time="23:33" delta_minutes="-33" format="HH:mm:ss" }} + ` + }; + testVariableTypes({ + var1: NumberCustomVariable, + }); + + handleVariableDialog("ok", { + var1: "20", + }); + const parsedTemplate = await parser.parseTemplate(template); + expect(parsedTemplate.folder).toBeNull(); + expect(parsedTemplate.tags.length).toEqual(0); + expect(parsedTemplate.title).toEqual("Some Template"); + expect(parsedTemplate.body).toEqual(dedent` + 12/08/2021 17:04 + 2021-08-12 17:04:54 + 12/07/2023 17:04 + 23/07/2021 17:04 + 24/07/2021 18:00 + 17:05:02 + 10/08/2021, Tuesday + 23:00:00 + `); + }); + + test("should show error with invalid usage of datetime helper", async () => { + const invalidTemplates = []; + invalidTemplates.push(dedent` + {{ datetime delta_hours="abc" }} + `); + invalidTemplates.push(dedent` + {{ datetime set_time="23:62" delta_minutes="-33" format="HH:mm:ss" }} + `); + invalidTemplates.push(dedent` + {{ datetime set_date="23:33" }} + `); + + let errorMessagesShown = 0; + jest.spyOn(joplin.views.dialogs, "showMessageBox").mockImplementation(async () => { + errorMessagesShown++; + return 0; + }); + + for (const body of invalidTemplates) { + console.log(await parser.parseTemplate({ + id: "some-id", + title: "some template", + body, + })); + } + + expect(errorMessagesShown).toEqual(invalidTemplates.length); + }); }); diff --git a/tests/utils/dateAndTime.spec.ts b/tests/utils/dateAndTime.spec.ts index 3ea4749..58e91b9 100644 --- a/tests/utils/dateAndTime.spec.ts +++ b/tests/utils/dateAndTime.spec.ts @@ -39,4 +39,29 @@ describe("Date and time utils", () => { expect(bows).toEqual("08/08/2021"); expect(bowm).toEqual("09/08/2021"); }); + + test("should correctly parse date", () => { + const utils = new DateAndTimeUtils(userLocale, dateFormat, timeFormat); + const parsedDate = utils.parseDate("2023/19/12", "YYYY/DD/MM"); + expect(parsedDate.date).toEqual(19); + expect(parsedDate.month).toEqual(11); + expect(parsedDate.year).toEqual(2023); + }); + + test("should throw error if couldn't parse date", () => { + const utils = new DateAndTimeUtils(userLocale, dateFormat, timeFormat); + expect(() => utils.parseDate("abc", "YYYY/DD/MM")).toThrow(); + }); + + test("should correctly parse time", () => { + const utils = new DateAndTimeUtils(userLocale, dateFormat, timeFormat); + const parsedTime = utils.parseTime("04:45 pm", "HH:mm a"); + expect(parsedTime.hours).toEqual(16); + expect(parsedTime.minutes).toEqual(45); + }); + + test("should throw error if couldn't parse time", () => { + const utils = new DateAndTimeUtils(userLocale, dateFormat, timeFormat); + expect(() => utils.parseTime("04:61 pm", "HH:mm a")).toThrow(); + }); });