Skip to content

Commit

Permalink
Merge pull request #79 from joplin/logic-in-templates
Browse files Browse the repository at this point in the history
Allow users to have complex logic in templates
  • Loading branch information
nishantwrp committed Nov 9, 2023
2 parents 8b46afd + fd6f161 commit 0142970
Show file tree
Hide file tree
Showing 16 changed files with 1,078 additions and 4 deletions.
16 changes: 16 additions & 0 deletions src/helpers/case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { HandlebarsHelper, HelperConstructorBlock } from "./helper";

export const caseHelper: HelperConstructorBlock = ctx => {

Check warning on line 3 in src/helpers/case.ts

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

'ctx' is defined but never used
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}`);
}
});
};
42 changes: 42 additions & 0 deletions src/helpers/compare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { HandlebarsHelper, HelperConstructorBlock } from "./helper";

export const compareHelper: HelperConstructorBlock = ctx => {

Check warning on line 3 in src/helpers/compare.ts

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

'ctx' is defined but never used
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}`);
}
});
};
19 changes: 19 additions & 0 deletions src/helpers/condition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { HandlebarsHelper, HelperConstructorBlock } from "./helper";

export const conditionHelper: HelperConstructorBlock = ctx => {

Check warning on line 3 in src/helpers/condition.ts

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

'ctx' is defined but never used
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}`);
}
});
};
9 changes: 9 additions & 0 deletions src/helpers/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { DateAndTimeUtils } from "../utils/dateAndTime";

export class HelperContext {
public dateAndTimeUtils: DateAndTimeUtils;

constructor(dateUtils: DateAndTimeUtils) {
this.dateAndTimeUtils = dateUtils;
}
}
7 changes: 7 additions & 0 deletions src/helpers/custom_datetime.ts
Original file line number Diff line number Diff line change
@@ -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));
});
};
94 changes: 94 additions & 0 deletions src/helpers/datetime.ts
Original file line number Diff line number Diff line change
@@ -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);
});
};
21 changes: 21 additions & 0 deletions src/helpers/helper.ts
Original file line number Diff line number Diff line change
@@ -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>) => 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);
}
}
30 changes: 30 additions & 0 deletions src/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
30 changes: 30 additions & 0 deletions src/helpers/math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { HandlebarsHelper, HelperConstructorBlock } from "./helper";

export const mathHelper: HelperConstructorBlock = ctx => {

Check warning on line 3 in src/helpers/math.ts

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

'ctx' is defined but never used
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}`);
}
});
};
17 changes: 17 additions & 0 deletions src/helpers/repeat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { HandlebarsHelper, HelperConstructorBlock } from "./helper";

export const repeatHelper: HelperConstructorBlock = (ctx) => {

Check warning on line 3 in src/helpers/repeat.ts

View workflow job for this annotation

GitHub Actions / lint (ubuntu-latest)

'ctx' is defined but never used
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;
});
};
57 changes: 57 additions & 0 deletions src/helpers/utils/attributes.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
7 changes: 3 additions & 4 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()),
Expand Down Expand Up @@ -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;

Expand Down
Loading

0 comments on commit 0142970

Please sign in to comment.