@adobe/helix-pipeline
Version:
(formerly known as Hypermedia Pipeline)
166 lines (143 loc) • 5.39 kB
JavaScript
/*
* Copyright 2019 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
const { expand } = require('@emmetio/expand-abbreviation');
const { setdefault } = require('ferrum');
const findAndReplace = require('hast-util-find-and-replace');
const fromDOM = require('hast-util-from-dom');
const { JSDOM } = require('jsdom');
const { MarkupConfig } = require('@adobe/helix-shared');
/** Pleceholder variable for the generate template. */
const PLACEHOLDER_TEMPLATE = /\$\{\d+\}/g;
async function getMarkupConfig(context, action) {
setdefault(context, 'content', {});
const { logger, downloader } = action;
const markupConfigTask = downloader.getTaskById('markupconfig');
if (!markupConfigTask) {
logger.info('unable to adjust markup. no markup config task scheduled.');
return;
}
const res = await markupConfigTask;
if (res.status !== 200) {
logger.info(`unable to fetch markupconfig.yaml: ${res.status}`);
return;
}
// remember markupconfig as source
setdefault(context.content, 'sources', []).push(markupConfigTask.uri);
// Expose markupconfig on the action
const cfg = await new MarkupConfig()
.withSource(res.body)
.init();
setdefault(action, 'markupconfig', cfg.toJSON());
}
/**
* Returns the HTML element for the provided HTML template.
*
* @param {String} template The HTML template to use
*
* @returns {HTMLElement} the resulting HTML element including a `${0}` placeholder
*/
function getHTMLElement(template) {
const html = expand(template, {
field: (index, placeholder) => {
const p = placeholder ? `:${placeholder}` : '';
return `\${${index}${p}}`;
},
});
const doc = new JSDOM().window.document;
const dom = doc.createElement('div');
dom.innerHTML = html;
return dom.firstChild;
}
/**
* Adjust the MDAST tree according to the markup config.
* This is done by registering new matchers on the VDOMTransformer before the HTML
* is generated in the pipeline
*
* @param {Object} context the execution context
* @param {Object} logger the pipeline logger
* @param {Object} transformer the VDOM transformer
* @param {Object} markupconfig the markup config
*/
async function adjustMDAST(context, action) {
// Since this is called first in the pipeline, we get the markup config here
await getMarkupConfig(context, action);
const { logger, transformer, markupconfig } = action;
if (!markupconfig || !markupconfig.markup) {
return;
}
Object.entries(markupconfig.markup)
.filter(([_, cfg]) => cfg.type === 'markdown')
.forEach(([name, cfg]) => {
logger.info(`Applying markdown markup adjustment: ${name}`);
transformer.match(cfg.match, (h, node) => {
const handler = transformer.constructor.default(node);
let el = handler(h, node);
// Append classes to the element (space or comma separated)
if (cfg.classnames) {
el.properties.className = [
...(el.properties.className || '').split(' '),
...cfg.classnames,
].join(' ').trim();
}
// Append attributes to the element
if (cfg.attribute) {
Object.assign(el.properties, cfg.attribute);
}
// Wrap the element
if (cfg.wrap) {
const wrapperEl = getHTMLElement(cfg.wrap);
const n = fromDOM(wrapperEl);
el = findAndReplace(n, PLACEHOLDER_TEMPLATE, () => el);
}
return el;
});
});
}
/**
* Adjust the DOM tree according to the markup config.
* This is done by directly manipulating the DOM after it has been generated by the pipeline.
*
* @param {Object} context the execution context
* @param {Object} logger the pipeline logger
* @param {Object} markupconfig the markup config
*/
async function adjustHTML(context, { logger, markupconfig }) {
if (!markupconfig || !markupconfig.markup) {
return;
}
Object.entries(markupconfig.markup)
.filter(([_, cfg]) => !cfg.type || cfg.type === 'html')
.forEach(([name, cfg]) => {
logger.info(`Applying HTML markup adjustment: ${name}`);
const elements = context.content.document.querySelectorAll(cfg.match);
elements.forEach((el) => {
// Append classes to the element (space or comma separated)
if (cfg.classnames) {
el.classList.add(...cfg.classnames);
}
// Append attributes to the element
if (cfg.attribute) {
Object.entries(cfg.attribute).forEach((e) => el.setAttribute(e[0], e[1]));
}
// Wrap the element
if (cfg.wrap) {
const wrapperEl = getHTMLElement(cfg.wrap);
wrapperEl.innerHTML = wrapperEl.innerHTML.replace(PLACEHOLDER_TEMPLATE, el.outerHTML);
el.replaceWith(wrapperEl);
}
});
});
}
module.exports = {
adjustMDAST,
adjustHTML,
};