UNPKG

@athenna/view

Version:

The Athenna template engine. Built on top of Edge.js.

423 lines (422 loc) 12.4 kB
/** * @athenna/view * * (c) João Lenon <lenon@athenna.io> * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ import { Edge } from 'edge.js'; import { debug } from '#src/debug'; import { Config } from '@athenna/config'; import { resolve, isAbsolute } from 'node:path'; import { Is, File, Path, Macroable } from '@athenna/common'; import { EmptyComponentException } from '#src/exceptions/EmptyComponentException'; import { NotFoundComponentException } from '#src/exceptions/NotFoundComponentException'; import { AlreadyExistComponentException } from '#src/exceptions/AlreadyExistComponentException'; export class ViewImpl extends Macroable { constructor() { super(); this.edge = Edge.create(Config.get('view.edge', {})); } /** * Render some view with optional data included. * * @example * ```ts * View.render('welcome', { greeting: 'Hello world' }) * ``` */ async render(template, data) { if (!this.isMountedOrIsTemplate(template)) { throw new NotFoundComponentException(template); } return this.edge.render(template, data); } /** * Render some view asynchronously with optional data included. * * @example * ```ts * View.renderSync('welcome', { greeting: 'Hello world' }) * ``` */ renderSync(template, data) { if (!this.isMountedOrIsTemplate(template)) { throw new NotFoundComponentException(template); } return this.edge.renderSync(template, data); } /** * Render some raw-edge content with optional data included. * * @example * ```ts * View.renderRaw('Hello {{ value }}', { value: 'World!' }) * ``` */ async renderRaw(content, data) { return this.edge.renderRaw(content, data); } /** * Render some raw-edge content asynchronously with optional * data included. * * @example * ```ts * View.renderRawSync('Hello {{ value }}', { value: 'World!' }) * ``` */ renderRawSync(content, data) { return this.edge.renderRawSync(content, data); } /** * Render some raw-edge file content with optional data included. * * @example * ```ts * View.renderRawByPath(Path.views('hello.edge'), { value: 'World!' }) * ``` */ async renderRawByPath(path, data) { return new File(path) .getContentAsString() .then(content => this.edge.renderRaw(content, data)); } /** * Render some raw-edge file content asynchronously with optional * data included. * * @example * ```ts * View.renderRawByPathSync(Path.views('hello.edge'), { value: 'World!' }) * ``` */ renderRawByPathSync(path, data) { const content = new File(path).getContentAsStringSync(); return this.edge.renderRawSync(content, data); } /** * Add a new property to templates. The properties registered * here will be available to all the templates. You can use * this method to update properties too. * * @example * ```ts * View * .addProperty('usernameOne', 'txsoura') * .addProperty('usernameTwo', 'jlenon7') * .addProperty('time', () => new Date().getTime()) * ``` */ addProperty(key, value) { this.edge.global(key, value); return this; } /** * Remove some property from views registered using * "addProperty" method. * * @example * ```ts * View * .addProperty('testing', '') * .removeProperty('testing') * ``` */ removeProperty(key) { if (!this.edge.globals[key]) { return this; } delete this.edge.globals[key]; return this; } /** * Add a new tag to templates. Just like @component * @if, etc. * * @example * ```ts * import type { TagContract } from '@athenna/view' * * const reverseTagOptions: TagContract = { * block: false, * seekable: true, * compile(parser, buffer, token) { * buffer.outputRaw('Hello from reverse tag') * } * } * * View.addTag('reverse', reverseTagOptions) * * const output = await View.renderRaw('@reverse()') // 'Hello from reverse tag' * ``` */ addTag(name, options) { this.edge.registerTag({ tagName: name, ...options }); return this; } /** * Remove some tag from views registered using * "addTag" method. * * @example * ```ts * View * .addTag('reverse', { ... }) * .removeTag('reverse') * ``` */ removeTag(name) { if (!this.edge.tags[name]) { return this; } delete this.edge.tags[name]; return this; } /** * Create a new view disk. View disks can be used * to register multiple views at the same time. * * Imagine these three paths: * * resources/views/admin/listUsers.edge\ * resources/views/admin/createUser.edge\ * resources/views/admin/details/listUserDetails.edge * * @example * ```ts * View.createViewDisk(Path.views()) * View.createViewDisk('admin', Path.views('admin')) * * const users = [...] * * View.render('admin/listUsers', { users }) * View.render('admin::listUsers', { users }) * * View.render('admin/createUser') * View.render('admin::createUser') * * View.render('admin/details/listUserDetails', { users }) * View.render('admin::details/listUserDetails', { users }) * ``` */ createViewDisk(name, path) { if (!path) { debug('Creating view disk for path %s.', name); if (!isAbsolute(name)) { debug('Path %s is not absolute and is going to be resolved using cwd %s.', name, Path.pwd()); name = resolve(Path.pwd(), name); } this.edge.mount(name); return this; } debug('Creating view disk %s for path %s.', name, path); if (!isAbsolute(path)) { debug('Path %s for view disk %s is not absolute and is going to be resolved using cwd %s.', path, name, Path.pwd()); path = resolve(Path.pwd(), path); } this.edge.mount(name, path); return this; } /** * Delete a view disk that was registered using * the "createViewDisk" method. * * @example * ```ts * View * .createViewDisk('admin', Path.views('admin')) * .removeViewDisk('admin') * ``` */ removeViewDisk(name) { if (!this.hasViewDisk(name)) { debug('View disk %s does not exist, skipping removing operation.', name); return this; } debug('Removing view disk %s.', name); this.edge.unmount(name); return this; } /** * Verify if some view disk exists. * * @example * View.createViewDisk('testing', Path.views('testing')) * * View.hasViewDisk('testing') // true * View.hasViewDisk('testing::subTesting') // true * View.hasViewDisk('testing::subTesting/notFound') // false */ hasViewDisk(name) { try { const path = this.edge.loader.makePath(name); this.edge.loader.resolve(path); return true; } catch (_err) { const has = this.edge.loader.mounted[name]; return !!has; } } /** * Create an in-memory component. * * @example * ```ts * View.createComponent('button', '<button class="{{ this.type }}">@!yield($slots.main())</button>') * ``` * * In-memory components could be used this * way (Ignore the "\\" value in the example): * * @example * ```edge * \@component('button', type = 'primary') * Get started * \@endcomponent * ``` */ createComponent(name, component) { if (!Is.Defined(component)) { throw new EmptyComponentException(name); } if (this.hasComponent(name)) { throw new AlreadyExistComponentException(name); } debug('Registering component %s.', name); this.edge.registerTemplate(name, { template: component }); return this; } /** * Same as "createComponent" method but create the template by the path * instead. If the file path does not exist, an error will throw. * * @example * ```ts * const path = Path.resources('views/myTemplate.edge') * * View.createComponentByPath('myTemplate', path) * ``` */ createComponentByPath(name, path) { if (!isAbsolute(path)) { debug('Path %s for view disk is not absolute and is going to be resolved using cwd %s.', path, Path.pwd()); path = resolve(Path.pwd(), path); } const file = new File(path); return this.createTemplate(name, file.getContentAsStringSync()); } /** * Verify if some component exists. * * @example * View * .createComponent('testing', '') * .hasComponent('testing') // true */ hasComponent(name) { return !!this.edge.loader.templates[name]; } /** * Delete the component created using the "createComponent" * method. * * @example * View * .createComponent('testing', '') * .removeComponent('testing') */ removeComponent(name) { debug('Removing component %s.', name); this.edge.removeTemplate(name); return this; } /** * Create a in-memory template. If the template name already exists, it * will be replaced. * * @example * ```ts * View.createTemplate('artisan::command', 'export class {{ namePascal }}') * * View.render('artisan::command', { namePascal: 'MyCommand' }) * ``` * * In-memory template could be used as components also * (Ignore the "\\" value in the example): * * @example * ```edge * \@component('artisan::command') * Hello * \@endcomponent * ``` */ createTemplate(name, template) { if (this.hasTemplate(name)) { debug('Template %s already exists and will be removed first.', name); this.removeTemplate(name); } debug('Creating template %s.', name); return this.createComponent(name, template); } /** * Same as "createTemplate" method but create the template by the path * instead. If the file path does not exist, the registration is ignored * (no errors). * * @example * ```ts * const path = Path.resources('views/myTemplate.edge') * * View.createTemplateByPath('myTemplate', path) * ``` */ createTemplateByPath(name, path) { if (!isAbsolute(path)) { debug('Path %s for view disk is not absolute and is going to be resolved using cwd %s.', path, Path.pwd()); path = resolve(Path.pwd(), path); } const file = new File(path, Buffer.from('')); if (!file.fileExists) { return this; } return this.createTemplate(name, file.getContentAsStringSync()); } /** * Verify if some template exists. * * @example * View * .createTemplate('testing', '') * .hasTemplate('testing') // true */ hasTemplate(name) { return this.hasComponent(name); } /** * Delete the template created using the "createTemplate" * method. * * @example * View * .createTemplate('testing', '') * .removeTemplate('testing') */ removeTemplate(name) { if (!this.hasTemplate(name)) { debug('Template %s does not exist and removing operation will be skipped.', name); return this; } debug('Removing template %s.', name); return this.removeComponent(name); } /** * Verify if Edge has the template name loaded or mounted. */ isMountedOrIsTemplate(template) { return this.hasViewDisk(template) || this.hasTemplate(template); } }