Templating functionality has changed considerably from LiTScript version 1.x.
Starting from version 2.0, templates are no longer external
projects/libraries. Templates reside now under the <baseDir>/site/pages
directory. Any LiTScript project can define its own templates or just use the
built-in ones.
A template does not need implement any interfaces. It is simply a function that returns the HTML for a given input page.
import * as path from 'path'
import * as fs from 'fs'
import * as toc from './toc'
import * as fm from './front-matter'
import * as utils from '../utils'
import { HtmlTemplate, saveHtmlTemplate } from './html';
A template function is passed a context object that contains data needed for generating HTML and methods to add new files for the bundler.
export class TemplateContext {
All the modules referenced by calling require
method are stored in
this property.
modules: string[] = []
The constructor takes all the required data as a parameter and stores them in read-only properties.
constructor(
Front matter that comes from the configuration or from the source file.
readonly frontMatter: fm.FrontMatter,
Table of contents data structure generated by the weaver.
readonly toc: toc.Toc,
The actual documentation contents of the file in HTML format.
readonly contents: string,
The relative path of the output file. The path is relative to
outDir
.
readonly relFilePath: string,
The fully resolved path of the output file.
readonly fullFilePath: string,
The site directory of the project where templates reside.
readonly siteSrcDir: string,
The site directory where the compiled templates reside.
readonly siteOutDir: string,
The output directory for HTML files.
readonly outDir: string,
Style <link>
tags to be added to the head section.
readonly styles: string,
<script>
tags to be added at the end of the body.
readonly scripts: string) { }
Modules and style files required by the template can be imported with the
require
method. It adds the referred TS, JS, or CSS file into the
modules
array, if it's not already there.
The method takes directory and module path as parameter and resolves them to an absolute path. Reason why the path is given in two parts is to make referring to modules in different source directories more convenient.
We also check that the given path exists. This makes debugging templates
easier as missing modules are noticed before bundling phase. If the file
has no extension, .js
is appended to the path. As the final
transformation we convert the module path to be relative to the main
directory under the <siteDir>
. See below why we do that.
require(dir: string, modPath: string) {
let module = path.resolve(dir, modPath)
if (path.extname(module) == "")
module += ".js"
checkModuleExists(this)
let mainDir = path.resolve(this.siteOutDir, "main/")
let modpath = utils.toPosixPath(path.relative(mainDir, module))
if (!this.modules.includes(modpath))
this.modules.push(modpath)
Checking that the module exists is a bit involved operation because
TypeScript compiler does not copy CSS files to the output directory.
This makes referring to them from templates tricky. We would like to
specify a relative path starting from the module's location directory
available in the __dirname
variable. But when the compiled JS code
evaluates this variable it gets the compiler's ouput directory, not
the template source directory.
We solve the problem like this: If the module is not found under the
given path, we check whether it resides under the siteOutDir
. If
so, we replace the siteOutDir
with siteSrcDir
, which is the
corresponding source directory and check again. If still cannot find
the module, we throw an exception.
function checkModuleExists(ctx: TemplateContext) {
if (fs.existsSync(module))
return
else if (utils.isInsideDir(module, ctx.siteOutDir)) {
let relPath = path.relative(ctx.siteOutDir, module)
module = path.resolve(ctx.siteSrcDir, relPath)
checkModuleExists(ctx)
}
else
throw new Error(`Cannot find module "${module}"`)
}
}
}
A template is a function that gets a TemplateContext object as an argument and returns a HTML template literal. The HtmlTemplate object can be then efficiently outputted to a file.
export type Template = (ctx: TemplateContext) => HtmlTemplate
Page templates live under the <baseDir>/site
directory. They are loaded
dynamically when a page is generated. To prevent unnecessary reloads, we
cache the loedded templates in a dictionary. Its keys are template names and
values are template functions.
let templates: Record<string, Template> = {}
Before we can generate pages with a template, we must clean up the working
directory at <siteDir>/main
. It contains the bundler entry/root files that
import the required modules.
export function initialize(siteDir: string) {
let mainDir = path.resolve(siteDir, "main/")
if (fs.existsSync(mainDir))
utils.clearDir(mainDir)
}
When template source code changes, we need to recompile and reload them. The function below clears the template cache as well as node.js module cache.
export function clearCache(siteDir: string) {
templates = {}
for (let mod in require.cache)
if (utils.isInsideDir(mod, siteDir))
delete require.cache[mod]
}
generate
function takes as an argument all the needed properties and:
export function generate(fm: fm.FrontMatter, toc: toc.Toc, contents: string,
styles: string, scripts: string, fullFilePath: string, relFilePath: string,
siteSrcDir: string, siteOutDir: string, outDir: string): [string, string] {
let ctx = new TemplateContext(fm, toc, contents, relFilePath, fullFilePath,
siteSrcDir, siteOutDir, outDir, styles, scripts)
let template = loadTemplate(siteOutDir, fm.pageTemplate)
let htmlTemp = template(ctx)
saveHtmlTemplate(htmlTemp, fullFilePath);
return [fm.pageTemplate, saveMain(siteOutDir, fm.pageTemplate, ctx)]
}
The module that contains the named <template>
must be located at
<siteDir>/pages/<template>.js
or <siteDir>/pages/<template>/index.js
.
The template function must be the default export of that module. We use
node.js require
function to load the module and get its detault export. If
that succeeds, we store the template in the cache dictionary.
function loadTemplate(siteDir: string, tempName: string): Template {
let temp = templates[tempName]
if (!temp) {
let tempFile = path.resolve(siteDir, "pages/", tempName)
temp = require(tempFile).default as Template
templates[tempName] = temp
}
return temp
}
The main module is used as the entry file for the bundler. It imports all
required modules which bundler subsequently combines into a single JS or CSS
file. The function below outputs the main module into
<siteDir>/main/<template>.js
and returns the path to it.
function saveMain(siteDir: string, tempName: string, ctx: TemplateContext):
string {
let jsPath = path.resolve(siteDir, `main/${tempName}.js`)
if (!fs.existsSync(jsPath)) {
utils.ensureDirExist(jsPath)
let fd = fs.openSync(jsPath, 'w')
try {
fs.writeSync(fd, '"use strict";\n' +
'Object.defineProperty(exports, "__esModule", { value: true });\n',
null, 'utf8')
for (let mod of ctx.modules)
fs.writeSync(fd, `require("${mod}");\n`, null, 'utf8')
}
finally {
fs.closeSync(fd)
}
}
return jsPath
}
The relLink
function returns a relative path from an URL to another.
export function relLink(from: string, to: string): string {
return to.match(/^https?:\/\//) ? to :
path.relative(path.dirname(from), to).replace(/\\/g, "/")
}