// types export type Attributes = { [key: string]: string | number | boolean }; // internal helper function isVoidTag(tagName: string): boolean { // deno-fmt-ignore return [ "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr", ].includes(tagName); } function parseAttributes(attributes: Attributes = {}): string[] { const attrs: string[] = []; Object.entries(attributes) .forEach(([k, v]) => { if (typeof v !== "boolean") { // add the pair of key and value when the attribute is string or number const value = sanitize(`${v}`, { amp: false, lt: false, gt: false }); attrs.push(` ${k}="${value}"`); } else if (v) { // add just key key when the attribute is true attrs.push(` ${k}`); } // skip when the attribute is false }); return attrs; } /** * Render markup tag. * @param tagName (required) * @param attributes (optional) * @param children (optional) * @return rendered tag * * Examples: * * ```ts * import { tag } from "https://deno.land/x/markup_tag/mod.ts"; * import { assertEquals } from "https://deno.land/std/testing/asserts.ts" * * // common usage * assertEquals( * tag("div", { id: "foo", class: "bar" }, "Hello world!"), * `
`, * ); * * // void (no-close) tag * assertEquals( * tag("meta", { charset: "utf-8" }), * ``, * ); * * // nested tags * assertEquals( * tag( "ul", { class: "nav" }, tag("li", "first"), tag("li", "second")), * ` `, * ); * * // boolean attributes * assertEquals( * tag("button", { type: "button", disabled: true }, "disabled"), * ``, * ); * // skip attributes * assertEquals( * tag("input", { type: "text", readonly: false }), * ``, * ); * ``` */ export function tag( tagName: string, attributesOrFirstChild?: Attributes | string, ...children: string[] ): string { if (!tagName) { throw new Error("tagName is empty."); } if (/\s/.test(tagName)) { throw new Error("tagName has whitespace characters."); } if (typeof attributesOrFirstChild === "string") { children.unshift(attributesOrFirstChild); attributesOrFirstChild = {}; } const attrs = parseAttributes(attributesOrFirstChild); const close = isVoidTag(tagName) ? "" : `${children.join("")}${tagName}>`; return `<${tagName}${attrs.join("")}>${close}`; } /** * Render markup tag, always add closing tag unlike tag(). * @param tagName (required) * @param attributes (optional) * @param children (optional) * @return rendered tag * * Examples: * * ```ts * import { tag, tagNoVoid } from "https://deno.land/x/markup_tag/mod.ts"; * import { assertEquals } from "https://deno.land/std/testing/asserts.ts" * * // in tag(), skip attributes in void tags like 'link' * assertEquals( * tag("link", "http://example.com"), * ``, * ); * * // in tagNoVoid(), always add closing tag * assertEquals( * tagNoVoid("link", "http://example.com"), * `http://example.com`, * ); * ``` */ export function tagNoVoid( tagName: string, attributesOrFirstChild?: Attributes | string, ...children: string[] ): string { if (isVoidTag(tagName)) { if (typeof attributesOrFirstChild === "string") { children.unshift(attributesOrFirstChild); attributesOrFirstChild = {}; } return tag(tagName, attributesOrFirstChild) + `${children.join("")}${tagName}>`; } return tag(tagName, attributesOrFirstChild, ...children); } /** * Render markup tag, always remove closing tag unlike tag(). * @param tagName (required) * @param attributes (optional) * @return rendered tag * * Examples: * * ```ts * import { tag, tagVoid } from "https://deno.land/x/markup_tag/mod.ts"; * import { assertEquals } from "https://deno.land/std/testing/asserts.ts" * * // in tag(), add close tag like 'div' * assertEquals( * tag("div", { class: "red" }), * ``, * ); * * // in tagVoid(), always remove closing tag * assertEquals( * tagVoid("div", { class: "red" }), * `