mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
refactor: Allow default template in template engines.
This commit is contained in:
parent
1488c7e221
commit
19624dc729
@ -4,5 +4,6 @@
|
||||
"Error",
|
||||
"EventEmitter",
|
||||
"HttpErrorOptions",
|
||||
"Template",
|
||||
"ValuePreferencesArg"
|
||||
]
|
||||
|
@ -86,6 +86,10 @@ module.exports = {
|
||||
'unicorn/prefer-ternary': 'off',
|
||||
'yield-star-spacing': [ 'error', 'after' ],
|
||||
|
||||
// Need to use the typescript version of this rule to support overloading
|
||||
"no-dupe-class-members": "off",
|
||||
"@typescript-eslint/no-dupe-class-members": ["error"],
|
||||
|
||||
// Naming conventions
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
|
@ -9,7 +9,7 @@
|
||||
"factory": {
|
||||
"@type": "ExtensionBasedMapperFactory"
|
||||
},
|
||||
"engine": {
|
||||
"templateEngine": {
|
||||
"@type": "HandlebarsTemplateEngine"
|
||||
}
|
||||
}
|
||||
|
@ -26,17 +26,24 @@
|
||||
{ "@id": "urn:solid-server:default:QuadToRdfConverter" },
|
||||
{ "@type": "ErrorToQuadConverter" },
|
||||
{
|
||||
"comment": "Converts an error into a Markdown description of its details.",
|
||||
"@type": "ErrorToTemplateConverter",
|
||||
"engine": { "@type": "HandlebarsTemplateEngine" },
|
||||
"templatePath": "$PACKAGE_ROOT/templates/error/main.md",
|
||||
"descriptions": "$PACKAGE_ROOT/templates/error/descriptions/",
|
||||
"contentType": "text/markdown",
|
||||
"extension": ".md"
|
||||
"templateEngine": {
|
||||
"@type": "HandlebarsTemplateEngine",
|
||||
"template": { "templateFile": "$PACKAGE_ROOT/templates/error/main.md" }
|
||||
},
|
||||
"templatePath": "$PACKAGE_ROOT/templates/error/descriptions/",
|
||||
"extension": ".md",
|
||||
"contentType": "text/markdown"
|
||||
},
|
||||
{
|
||||
"comment": "Renders Markdown snippets into the main page template.",
|
||||
"@type": "MarkdownToHtmlConverter",
|
||||
"engine": { "@type": "HandlebarsTemplateEngine" },
|
||||
"templatePath": "$PACKAGE_ROOT/templates/main.html"
|
||||
"templateEngine": {
|
||||
"@id": "urn:solid-server:default:MainTemplateEngine",
|
||||
"@type": "HandlebarsTemplateEngine",
|
||||
"template": { "templateFile": "$PACKAGE_ROOT/templates/main.html" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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
|
||||
@ -223,8 +221,8 @@ 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/IfNeededConverter';
|
||||
export * from './storage/conversion/MarkdownToHtmlConverter';
|
||||
export * from './storage/conversion/PassthroughConverter';
|
||||
@ -311,6 +309,10 @@ export * from './util/locking/ResourceLocker';
|
||||
export * from './util/locking/SingleThreadedResourceLocker';
|
||||
export * from './util/locking/WrappedExpiringReadWriteLocker';
|
||||
|
||||
// Util/Templates
|
||||
export * from './util/templates/HandlebarsTemplateEngine';
|
||||
export * from './util/templates/TemplateEngine';
|
||||
|
||||
// Util
|
||||
export * from './util/ContentTypes';
|
||||
export * from './util/GuardedStream';
|
||||
|
@ -1,12 +0,0 @@
|
||||
import { compile } from 'handlebars';
|
||||
import type { TemplateEngine } from './TemplateEngine';
|
||||
|
||||
/**
|
||||
* Fills in Handlebars templates.
|
||||
*/
|
||||
export class HandlebarsTemplateEngine implements TemplateEngine {
|
||||
public apply(template: string, options: NodeJS.Dict<string>): string {
|
||||
const compiled = compile(template);
|
||||
return compiled(options);
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
/**
|
||||
* A template engine takes as input a template and applies the given options to it.
|
||||
*/
|
||||
export interface TemplateEngine {
|
||||
apply: (template: string, options: Dict<string>) => string;
|
||||
}
|
@ -13,8 +13,8 @@ import { guardStream } from '../../util/GuardedStream';
|
||||
import type { Guarded } from '../../util/GuardedStream';
|
||||
import { joinFilePath, isContainerIdentifier, resolveAssetPath } from '../../util/PathUtil';
|
||||
import { guardedStreamFrom, readableToString } from '../../util/StreamUtil';
|
||||
import type { TemplateEngine } from '../../util/templates/TemplateEngine';
|
||||
import type { Resource, ResourcesGenerator } from './ResourcesGenerator';
|
||||
import type { TemplateEngine } from './TemplateEngine';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
interface TemplateResourceLink extends ResourceLink {
|
||||
@ -33,7 +33,7 @@ interface TemplateResourceLink extends ResourceLink {
|
||||
export class TemplatedResourcesGenerator implements ResourcesGenerator {
|
||||
private readonly templateFolder: string;
|
||||
private readonly factory: FileIdentifierMapperFactory;
|
||||
private readonly engine: TemplateEngine;
|
||||
private readonly templateEngine: TemplateEngine;
|
||||
private readonly templateExtension: string;
|
||||
|
||||
/**
|
||||
@ -41,28 +41,28 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
|
||||
*
|
||||
* @param templateFolder - Folder where the templates are located.
|
||||
* @param factory - Factory used to generate mapper relative to the base identifier.
|
||||
* @param engine - Template engine for generating the resources.
|
||||
* @param templateEngine - Template engine for generating the resources.
|
||||
* @param templateExtension - The extension of files that need to be interpreted as templates.
|
||||
* Will be removed to generate the identifier.
|
||||
*/
|
||||
public constructor(templateFolder: string, factory: FileIdentifierMapperFactory, engine: TemplateEngine,
|
||||
public constructor(templateFolder: string, factory: FileIdentifierMapperFactory, templateEngine: TemplateEngine,
|
||||
templateExtension = '.hbs') {
|
||||
this.templateFolder = resolveAssetPath(templateFolder);
|
||||
this.factory = factory;
|
||||
this.engine = engine;
|
||||
this.templateEngine = templateEngine;
|
||||
this.templateExtension = templateExtension;
|
||||
}
|
||||
|
||||
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);
|
||||
@ -76,7 +76,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);
|
||||
}
|
||||
@ -131,7 +131,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;
|
||||
}
|
||||
|
||||
@ -154,7 +154,7 @@ export class TemplatedResourcesGenerator implements ResourcesGenerator {
|
||||
Promise<RepresentationMetadata> {
|
||||
const metadata = new RepresentationMetadata(metaLink.identifier);
|
||||
|
||||
const data = await this.parseFile(metaLink, options);
|
||||
const data = await this.processFile(metaLink, options);
|
||||
const parser = new Parser({ format: metaLink.contentType, baseIRI: metaLink.identifier.path });
|
||||
const quads = parser.parse(await readableToString(data));
|
||||
metadata.addQuads(quads);
|
||||
@ -165,11 +165,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.templateEngine.render(options, { templateFile: link.filePath });
|
||||
return guardedStreamFrom(rendered);
|
||||
}
|
||||
return guardStream(createReadStream(link.filePath));
|
||||
}
|
||||
|
@ -1,13 +1,11 @@
|
||||
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 { TemplateEngine } from '../../util/templates/TemplateEngine';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
import { TypedRepresentationConverter } from './TypedRepresentationConverter';
|
||||
|
||||
@ -23,52 +21,45 @@ import { TypedRepresentationConverter } from './TypedRepresentationConverter';
|
||||
* using the variable `codeMessage`.
|
||||
*/
|
||||
export class ErrorToTemplateConverter extends TypedRepresentationConverter {
|
||||
private readonly engine: TemplateEngine;
|
||||
private readonly templateEngine: TemplateEngine;
|
||||
private readonly templatePath: string;
|
||||
private readonly descriptions: string;
|
||||
private readonly contentType: string;
|
||||
private readonly extension: string;
|
||||
private readonly contentType: string;
|
||||
|
||||
public constructor(engine: TemplateEngine, templatePath: string, descriptions: string, contentType: string,
|
||||
extension: string) {
|
||||
public constructor(templateEngine: TemplateEngine, templatePath: string, extension: string, contentType: string) {
|
||||
super(INTERNAL_ERROR, contentType);
|
||||
this.engine = engine;
|
||||
this.templatePath = resolveAssetPath(templatePath);
|
||||
this.descriptions = resolveAssetPath(descriptions);
|
||||
this.contentType = contentType;
|
||||
this.templateEngine = templateEngine;
|
||||
this.templatePath = templatePath;
|
||||
this.extension = extension;
|
||||
this.contentType = contentType;
|
||||
}
|
||||
|
||||
public async handle({ representation }: RepresentationConverterArgs): Promise<Representation> {
|
||||
// Obtain the error from the representation stream
|
||||
const errors = await arrayifyStream(representation.data);
|
||||
if (errors.length !== 1) {
|
||||
throw new InternalServerError('Only single errors are supported.');
|
||||
}
|
||||
const error = errors[0] as Error;
|
||||
|
||||
// Render the template
|
||||
// Render the error description using an error-specific template
|
||||
let description: string | undefined;
|
||||
if (HttpError.isInstance(error)) {
|
||||
try {
|
||||
const templateFile = `${error.errorCode}${this.extension}`;
|
||||
assert(/^[\w.-]+$/u.test(templateFile), 'Invalid error template name');
|
||||
description = await this.templateEngine.render(error.details ?? {},
|
||||
{ templateFile, templatePath: this.templatePath });
|
||||
} catch {
|
||||
// In case no template is found, or rendering errors, we still want to convert
|
||||
}
|
||||
}
|
||||
|
||||
// Render the main template, embedding the rendered error description
|
||||
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);
|
||||
const rendered = await this.templateEngine.render(variables);
|
||||
|
||||
return new BasicRepresentation(rendered, representation.metadata, this.contentType);
|
||||
}
|
||||
|
||||
private async getErrorCodeMessage(error: Error): Promise<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');
|
||||
} catch {
|
||||
// In case no template is found we still want to convert
|
||||
return;
|
||||
}
|
||||
|
||||
return this.engine.apply(template, (error.details ?? {}) as NodeJS.Dict<string>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 { TemplateEngine } from '../../util/templates/TemplateEngine';
|
||||
import type { RepresentationConverterArgs } from './RepresentationConverter';
|
||||
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;
|
||||
private readonly templateEngine: TemplateEngine;
|
||||
|
||||
public constructor(engine: TemplateEngine, templatePath: string) {
|
||||
public constructor(templateEngine: TemplateEngine) {
|
||||
super(TEXT_MARKDOWN, TEXT_HTML);
|
||||
this.engine = engine;
|
||||
this.templatePath = resolveAssetPath(templatePath);
|
||||
this.templateEngine = templateEngine;
|
||||
}
|
||||
|
||||
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.templateEngine.render({ htmlBody, title });
|
||||
|
||||
return new BasicRepresentation(html, representation.metadata, TEXT_HTML);
|
||||
}
|
||||
|
29
src/util/templates/HandlebarsTemplateEngine.ts
Normal file
29
src/util/templates/HandlebarsTemplateEngine.ts
Normal file
@ -0,0 +1,29 @@
|
||||
/* eslint-disable tsdoc/syntax */
|
||||
// tsdoc/syntax can't handle {json} parameter
|
||||
import type { TemplateDelegate } from 'handlebars';
|
||||
import { compile } from 'handlebars';
|
||||
import type { TemplateEngine, Template } from './TemplateEngine';
|
||||
import { readTemplate } from './TemplateEngine';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
/**
|
||||
* Fills in Handlebars templates.
|
||||
*/
|
||||
export class HandlebarsTemplateEngine<T extends Dict<any> = Dict<any>> implements TemplateEngine<T> {
|
||||
private readonly applyTemplate: Promise<TemplateDelegate>;
|
||||
|
||||
/**
|
||||
* @param template - The default template @range {json}
|
||||
*/
|
||||
public constructor(template?: Template) {
|
||||
this.applyTemplate = readTemplate(template)
|
||||
.then((templateString: string): TemplateDelegate => compile(templateString));
|
||||
}
|
||||
|
||||
public async render(contents: T): Promise<string>;
|
||||
public async render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
|
||||
public async render<TCustom = T>(contents: TCustom, template?: Template): Promise<string> {
|
||||
const applyTemplate = template ? compile(await readTemplate(template)) : await this.applyTemplate;
|
||||
return applyTemplate(contents);
|
||||
}
|
||||
}
|
49
src/util/templates/TemplateEngine.ts
Normal file
49
src/util/templates/TemplateEngine.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { promises as fsPromises } from 'fs';
|
||||
import { joinFilePath, resolveAssetPath } from '../PathUtil';
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
export type Template = TemplateString | TemplatePath;
|
||||
|
||||
export interface TemplateString {
|
||||
// String contents of the template
|
||||
templateString: string;
|
||||
}
|
||||
|
||||
export interface TemplatePath {
|
||||
// Name of the template file
|
||||
templateFile: string;
|
||||
// Path of the template file
|
||||
templatePath?: string;
|
||||
}
|
||||
|
||||
/* eslint-disable @typescript-eslint/method-signature-style */
|
||||
/**
|
||||
* A template engine renders content into a template.
|
||||
*/
|
||||
export interface TemplateEngine<T extends Dict<any> = Dict<any>> {
|
||||
/**
|
||||
* Renders the given contents into the template.
|
||||
*
|
||||
* @param contents - The contents to render.
|
||||
* @param template - The template to use for rendering;
|
||||
* if omitted, a default template is used.
|
||||
* @returns The rendered contents.
|
||||
*/
|
||||
render(contents: T): Promise<string>;
|
||||
render<TCustom = T>(contents: TCustom, template: Template): Promise<string>;
|
||||
}
|
||||
/* eslint-enable @typescript-eslint/method-signature-style */
|
||||
|
||||
/**
|
||||
* Reads the template and returns it as a string.
|
||||
*/
|
||||
export async function readTemplate(template: Template = { templateString: '' }): Promise<string> {
|
||||
// The template has already been given as a string
|
||||
if ('templateString' in template) {
|
||||
return template.templateString;
|
||||
}
|
||||
// The template needs to be read from disk
|
||||
const { templateFile, templatePath } = template;
|
||||
const fullTemplatePath = templatePath ? joinFilePath(templatePath, templateFile) : templateFile;
|
||||
return fsPromises.readFile(resolveAssetPath(fullTemplatePath), 'utf8');
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import { HandlebarsTemplateEngine } from '../../../../src/pods/generate/HandlebarsTemplateEngine';
|
||||
|
||||
describe('A HandlebarsTemplateEngine', (): void => {
|
||||
const engine = new HandlebarsTemplateEngine();
|
||||
|
||||
it('fills in Handlebars templates.', async(): Promise<void> => {
|
||||
const template = '<{{webId}}> a <http://xmlns.com/foaf/0.1/Person>.';
|
||||
const options = { webId: 'http://alice/#profile' };
|
||||
expect(engine.apply(template, options)).toBe('<http://alice/#profile> a <http://xmlns.com/foaf/0.1/Person>.');
|
||||
});
|
||||
});
|
@ -1,5 +1,4 @@
|
||||
import type { ResourceIdentifier } from '../../../../src/ldp/representation/ResourceIdentifier';
|
||||
import { HandlebarsTemplateEngine } from '../../../../src/pods/generate/HandlebarsTemplateEngine';
|
||||
import { TemplatedResourcesGenerator } from '../../../../src/pods/generate/TemplatedResourcesGenerator';
|
||||
import type {
|
||||
FileIdentifierMapper,
|
||||
@ -8,6 +7,7 @@ import type {
|
||||
} from '../../../../src/storage/mapping/FileIdentifierMapper';
|
||||
import { ensureTrailingSlash, trimTrailingSlashes } from '../../../../src/util/PathUtil';
|
||||
import { readableToString } from '../../../../src/util/StreamUtil';
|
||||
import { HandlebarsTemplateEngine } from '../../../../src/util/templates/HandlebarsTemplateEngine';
|
||||
import { mockFs } from '../../../util/Util';
|
||||
|
||||
jest.mock('fs');
|
||||
|
@ -1,32 +1,23 @@
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { TemplateEngine } from '../../../../src/pods/generate/TemplateEngine';
|
||||
import { ErrorToTemplateConverter } from '../../../../src/storage/conversion/ErrorToTemplateConverter';
|
||||
import { BadRequestHttpError } from '../../../../src/util/errors/BadRequestHttpError';
|
||||
import { InternalServerError } from '../../../../src/util/errors/InternalServerError';
|
||||
import { readableToString } from '../../../../src/util/StreamUtil';
|
||||
import { mockFs } from '../../../util/Util';
|
||||
|
||||
jest.mock('fs');
|
||||
import type { TemplateEngine } from '../../../../src/util/templates/TemplateEngine';
|
||||
|
||||
describe('An ErrorToTemplateConverter', (): void => {
|
||||
let cache: { data: any };
|
||||
const identifier = { path: 'http://test.com/error' };
|
||||
const templatePath = '/templates/error.template';
|
||||
const descriptions = '/templates/codes';
|
||||
const templatePath = '/templates/codes';
|
||||
const errorCode = 'E0001';
|
||||
let engine: TemplateEngine;
|
||||
let templateEngine: jest.Mocked<TemplateEngine>;
|
||||
let converter: ErrorToTemplateConverter;
|
||||
const preferences = {};
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
cache = mockFs('/templates');
|
||||
cache.data['error.template'] = '{{ template }}';
|
||||
cache.data.codes = { [`${errorCode}.html`]: '{{{ errorText }}}' };
|
||||
engine = {
|
||||
apply: jest.fn().mockReturnValue('<html>'),
|
||||
templateEngine = {
|
||||
render: jest.fn().mockReturnValue(Promise.resolve('<html>')),
|
||||
};
|
||||
|
||||
converter = new ErrorToTemplateConverter(engine, templatePath, descriptions, 'text/html', '.html');
|
||||
converter = new ErrorToTemplateConverter(templateEngine, templatePath, '.html', 'text/html');
|
||||
});
|
||||
|
||||
it('supports going from errors to the given content type.', async(): Promise<void> => {
|
||||
@ -46,13 +37,14 @@ describe('An ErrorToTemplateConverter', (): void => {
|
||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||
const prom = converter.handle({ identifier, representation, preferences });
|
||||
await expect(prom).resolves.toBeDefined();
|
||||
|
||||
const result = await prom;
|
||||
expect(result.binary).toBe(true);
|
||||
expect(result.metadata.contentType).toBe('text/html');
|
||||
await expect(readableToString(result.data)).resolves.toBe('<html>');
|
||||
expect(engine.apply).toHaveBeenCalledTimes(1);
|
||||
expect(engine.apply).toHaveBeenLastCalledWith(
|
||||
'{{ template }}', { name: 'Error', message: 'error text', stack: error.stack },
|
||||
expect(templateEngine.render).toHaveBeenCalledTimes(1);
|
||||
expect(templateEngine.render).toHaveBeenLastCalledWith(
|
||||
{ name: 'Error', message: 'error text', stack: error.stack },
|
||||
);
|
||||
});
|
||||
|
||||
@ -60,15 +52,19 @@ describe('An ErrorToTemplateConverter', (): void => {
|
||||
const error = new BadRequestHttpError('error text');
|
||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||
const prom = converter.handle({ identifier, representation, preferences });
|
||||
templateEngine.render.mockRejectedValueOnce(new Error('error-specific template not found'));
|
||||
await expect(prom).resolves.toBeDefined();
|
||||
|
||||
const result = await prom;
|
||||
expect(result.binary).toBe(true);
|
||||
expect(result.metadata.contentType).toBe('text/html');
|
||||
await expect(readableToString(result.data)).resolves.toBe('<html>');
|
||||
expect(engine.apply).toHaveBeenCalledTimes(1);
|
||||
expect(engine.apply).toHaveBeenLastCalledWith(
|
||||
'{{ template }}', { name: 'BadRequestHttpError', message: 'error text', stack: error.stack },
|
||||
);
|
||||
expect(templateEngine.render).toHaveBeenCalledTimes(2);
|
||||
expect(templateEngine.render).toHaveBeenNthCalledWith(1,
|
||||
{},
|
||||
{ templatePath: '/templates/codes', templateFile: 'H400.html' });
|
||||
expect(templateEngine.render).toHaveBeenNthCalledWith(2,
|
||||
{ name: 'BadRequestHttpError', message: 'error text', stack: error.stack });
|
||||
});
|
||||
|
||||
it('only adds stack if it is defined.', async(): Promise<void> => {
|
||||
@ -76,18 +72,22 @@ describe('An ErrorToTemplateConverter', (): void => {
|
||||
delete error.stack;
|
||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||
const prom = converter.handle({ identifier, representation, preferences });
|
||||
templateEngine.render.mockRejectedValueOnce(new Error('error-specific template not found'));
|
||||
await expect(prom).resolves.toBeDefined();
|
||||
|
||||
const result = await prom;
|
||||
expect(result.binary).toBe(true);
|
||||
expect(result.metadata.contentType).toBe('text/html');
|
||||
await expect(readableToString(result.data)).resolves.toBe('<html>');
|
||||
expect(engine.apply).toHaveBeenCalledTimes(1);
|
||||
expect(engine.apply).toHaveBeenLastCalledWith(
|
||||
'{{ template }}', { name: 'BadRequestHttpError', message: 'error text' },
|
||||
);
|
||||
expect(templateEngine.render).toHaveBeenCalledTimes(2);
|
||||
expect(templateEngine.render).toHaveBeenNthCalledWith(1,
|
||||
{},
|
||||
{ templatePath: '/templates/codes', templateFile: 'H400.html' });
|
||||
expect(templateEngine.render).toHaveBeenNthCalledWith(2,
|
||||
{ name: 'BadRequestHttpError', message: 'error text' });
|
||||
});
|
||||
|
||||
it('adds additional information if an error code is found.', async(): Promise<void> => {
|
||||
it('adds additional information if an error code description is found.', async(): Promise<void> => {
|
||||
const error = new BadRequestHttpError('error text', { errorCode, details: { key: 'val' }});
|
||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||
const prom = converter.handle({ identifier, representation, preferences });
|
||||
@ -96,14 +96,12 @@ describe('An ErrorToTemplateConverter', (): void => {
|
||||
expect(result.binary).toBe(true);
|
||||
expect(result.metadata.contentType).toBe('text/html');
|
||||
await expect(readableToString(result.data)).resolves.toBe('<html>');
|
||||
expect(engine.apply).toHaveBeenCalledTimes(2);
|
||||
expect(engine.apply).toHaveBeenCalledWith(
|
||||
'{{{ errorText }}}', { key: 'val' },
|
||||
);
|
||||
expect(engine.apply).toHaveBeenLastCalledWith(
|
||||
'{{ template }}',
|
||||
{ name: 'BadRequestHttpError', message: 'error text', stack: error.stack, description: '<html>' },
|
||||
);
|
||||
expect(templateEngine.render).toHaveBeenCalledTimes(2);
|
||||
expect(templateEngine.render).toHaveBeenNthCalledWith(1,
|
||||
{ key: 'val' },
|
||||
{ templatePath: '/templates/codes', templateFile: 'E0001.html' });
|
||||
expect(templateEngine.render).toHaveBeenNthCalledWith(2,
|
||||
{ name: 'BadRequestHttpError', message: 'error text', stack: error.stack, description: '<html>' });
|
||||
});
|
||||
|
||||
it('sends an empty object for additional error code parameters if none are defined.', async(): Promise<void> => {
|
||||
@ -111,33 +109,35 @@ describe('An ErrorToTemplateConverter', (): void => {
|
||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||
const prom = converter.handle({ identifier, representation, preferences });
|
||||
await expect(prom).resolves.toBeDefined();
|
||||
|
||||
const result = await prom;
|
||||
expect(result.binary).toBe(true);
|
||||
expect(result.metadata.contentType).toBe('text/html');
|
||||
await expect(readableToString(result.data)).resolves.toBe('<html>');
|
||||
expect(engine.apply).toHaveBeenCalledTimes(2);
|
||||
expect(engine.apply).toHaveBeenCalledWith(
|
||||
'{{{ errorText }}}', { },
|
||||
);
|
||||
expect(engine.apply).toHaveBeenLastCalledWith(
|
||||
'{{ template }}',
|
||||
{ name: 'BadRequestHttpError', message: 'error text', stack: error.stack, description: '<html>' },
|
||||
);
|
||||
expect(templateEngine.render).toHaveBeenCalledTimes(2);
|
||||
expect(templateEngine.render).toHaveBeenNthCalledWith(1,
|
||||
{},
|
||||
{ templatePath: '/templates/codes', templateFile: 'E0001.html' });
|
||||
expect(templateEngine.render).toHaveBeenNthCalledWith(2,
|
||||
{ name: 'BadRequestHttpError', message: 'error text', stack: error.stack, description: '<html>' });
|
||||
});
|
||||
|
||||
it('converts errors with a code as usual if no corresponding template is found.', async(): Promise<void> => {
|
||||
const error = new BadRequestHttpError('error text', { errorCode: 'invalid' });
|
||||
const representation = new BasicRepresentation([ error ], 'internal/error', false);
|
||||
const prom = converter.handle({ identifier, representation, preferences });
|
||||
templateEngine.render.mockRejectedValueOnce(new Error('error-specific template not found'));
|
||||
await expect(prom).resolves.toBeDefined();
|
||||
|
||||
const result = await prom;
|
||||
expect(result.binary).toBe(true);
|
||||
expect(result.metadata.contentType).toBe('text/html');
|
||||
await expect(readableToString(result.data)).resolves.toBe('<html>');
|
||||
expect(engine.apply).toHaveBeenCalledTimes(1);
|
||||
expect(engine.apply).toHaveBeenLastCalledWith(
|
||||
'{{ template }}',
|
||||
{ name: 'BadRequestHttpError', message: 'error text', stack: error.stack },
|
||||
);
|
||||
expect(templateEngine.render).toHaveBeenCalledTimes(2);
|
||||
expect(templateEngine.render).toHaveBeenNthCalledWith(1,
|
||||
{},
|
||||
{ templatePath: '/templates/codes', templateFile: 'invalid.html' });
|
||||
expect(templateEngine.render).toHaveBeenNthCalledWith(2,
|
||||
{ name: 'BadRequestHttpError', message: 'error text', stack: error.stack });
|
||||
});
|
||||
});
|
||||
|
@ -1,27 +1,19 @@
|
||||
import { BasicRepresentation } from '../../../../src/ldp/representation/BasicRepresentation';
|
||||
import type { TemplateEngine } from '../../../../src/pods/generate/TemplateEngine';
|
||||
import { MarkdownToHtmlConverter } from '../../../../src/storage/conversion/MarkdownToHtmlConverter';
|
||||
import { readableToString } from '../../../../src/util/StreamUtil';
|
||||
import { mockFs } from '../../../util/Util';
|
||||
|
||||
jest.mock('fs');
|
||||
import type { TemplateEngine } from '../../../../src/util/templates/TemplateEngine';
|
||||
|
||||
describe('A MarkdownToHtmlConverter', (): void => {
|
||||
let cache: { data: any };
|
||||
const identifier = { path: 'http://test.com/text' };
|
||||
const templatePath = '/templates/error.template';
|
||||
const preferences = {};
|
||||
let engine: TemplateEngine;
|
||||
let templateEngine: TemplateEngine;
|
||||
let converter: MarkdownToHtmlConverter;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
cache = mockFs('/templates');
|
||||
cache.data['error.template'] = '{{ template }}';
|
||||
engine = {
|
||||
apply: jest.fn().mockReturnValue('<html>'),
|
||||
templateEngine = {
|
||||
render: jest.fn().mockReturnValue(Promise.resolve('<html>')),
|
||||
};
|
||||
|
||||
converter = new MarkdownToHtmlConverter(engine, templatePath);
|
||||
converter = new MarkdownToHtmlConverter(templateEngine);
|
||||
});
|
||||
|
||||
it('supports going from markdown to html.', async(): Promise<void> => {
|
||||
@ -38,9 +30,9 @@ describe('A MarkdownToHtmlConverter', (): void => {
|
||||
expect(result.binary).toBe(true);
|
||||
expect(result.metadata.contentType).toBe('text/html');
|
||||
await expect(readableToString(result.data)).resolves.toBe('<html>');
|
||||
expect(engine.apply).toHaveBeenCalledTimes(1);
|
||||
expect(engine.apply).toHaveBeenLastCalledWith(
|
||||
'{{ template }}', { htmlBody: '<p>Text <code>code</code> more text.</p>\n' },
|
||||
expect(templateEngine.render).toHaveBeenCalledTimes(1);
|
||||
expect(templateEngine.render).toHaveBeenLastCalledWith(
|
||||
{ htmlBody: '<p>Text <code>code</code> more text.</p>\n' },
|
||||
);
|
||||
});
|
||||
|
||||
@ -53,9 +45,9 @@ describe('A MarkdownToHtmlConverter', (): void => {
|
||||
expect(result.binary).toBe(true);
|
||||
expect(result.metadata.contentType).toBe('text/html');
|
||||
await expect(readableToString(result.data)).resolves.toBe('<html>');
|
||||
expect(engine.apply).toHaveBeenCalledTimes(1);
|
||||
expect(engine.apply).toHaveBeenLastCalledWith(
|
||||
'{{ template }}', { htmlBody: '<h1 id="title-text">title text</h1>\n<p>more text</p>\n', title: 'title text' },
|
||||
expect(templateEngine.render).toHaveBeenCalledTimes(1);
|
||||
expect(templateEngine.render).toHaveBeenLastCalledWith(
|
||||
{ htmlBody: '<h1 id="title-text">title text</h1>\n<p>more text</p>\n', title: 'title text' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
23
test/unit/util/templates/HandlebarsTemplateEngine.test.ts
Normal file
23
test/unit/util/templates/HandlebarsTemplateEngine.test.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { HandlebarsTemplateEngine } from '../../../../src/util/templates/HandlebarsTemplateEngine';
|
||||
|
||||
jest.mock('../../../../src/util/templates/TemplateEngine', (): any => ({
|
||||
readTemplate: jest.fn(async({ templateString }): Promise<string> => `${templateString}: {{detail}}`),
|
||||
}));
|
||||
|
||||
describe('A HandlebarsTemplateEngine', (): void => {
|
||||
const template = { templateString: 'xyz' };
|
||||
const contents = { detail: 'a&b' };
|
||||
let templateEngine: HandlebarsTemplateEngine;
|
||||
|
||||
beforeEach((): void => {
|
||||
templateEngine = new HandlebarsTemplateEngine(template);
|
||||
});
|
||||
|
||||
it('uses the default template when no template was passed.', async(): Promise<void> => {
|
||||
await expect(templateEngine.render(contents)).resolves.toBe('xyz: a&b');
|
||||
});
|
||||
|
||||
it('uses the passed template.', async(): Promise<void> => {
|
||||
await expect(templateEngine.render(contents, { templateString: 'my' })).resolves.toBe('my: a&b');
|
||||
});
|
||||
});
|
36
test/unit/util/templates/TemplateEngine.test.ts
Normal file
36
test/unit/util/templates/TemplateEngine.test.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { resolveAssetPath } from '../../../../src/util/PathUtil';
|
||||
import { readTemplate } from '../../../../src/util/templates/TemplateEngine';
|
||||
import { mockFs } from '../../../util/Util';
|
||||
|
||||
jest.mock('fs');
|
||||
|
||||
describe('readTemplate', (): void => {
|
||||
const templateFile = 'template.xyz';
|
||||
const templatePath = 'other';
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
const { data } = mockFs(resolveAssetPath(''));
|
||||
Object.assign(data, {
|
||||
'template.xyz': '{{template}}',
|
||||
other: {
|
||||
'template.xyz': '{{other}}',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the empty string when no template is provided.', async(): Promise<void> => {
|
||||
await expect(readTemplate()).resolves.toBe('');
|
||||
});
|
||||
|
||||
it('accepts string templates.', async(): Promise<void> => {
|
||||
await expect(readTemplate({ templateString: 'abc' })).resolves.toBe('abc');
|
||||
});
|
||||
|
||||
it('accepts a filename.', async(): Promise<void> => {
|
||||
await expect(readTemplate({ templateFile })).resolves.toBe('{{template}}');
|
||||
});
|
||||
|
||||
it('accepts a filename and path.', async(): Promise<void> => {
|
||||
await expect(readTemplate({ templateFile, templatePath })).resolves.toBe('{{other}}');
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user