@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
199 lines (178 loc) • 5.28 kB
JavaScript
/**
* Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact Volker Schukai.
*
* SPDX-License-Identifier: AGPL-3.0
*/
import { instanceSymbol } from "../../../constants.mjs";
import {
assembleMethodSymbol,
CustomElement,
registerCustomElement,
updaterTransformerMethodsSymbol,
} from "../../../dom/customelement.mjs";
import { HtmlStyleSheet } from "./stylesheet/html.mjs";
import { sanitizeHtml } from "../../../dom/sanitize-html.mjs";
export { HtmlContent };
/**
* @private
* @type {symbol}
*/
const htmlContentElementSymbol = Symbol("htmlContentElement");
/**
* The HtmlContent component is used to display arbitrary HTML content within its shadow DOM.
* It provides options for sanitization to prevent XSS attacks.
*
* @copyright Volker Schukai
* @summary An HTML content component that can display sanitized HTML.
*/
class HtmlContent extends CustomElement {
/**
* This method is called by the `instanceof` operator.
* @return {symbol}
*/
static get [instanceSymbol]() {
return Symbol.for(
"@schukai/monster/components/content/viewer/html-content@@instance",
);
}
/**
* To set the options via the HTML tag, the attribute `data-monster-options` must be used.
* @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
*
* The individual configuration values can be found in the table.
*
* @property {Object} templates Template definitions
* @property {string} templates.main Main template
* @property {string} content The HTML string to be displayed.
* @property {Object} features Features to enable or disable specific functionalities.
* @property {boolean} features.sanitize Whether to sanitize the HTML content (removes scripts, etc.). Defaults to true.
* @property {object} sanitize Sanitization options.
* @property {function} sanitize.callback A callback function to sanitize the HTML content. Defaults to a built-in sanitizer. You can use libraries like DOMPurify for this purpose.
*/
get defaults() {
return Object.assign({}, super.defaults, {
templates: {
main: getTemplate(),
},
content: "", // Default content is an empty slot
features: {
sanitize: true,
},
sanitize: {
callback: sanitizeHtml.bind(this),
},
});
}
/**
* Returns the updater transformer methods for this component.
* @returns {{sanitizeHtml: ((function(*): (*))|*)}}
*/
[updaterTransformerMethodsSymbol]() {
return {
sanitizeHtml: (value) => {
if (this.getOption("features.sanitize")) {
return this.getOption("sanitize.callback")(value);
}
return value;
},
addErrorHandler: (value) => {
// Add error handling for images
if (typeof value === "string") {
const parser = new DOMParser();
const doc = parser.parseFromString(value, "text/html");
const images = doc.querySelectorAll("img");
images.forEach((img) => {
img.setAttribute(
"onerror",
"this.classList.add('notFoundImage'); this.src='data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';",
);
img.classList.add("notFoundError");
});
return doc.body.innerHTML;
}
return value;
},
};
}
/**
* Sets the content of the HtmlContent component.
* @param content
* @returns {HtmlContent}
*/
setContent(content) {
this.setOption("content", content);
return this;
}
/**
* @return {string}
*/
static getTag() {
return "monster-html-content";
}
/**
* @return {HtmlContent}
*/
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
initControlReferences.call(this);
initEventHandler.call(this);
grabChildren.call(this);
}
/**
* @return {Array}
*/
static getCSSStyleSheet() {
return [HtmlStyleSheet];
}
}
function grabChildren() {
// If there are children, we grab them and set the content
if (this.children.length > 0) {
const content = Array.from(this.children)
.map((child) => child.outerHTML)
.join("");
this.setContent(content);
this.innerHTML = "";
}
}
/**
* @private
* @return {HtmlContent}
*/
function initEventHandler() {
return this;
}
/**
* @private
* @return {HtmlContent}
*/
function initControlReferences() {
if (!this.shadowRoot) {
throw new Error("no shadow-root is defined");
}
this[htmlContentElementSymbol] = this.shadowRoot.querySelector(
"[data-monster-role=content-container]",
);
return this;
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
<div data-monster-role="content-container" part="content-container"
data-monster-replace="path:content | call:sanitizeHtml | call:addErrorHandler | default: "
></div>
`;
}
registerCustomElement(HtmlContent);