From b8c3e1ee276efb066c1030ef0457ad104126c8ae Mon Sep 17 00:00:00 2001 From: tai2 Date: Sat, 27 Jan 2024 11:21:51 +0900 Subject: [PATCH] Add parameter replacement --- README.md | 2 +- contents/parameters.json | 4 ++ .../template_with_parameter_replacement.html | 11 ++++ src/decor.ts | 29 +++++++++- src/decor_test.ts | 31 ++++++++++ src/extract_template.ts | 32 +---------- src/replace_parameters.ts | 56 +++++++++++++++++++ src/replace_parameters_test.ts | 39 +++++++++++++ src/template.ts | 32 +++++++++++ 9 files changed, 202 insertions(+), 34 deletions(-) create mode 100644 contents/parameters.json create mode 100644 contents/template_with_parameter_replacement.html create mode 100644 src/replace_parameters.ts create mode 100644 src/replace_parameters_test.ts diff --git a/README.md b/README.md index 868e46d..7e0e6b1 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ default mappings of the `paragraph`and `image` elements look like these. /> ``` -## Parameter Replacement (WIP) +## Parameter Replacement With content and attribute specifiers, you can reference a special key name starting with `param:`. When it is specified, decor tries to resolve the name diff --git a/contents/parameters.json b/contents/parameters.json new file mode 100644 index 0000000..984ede3 --- /dev/null +++ b/contents/parameters.json @@ -0,0 +1,4 @@ +{ + "id": "replaced ID", + "title": "replaced title" +} diff --git a/contents/template_with_parameter_replacement.html b/contents/template_with_parameter_replacement.html new file mode 100644 index 0000000..7933293 --- /dev/null +++ b/contents/template_with_parameter_replacement.html @@ -0,0 +1,11 @@ + + + + + Decor default template + + + + + + diff --git a/src/decor.ts b/src/decor.ts index 757d150..f751715 100644 --- a/src/decor.ts +++ b/src/decor.ts @@ -5,6 +5,10 @@ import { PartialTemplate, Template } from './template.ts' import { parsePartialTemplate, parseTemplate } from './extract_template.ts' import { templateRenderer } from './template_renderer.ts' import { renderHtml } from './render_html.ts' +import { + replaceDocumentParameters, + replaceTemplateParameters, +} from './replace_parameters.ts' import assets from './assets.json' assert { type: 'json' } async function renderDefaultTemplate(options: { output?: string }) { @@ -30,6 +34,7 @@ async function runOneshot(options: { input?: string output?: string defaultTemplate: [HTMLDocument, Template] + parameters: Record }) { // Prepare the template const [defaultHtmlDocument, defaultTemplate] = options.defaultTemplate @@ -50,12 +55,15 @@ async function runOneshot(options: { } } - template = partialTemplate as Template document = templateDocument + template = partialTemplate as Template + + replaceDocumentParameters(document, options.parameters) + replaceTemplateParameters(template, options.parameters) } else { // Use the default template but clone the document so that we can reuse the original data. - template = defaultTemplate document = defaultHtmlDocument.cloneNode(true) as HTMLDocument + template = defaultTemplate } // Execute rendering @@ -94,6 +102,7 @@ async function runWatch(options: { input?: string output: string defaultTemplate: [HTMLDocument, Template] + parameters: Record }) { const watchTargets: string[] = [] if (options.template) { @@ -129,7 +138,7 @@ async function main() { ...options } = parseArgs(Deno.args, { boolean: ['help', 'show-default-template', 'watch'], - string: ['template', 'output'], + string: ['template', 'output', 'parameters'], }) if (options['show-default-template']) { @@ -151,6 +160,18 @@ async function main() { const defaultTemplate = parseTemplate( assets.defaultTemplate, ) + + let parameters: Record + if (options.parameters) { + const parametersString = Deno.readTextFileSync(options.parameters) + parameters = JSON.parse(parametersString) + } else { + parameters = {} + } + + replaceDocumentParameters(defaultTemplate[0], parameters) + replaceTemplateParameters(defaultTemplate[1], parameters) + if (options.watch) { if (options.output === undefined) { console.error('--output is required when --watch is specified') @@ -162,6 +183,7 @@ async function main() { input: input?.toString(), output: options.output, defaultTemplate, + parameters, }) } else { await runOneshot({ @@ -169,6 +191,7 @@ async function main() { input: input?.toString(), output: options.output, defaultTemplate, + parameters, }) } } catch (e) { diff --git a/src/decor_test.ts b/src/decor_test.ts index 7227fe3..41895fd 100644 --- a/src/decor_test.ts +++ b/src/decor_test.ts @@ -1,11 +1,13 @@ import { assert, assertEquals, + assertExists, assertStringIncludes, } from './deps/std/assert.ts' import * as path from './deps/std/path.ts' import * as fs from './deps/std/fs.ts' import { delay } from './deps/std/async.ts' +import { DOMParser } from './deps/deno-dom.ts' function decor(...args: string[]): Deno.Command { const dirname = path.dirname(path.fromFileUrl(import.meta.url)) @@ -208,3 +210,32 @@ Deno.test( ) }, ) + +Deno.test('decor runs parameter replacement', () => { + const dirname = path.dirname(path.fromFileUrl(import.meta.url)) + const templatePath = path.join( + dirname, + '../contents/template_with_parameter_replacement.html', + ) + const parametersPath = path.join( + dirname, + '../contents/parameters.json', + ) + + const { code, stdout, stderr } = decor( + '--template', + templatePath, + '--parameters', + parametersPath, + ).outputSync() + + assertEquals(code, 0) + + const outputHtml = new TextDecoder().decode(stdout) + const document = new DOMParser().parseFromString(outputHtml, 'text/html')! + const title = document.querySelector('title') + + assertExists(title) + assertEquals(title.getAttribute('id'), 'replaced ID') + assertEquals(title.innerHTML, 'replaced title') +}) diff --git a/src/extract_template.ts b/src/extract_template.ts index 14fba74..cc913e8 100644 --- a/src/extract_template.ts +++ b/src/extract_template.ts @@ -1,38 +1,10 @@ import { DOMParser, HTMLDocument } from './deps/deno-dom.ts' -import { PartialTemplate, Template } from './template.ts' +import { createPartialTemplate, PartialTemplate, Template } from './template.ts' export function extractPartialTemplate( templateDocument: HTMLDocument, ): PartialTemplate { - const template: PartialTemplate = { - heading1: null, - heading2: null, - heading3: null, - heading4: null, - heading5: null, - heading6: null, - thematic_break: null, - paragraph: null, - code_block: null, - block_quote: null, - table: null, - table_header: null, - table_header_cell: null, - table_row: null, - table_row_cell: null, - ordered_list: null, - ordered_list_item: null, - unordered_list: null, - unordered_list_item: null, - link: null, - image: null, - video: null, - code_span: null, - emphasis: null, - strong_emphasis: null, - strikethrough: null, - hard_line_break: null, - } + const template = createPartialTemplate() for (const key of Object.keys(template) as Array) { const fragment = templateDocument.querySelector( diff --git a/src/replace_parameters.ts b/src/replace_parameters.ts new file mode 100644 index 0000000..fea2b0a --- /dev/null +++ b/src/replace_parameters.ts @@ -0,0 +1,56 @@ +import { Element, HTMLDocument } from './deps/deno-dom.ts' +import { PartialTemplate, Template } from './template.ts' + +const attributeRegex = /^data-decor-attribute-(.+)$/ +const parameterRegex = /^param:(.+)$/ + +function replaceElementAttributes( + element: Element, + parameters: Record, +): void { + for (const attribute of element.attributes) { + const parametersMatch = parameterRegex.exec(attribute.value) + if (!parametersMatch) { + continue + } + + const parameterKey = parametersMatch[1] + if (!parameters[parameterKey]) { + continue + } + + const attrebuteMatch = attributeRegex.exec(attribute.name) + if (attrebuteMatch) { + const attributeName = attrebuteMatch[1] + element.setAttribute(attributeName, parameters[parameterKey]) + continue + } + + if (attribute.name === 'data-decor-content') { + element.innerHTML = parameters[parameterKey] + } + } +} + +export function replaceDocumentParameters( + document: HTMLDocument, + parameters: Record, +): void { + document.querySelectorAll('*').forEach((element) => { + replaceElementAttributes(element as Element, parameters) + }) +} + +export function replaceTemplateParameters( + template: PartialTemplate, + parameters: Record, +): void { + for ( + const key of Object.keys(template) as Array + ) { + const element = template[key] + if (element) { + replaceElementAttributes(element, parameters) + } + } +} diff --git a/src/replace_parameters_test.ts b/src/replace_parameters_test.ts new file mode 100644 index 0000000..c8d9df4 --- /dev/null +++ b/src/replace_parameters_test.ts @@ -0,0 +1,39 @@ +import { DOMParser } from './deps/deno-dom.ts' +import { assertEquals } from './deps/std/assert.ts' +import { createPartialTemplate } from './template.ts' +import { + replaceDocumentParameters, + replaceTemplateParameters, +} from './replace_parameters.ts' + +Deno.test( + 'replaceDocumentParameters handles parameter replacement in a document', + () => { + const document = new DOMParser().parseFromString( + '
', + 'text/html', + )! + replaceDocumentParameters(document, { bar: 'baz' }) + const element = document.querySelector('div')! + assertEquals(element.getAttribute('foo'), 'baz') + assertEquals(element.innerHTML, 'baz') + }, +) + +Deno.test( + 'replaceTemplateParameters handles parameter replacement in a template', + () => { + const document = new DOMParser().parseFromString( + '

', + 'text/html', + )! + const template = { + ...createPartialTemplate(), + heading1: document.querySelector('h1')!, + } + replaceTemplateParameters(template, { bar: 'baz' }) + const element = template.heading1 + assertEquals(element.getAttribute('foo'), 'baz') + assertEquals(element.innerHTML, 'baz') + }, +) diff --git a/src/template.ts b/src/template.ts index b4f8d67..23c507d 100644 --- a/src/template.ts +++ b/src/template.ts @@ -33,4 +33,36 @@ export type PartialTemplate = { hard_line_break: Element | null } +export function createPartialTemplate(): PartialTemplate { + return { + heading1: null, + heading2: null, + heading3: null, + heading4: null, + heading5: null, + heading6: null, + thematic_break: null, + paragraph: null, + code_block: null, + block_quote: null, + table: null, + table_header: null, + table_header_cell: null, + table_row: null, + table_row_cell: null, + ordered_list: null, + ordered_list_item: null, + unordered_list: null, + unordered_list_item: null, + link: null, + image: null, + video: null, + code_span: null, + emphasis: null, + strong_emphasis: null, + strikethrough: null, + hard_line_break: null, + } +} + export type Template = SetNonNullable