Skip to content

Commit

Permalink
refactor: Allow default template in template engines.
Browse files Browse the repository at this point in the history
  • Loading branch information
RubenVerborgh committed Jul 19, 2021
1 parent 79db9a7 commit b610b3b
Show file tree
Hide file tree
Showing 15 changed files with 233 additions and 156 deletions.
15 changes: 10 additions & 5 deletions config/util/representation-conversion/default.json
Expand Up @@ -27,16 +27,21 @@
{ "@type": "ErrorToQuadConverter" },
{
"@type": "ErrorToTemplateConverter",
"engine": { "@type": "HandlebarsTemplateEngine" },
"templatePath": "$PACKAGE_ROOT/templates/error/main.md",
"descriptions": "$PACKAGE_ROOT/templates/error/descriptions/",
"engine": {
"@type": "HandlebarsTemplateEngine",
"defaultTemplate_templateFile": "$PACKAGE_ROOT/templates/error/main.md"
},
"templatePath": "$PACKAGE_ROOT/templates/error/descriptions/",
"contentType": "text/markdown",
"extension": ".md"
},
{
"@type": "MarkdownToHtmlConverter",
"engine": { "@type": "HandlebarsTemplateEngine" },
"templatePath": "$PACKAGE_ROOT/templates/main.html"
"engine": {
"@id": "urn:solid-server:default:MainTemplateEngine",
"@type": "HandlebarsTemplateEngine",
"defaultTemplate_templateFile": "$PACKAGE_ROOT/templates/main.html"
}
}
]
}
Expand Down
6 changes: 3 additions & 3 deletions src/index.ts
Expand Up @@ -174,14 +174,12 @@ export * from './pods/generate/variables/VariableSetter';
export * from './pods/generate/BaseComponentsJsFactory';
export * from './pods/generate/ComponentsJsFactory';
export * from './pods/generate/GenerateUtil';
export * from './pods/generate/HandlebarsTemplateEngine';
export * from './pods/generate/IdentifierGenerator';
export * from './pods/generate/PodGenerator';
export * from './pods/generate/ResourcesGenerator';
export * from './pods/generate/SubdomainIdentifierGenerator';
export * from './pods/generate/SuffixIdentifierGenerator';
export * from './pods/generate/TemplatedPodGenerator';
export * from './pods/generate/TemplateEngine';
export * from './pods/generate/TemplatedResourcesGenerator';

// Pods/Settings
Expand Down Expand Up @@ -223,14 +221,16 @@ export * from './storage/conversion/ChainedConverter';
export * from './storage/conversion/ConstantConverter';
export * from './storage/conversion/ContentTypeReplacer';
export * from './storage/conversion/ConversionUtil';
export * from './storage/conversion/ErrorToTemplateConverter';
export * from './storage/conversion/ErrorToQuadConverter';
export * from './storage/conversion/ErrorToTemplateConverter';
export * from './storage/conversion/HandlebarsTemplateEngine';
export * from './storage/conversion/IfNeededConverter';
export * from './storage/conversion/MarkdownToHtmlConverter';
export * from './storage/conversion/PassthroughConverter';
export * from './storage/conversion/QuadToRdfConverter';
export * from './storage/conversion/RdfToQuadConverter';
export * from './storage/conversion/RepresentationConverter';
export * from './storage/conversion/TemplateEngine';
export * from './storage/conversion/TypedRepresentationConverter';

// Storage/KeyValue
Expand Down
12 changes: 0 additions & 12 deletions src/pods/generate/HandlebarsTemplateEngine.ts

This file was deleted.

8 changes: 0 additions & 8 deletions src/pods/generate/TemplateEngine.ts

This file was deleted.

