Adds a new Fragment component that provides the same functionality as React.Fragment. See issue #9 for details. closes issue #9
110 lines
3.1 KiB
TypeScript
110 lines
3.1 KiB
TypeScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
import { Element, HTMLElement } from "./component";
|
|
|
|
const HTML_ESCAPES: { [char: string]: string } = {
|
|
"&": "&",
|
|
"<": "<",
|
|
">": ">",
|
|
};
|
|
|
|
const escapeHtml = (text: string): string =>
|
|
text.replace(/[&<>]/g, (t) => HTML_ESCAPES[t]);
|
|
|
|
const escapeAttr = (text: string): string => text.replace(/"/g, """);
|
|
|
|
const renderElement = (elem: Element): string => {
|
|
// Ignore null and true/false to support nicer JSX conditional syntax with
|
|
// &&, ||, !! operators.
|
|
if (elem == null || typeof elem === "boolean") {
|
|
return "";
|
|
}
|
|
if (typeof elem === "number") {
|
|
return elem.toString();
|
|
}
|
|
if (typeof elem === "string") {
|
|
return escapeHtml(elem);
|
|
}
|
|
if (Array.isArray(elem)) {
|
|
return elem.map((e) => renderElement(e)).join("");
|
|
}
|
|
|
|
let output = "";
|
|
output += startTag(elem);
|
|
for (const child of elem.children) {
|
|
output += renderElement(child);
|
|
}
|
|
output += endTag(elem);
|
|
return output;
|
|
};
|
|
|
|
const startTag = (elem: HTMLElement): string => {
|
|
if (elem.tag === "#fragment") {
|
|
return "";
|
|
}
|
|
|
|
let output = `<${escapeHtml(elem.tag)}`;
|
|
|
|
for (const [attrName, attrValue] of Object.entries(elem.attributes)) {
|
|
// Handle boolean attributes with a false value by not outputting the
|
|
// attribute at all.
|
|
if (attrValue === false) {
|
|
continue;
|
|
}
|
|
|
|
let normalizedAttrName = escapeHtml(attrName.toLowerCase());
|
|
if (normalizedAttrName === "classname") {
|
|
normalizedAttrName = "class";
|
|
}
|
|
if (attrValue === true) {
|
|
output += ` ${normalizedAttrName}=""`;
|
|
} else {
|
|
output += ` ${normalizedAttrName}="${escapeAttr(
|
|
attrValue.toString()
|
|
)}"`;
|
|
}
|
|
}
|
|
|
|
output += ">";
|
|
return output;
|
|
};
|
|
|
|
const endTag = (elem: HTMLElement): string => {
|
|
if (elem.tag === "#fragment") {
|
|
return "";
|
|
}
|
|
return `</${escapeHtml(elem.tag)}>`;
|
|
};
|
|
|
|
/**
|
|
* Render a complete HTML page from an HTMLElement. Note that the root element
|
|
* must represent a valid HTML tag.
|
|
*
|
|
* @param rootElem HTML element representing the root of the document.
|
|
*
|
|
* @return Fully rendered HTML document as a string.
|
|
*/
|
|
export const renderPage = (rootElem: Element): string => {
|
|
if (rootElem == undefined) {
|
|
throw new Error(`root page element cannot be null`);
|
|
}
|
|
if (typeof rootElem !== "object" || !("tag" in rootElem)) {
|
|
throw new Error(
|
|
`root page element must be a valid HTMLElement, got ${JSON.stringify(
|
|
rootElem
|
|
)}`
|
|
);
|
|
}
|
|
if (rootElem.tag.toLowerCase() !== "html") {
|
|
throw new Error(
|
|
`attempted to render page with non-HTML root element ${rootElem.tag}`
|
|
);
|
|
}
|
|
|
|
let output = "<!DOCTYPE html>";
|
|
output += renderElement(rootElem);
|
|
return output;
|
|
};
|