This is part two of a series of articles on Accord Project TemplateMark. In this article I will show how TemplateMark can be statically compiled to TypeScript. I recommend you read Getting Started with TemplateMark if you have not yet done so.

One of the principles of TemplateMark is that templates should be SAFE. That is, a wide variety of common templating errors should be detectable at authoring time, rather than having the template engine fail in unpredictable ways at runtime. This include simple typos within the template (referencing template model properties that do not exist), syntax errors within TypeScript expressions, and type-checking errors, such as attempting to call methods that do not exist on objects, or other type safety checks.

Run the code for this article online

https://replit.com/@dselman/AccordProjectTemplateEngine-Compiler

One of the ways that TemplateMark achieves this is by compiling TemplateMark to TypeScript, and then using the powerful type inference and type-checking built into the TypeScript compiler. For example, if the TemplateMark contains a TypeScript expression that references an optional template model property the compiler will generate an error to indicate that the property may be null, if it is not guarded by an explicit null check.

The TemplateMark to TypeScript compilation process

The code to compile a template is quite short. The code below dynamically defines the data model for a template and the extended markdown for a template and then compiles the template code to the ./generated directory on disk.

  /**
   * Define the data model for the template. The model must have a concept with
   * the @template decorator. The types of properties allow the template to be
   * type-checked.
  */
  const model = `namespace helloworld@1.0.0
  @template
  concept TemplateData {
    o String message
  }`;

  /**
   * Load the template, rich-text with variables, conditional sections etc
   */
  const template = 'Hello {{message}}! Your name is **{{% return message.length %}}** characters long.';

  const modelManager = new ModelManager({ strict: true });
  modelManager.addCTOModel(model);
  const compiler = new TemplateMarkToTypeScriptCompiler(modelManager, {});
  const factory = new Factory(compiler.getTemplateMarkModelManager());
  const serializer = new Serializer(factory, compiler.getTemplateMarkModelManager());

  const templateMarkTransformer = new TemplateMarkTransformer();

  const templateMarkJson = templateMarkTransformer.fromMarkdownTemplate({ content: template },  
     modelManager, 'contract', { verbose: false });
  console.log('=== TemplateMark DOM ===')
  console.log(JSON.stringify(templateMarkJson, null, 2));

  /**
   * Compile the template to TypeScript code
   */
  const templateMarkDom = serializer.fromJSON(templateMarkJson);
  compiler.compile(templateMarkDom, './generated');
  console.log('Generated code.');

Behind the scenes the TemplateMark compiler generates the code for the TemplateMark, creating a function that takes an instance of the template model as input, the current date and time and produces an AgreementMark output document.

TypeScript code generated from TemplateMark

The code generation proceeds through the following steps:

Step 1: The TemplateMark is validated with respect to the model. I.e. a check is perfomed that the {{message}} variable actually exists in the template model

Step 2: The template model is converted to a TypeScript model, so that the TypeScript code is compiled against TypeScript interfaces. This is fairly extensive as the template model has various dependencies and the models for TemplateMark and AgreementMark are also converted to TypeScript.

import {IConcept} from './concerto@1.0.0';

// interfaces
export interface ITemplateData extends IConcept {
   message: string;
}

Step 3: The TypeScript expressions in the TemplateMark are extracted into a generated file called usercode.ts

export function formula_<generated_name>(data:TemplateModel.ITemplateData, library:any, now:dayjs.Dayjs) : any {
   const message = data.message;
   return message.length
}

Step 4: A sample input JSON document is generated for the template model:

{
"$class": "helloworld@1.0.0.TemplateData",
"message": "Lorem anim."
}

Step 5: The root generator function is created that performs the core work of converting the TemplateMark plus user functions to AgreementMark.

export function generator(data:TemplateModel.ITemplateData, library:any, now:dayjs.Dayjs) : CommonMark.IDocument {
... lots of lines of code to produce an AgreementMark document from the ITemplateData, evaluating conditional expressions, calculations etc.
}

Step 6: A package.json is generated with the required dependencies so that the generated code can be run with npm install && npm start — running the generator function against sample.json.

I’d encourage you to experiment with the REPL above, introducing various errors into the template and inspecting the generated code. You should see that the repl.it web-editor detects the errors in the generated TypeScript code. If you open a shell you can run the generated code and inspect the output.

Running the generated code within repl.it and viewing the generated AgreementMark document

Have fun creating templates and don’t hesitate to ask questions on the Accord Project Discord channel, or to create GitHub issues.