19 changes: 9 additions & 10 deletions src/pods/generate/TemplatedResourcesGenerator.ts
Expand Up @@ -4,6 +4,7 @@ import { Parser } from 'n3';
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
import { RepresentationMetadata } from '../../ldp/representation/RepresentationMetadata';
import type { ResourceIdentifier } from '../../ldp/representation/ResourceIdentifier';
import type { TemplateEngine } from '../../storage/conversion/TemplateEngine';
import type {
FileIdentifierMapper,
FileIdentifierMapperFactory,
Expand All @@ -14,7 +15,6 @@ import type { Guarded } from '../../util/GuardedStream';
import { joinFilePath, isContainerIdentifier, resolveAssetPath } from '../../util/PathUtil';
import { guardedStreamFrom, readableToString } from '../../util/StreamUtil';
import type { Resource, ResourcesGenerator } from './ResourcesGenerator';
import type { TemplateEngine } from './TemplateEngine';
import Dict = NodeJS.Dict;

interface TemplateResourceLink extends ResourceLink {
Expand Down Expand Up @@ -58,13 +58,13 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
public async* generate(location: ResourceIdentifier, options: Dict<string>): AsyncIterable<Resource> {
const mapper = await this.factory.create(location.path, this.templateFolder);
const folderLink = await this.toTemplateLink(this.templateFolder, mapper);
yield* this.parseFolder(folderLink, mapper, options);
yield* this.processFolder(folderLink, mapper, options);
}

/**
* Generates results for all entries in the given folder, including the folder itself.
*/
private async* parseFolder(folderLink: TemplateResourceLink, mapper: FileIdentifierMapper, options: Dict<string>):
private async* processFolder(folderLink: TemplateResourceLink, mapper: FileIdentifierMapper, options: Dict<string>):
AsyncIterable<Resource> {
// Group resource links with their corresponding metadata links
const links = await this.groupLinks(folderLink.filePath, mapper);
Expand All @@ -78,7 +78,7 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {

for (const { link, meta } of Object.values(links)) {
if (isContainerIdentifier(link.identifier)) {
yield* this.parseFolder(link, mapper, options);
yield* this.processFolder(link, mapper, options);
} else {
yield this.generateResource(link, options, meta);
}
Expand Down Expand Up @@ -139,7 +139,7 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {

// Read file if it is not a container
if (!isContainerIdentifier(link.identifier)) {
data = await this.parseFile(link, options);
data = await this.processFile(link, options);
metadata.contentType = link.contentType;
}

Expand All @@ -163,7 +163,7 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
const identifier = this.metaToResource(metaLink.identifier);
const metadata = new RepresentationMetadata(identifier);

const data = await this.parseFile(metaLink, options);
const data = await this.processFile(metaLink, options);
const parser = new Parser({ format: metaLink.contentType, baseIRI: identifier.path });
const quads = parser.parse(await readableToString(data));
metadata.addQuads(quads);
Expand All @@ -174,11 +174,10 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
/**
* Creates a read stream from the file and applies the template if necessary.
*/
private async parseFile(link: TemplateResourceLink, options: Dict<string>): Promise<Guarded<Readable>> {
private async processFile(link: TemplateResourceLink, options: Dict<string>): Promise<Guarded<Readable>> {
if (link.isTemplate) {
const raw = await fsPromises.readFile(link.filePath, 'utf8');
const result = this.engine.apply(raw, options);
return guardedStreamFrom(result);
const rendered = await this.engine.render(options, { templateFile: link.filePath });
return guardedStreamFrom(rendered);
}
return guardStream(createReadStream(link.filePath));
}
Expand Down
43 changes: 17 additions & 26 deletions src/storage/conversion/ErrorToTemplateConverter.ts
@@ -1,14 +1,12 @@
import assert from 'assert';
import { promises as fsPromises } from 'fs';
import arrayifyStream from 'arrayify-stream';
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
import type { Representation } from '../../ldp/representation/Representation';
import type { TemplateEngine } from '../../pods/generate/TemplateEngine';
import { INTERNAL_ERROR } from '../../util/ContentTypes';
import { HttpError } from '../../util/errors/HttpError';
import { InternalServerError } from '../../util/errors/InternalServerError';
import { joinFilePath, resolveAssetPath } from '../../util/PathUtil';
import type { RepresentationConverterArgs } from './RepresentationConverter';
import type { TemplateEngine } from './TemplateEngine';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';

/**
Expand All @@ -25,16 +23,14 @@ import { TypedRepresentationConverter } from './TypedRepresentationConverter';
export class ErrorToTemplateConverter extends TypedRepresentationConverter {
private readonly engine: TemplateEngine;
private readonly templatePath: string;
private readonly descriptions: string;
private readonly contentType: string;
private readonly extension: string;

public constructor(engine: TemplateEngine, templatePath: string, descriptions: string, contentType: string,
public constructor(engine: TemplateEngine, templatePath: string, contentType: string,
extension: string) {
super(INTERNAL_ERROR, contentType);
this.engine = engine;
this.templatePath = resolveAssetPath(templatePath);
this.descriptions = resolveAssetPath(descriptions);
this.templatePath = templatePath;
this.contentType = contentType;
this.extension = extension;
}
Expand All @@ -46,29 +42,24 @@ export class ErrorToTemplateConverter extends TypedRepresentationConverter {
}
const error = errors[0] as Error;

// Render the template
const { name, message, stack } = error;
const description = await this.getErrorCodeMessage(error);
const variables = { name, message, stack, description };
const template = await fsPromises.readFile(this.templatePath, 'utf8');
const rendered = this.engine.apply(template, variables);

return new BasicRepresentation(rendered, representation.metadata, this.contentType);
}

private async getErrorCodeMessage(error: Error): Promise<string | undefined> {
// Render the error description using an error-specific template
let description: string | undefined;
if (HttpError.isInstance(error)) {
let template: string;
try {
const fileName = `${error.errorCode}${this.extension}`;
assert(/^[\w.-]+$/u.test(fileName), 'Invalid error template name');
template = await fsPromises.readFile(joinFilePath(this.descriptions, fileName), 'utf8');
const templateFile = `${error.errorCode}${this.extension}`;
assert(/^[\w.-]+$/u.test(templateFile), 'Invalid error template name');
description = await this.engine.render((error.details ?? {}) as NodeJS.Dict<string>,
{ templateFile, templatePath: this.templatePath });
} catch {
// In case no template is found we still want to convert
return;
// In case no template is found, or rendering errors, we still want to convert
}

return this.engine.apply(template, (error.details ?? {}) as NodeJS.Dict<string>);
}

// Render the main template, embedding the rendered error description
const { name, message, stack } = error;
const variables = { name, message, stack, description };
const rendered = await this.engine.render(variables);

return new BasicRepresentation(rendered, representation.metadata, this.contentType);
}
}
21 changes: 21 additions & 0 deletions src/storage/conversion/HandlebarsTemplateEngine.ts
@@ -0,0 +1,21 @@
import type { TemplateDelegate } from 'handlebars';
import { compile } from 'handlebars';
import type { TemplateEngine, TemplateOptions } from './TemplateEngine';
import { readTemplate } from './TemplateEngine';

/**
* Fills in Handlebars templates.
*/
export class HandlebarsTemplateEngine implements TemplateEngine {
private readonly applyTemplate: Promise<TemplateDelegate>;

public constructor(defaultTemplate?: TemplateOptions) {
this.applyTemplate = readTemplate(defaultTemplate)
.then((template: string): TemplateDelegate => compile(template));
}

public async render(contents: NodeJS.Dict<string>, template?: TemplateOptions): Promise<string> {
const applyTemplate = template ? compile(await readTemplate(template)) : await this.applyTemplate;
return applyTemplate(contents);
}
}
25 changes: 9 additions & 16 deletions src/storage/conversion/MarkdownToHtmlConverter.ts
@@ -1,41 +1,34 @@
import { promises as fsPromises } from 'fs';
import marked from 'marked';
import { BasicRepresentation } from '../../ldp/representation/BasicRepresentation';
import type { Representation } from '../../ldp/representation/Representation';
import type { TemplateEngine } from '../../pods/generate/TemplateEngine';
import { TEXT_HTML, TEXT_MARKDOWN } from '../../util/ContentTypes';
import { resolveAssetPath } from '../../util/PathUtil';
import { readableToString } from '../../util/StreamUtil';
import type { RepresentationConverterArgs } from './RepresentationConverter';
import type { TemplateEngine } from './TemplateEngine';
import { TypedRepresentationConverter } from './TypedRepresentationConverter';

/**
* Converts markdown data to HTML.
* Converts Markdown data to HTML.
* The generated HTML will be injected into the given template using the parameter `htmlBody`.
* A standard markdown string will be converted to a <p> tag, so html and body tags should be part of the template.
* In case the markdown body starts with a header (#), that value will also be used as `title` parameter.
* A standard Markdown string will be converted to a <p> tag, so html and body tags should be part of the template.
* In case the Markdown body starts with a header (#), that value will also be used as `title` parameter.
*/
export class MarkdownToHtmlConverter extends TypedRepresentationConverter {
private readonly engine: TemplateEngine;
private readonly templatePath: string;

public constructor(engine: TemplateEngine, templatePath: string) {
public constructor(engine: TemplateEngine) {
super(TEXT_MARKDOWN, TEXT_HTML);
this.engine = engine;
this.templatePath = resolveAssetPath(templatePath);
}

public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {
const markdown = await readableToString(representation.data);
// Try to extract the main title for use in the <title> tag
const title = /^#+\s*([^\n]+)\n/u.exec(markdown)?.[1];

// See if there is a title we can use
const match = /^\s*#+\s*([^\n]+)\n/u.exec(markdown);
const title = match?.[1];

// Place the rendered Markdown into the HTML template
const htmlBody = marked(markdown);

const template = await fsPromises.readFile(this.templatePath, 'utf8');
const html = this.engine.apply(template, { htmlBody, title });
const html = await this.engine.render({ htmlBody, title });

return new BasicRepresentation(html, representation.metadata, TEXT_HTML);
}
Expand Down
44 changes: 44 additions & 0 deletions src/storage/conversion/TemplateEngine.ts
@@ -0,0 +1,44 @@
import { promises as fsPromises } from 'fs';
import { joinFilePath, resolveAssetPath } from '../../util/PathUtil';
import Dict = NodeJS.Dict;

export interface TemplateOptions {
// String contents of the template
template?: string;
// Name of the template file
templateFile?: string;
// Path of the template file
templatePath?: string;
}

/**
* A template engine renders content into a template.
*/
export interface TemplateEngine {
/**
* Renders the given contents into the template.
*
* @param contents - The contents to render.
* @param template - The template string to use for rendering;
* if omitted, a default template is used.
* @returns The rendered contents.
*/
render: (contents: Dict<string>, template?: TemplateOptions) => Promise<string>;
}

/**
* Reads the template and returns it as a string.
*/
export async function readTemplate({ template, templateFile, templatePath }: TemplateOptions = {}): Promise<string> {
// The template has already been given as a string
if (typeof template === 'string') {
return template;
}
// The template needs to be read from disk
if (typeof templateFile === 'string') {
const fullTemplatePath = templatePath ? joinFilePath(templatePath, templateFile) : templateFile;
return fsPromises.readFile(resolveAssetPath(fullTemplatePath), 'utf8');
}
// No template specified
return '';
}
11 changes: 0 additions & 11 deletions test/unit/pods/generate/HandlebarsTemplateEngine.test.ts

This file was deleted.

@@ -1,6 +1,6 @@
import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
import { HandlebarsTemplateEngine } from '../../../../src/pods/generate/HandlebarsTemplateEngine';
import { TemplatedResourcesGenerator } from '../../../../src/pods/generate/TemplatedResourcesGenerator';
import { HandlebarsTemplateEngine } from '../../../../src/storage/conversion/HandlebarsTemplateEngine';
import type {
FileIdentifierMapper,
FileIdentifierMapperFactory,
Expand Down

0 comments on commit b610b3b

Please sign in to comment.