]> Lady’s Gitweb - Lemon/blob - mod.js
51acb773424c3b1d4da29c90690d7f8541ed2c70
[Lemon] / mod.js
1 // 🍋🏷 Lemon ∷ mod.js
2 // ====================================================================
3 //
4 // Copyright © 2022 Lady [@ Lady’s Computer].
5 //
6 // This Source Code Form is subject to the terms of the Mozilla Public
7 // License, v. 2.0. If a copy of the MPL was not distributed with this
8 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
9
10 import { xhtmlNamespace } from "./names.js";
11
12 /**
13 * Create a D·O·M Element from a tagged template.
14 *
15 * Usage :—
16 *
17 * ```js
18 * Lemon("elementName", "namespace")`content`;
19 * // or
20 * Lemon("elementName", "namespace")({ attribute: "value" })`content`;
21 * ```
22 *
23 * Content may, via substitutions, include additional nodes. As a
24 * convenience, if the namespace is not defined, it is the X·H·T·M·L
25 * namespace. As a convenience, `Lemon.elementName` is a shorthand for
26 * `Lemon("element-name").` As a convenience, substitutions may take
27 * the form of an array of nodes.
28 */
29 const Lemon = new Proxy(
30 Object.defineProperties(
31 Object.setPrototypeOf(
32 function (name, namespace = xhtmlNamespace) {
33 return Object.setPrototypeOf(
34 nodeTagBuilder.bind({
35 name: `${name}`,
36 namespace: `${namespace}`,
37 }),
38 Lemon.prototype,
39 );
40 },
41 Function,
42 ),
43 {
44 name: { value: "Lemon" },
45 prototype: { value: Object.create(Function.prototype) },
46 },
47 ),
48 {
49 /** If `P` doesn’t exist on `O`, calls `O` with `P` instead. */
50 get(O, P, Receiver) {
51 if (typeof P != "string" || Reflect.has(O, P)) {
52 return Reflect.get(O, P, Receiver);
53 } else if (typeof O == "function") {
54 return O(
55 Array.from(function* () {
56 for (const character of P) {
57 yield /[A-Z]/u.test(character)
58 ? `-${character.toLowerCase()}`
59 : character;
60 }
61 }()).join(""),
62 );
63 } else {
64 return undefined;
65 }
66 },
67 },
68 );
69 export default Lemon;
70
71 /**
72 * Creates an Element with `name`, `namespace`, and `attributes` drawn
73 * from this object and content determined by the provided strings and
74 * expressions.
75 */
76 const createNode = function (strings, ...expressions) {
77 const { attributes, name, namespace } = this;
78 const { raw } = strings;
79 const result = document.createElementNS(namespace, name);
80 for (const [attName, attValue] of Object.entries(attributes)) {
81 result.setAttribute(attName, `${attValue}`);
82 }
83 const maxIndex = Math.max(raw.length, expressions.length);
84 for (let index = 0; index < maxIndex; ++index) {
85 result.append(...nodesFromExpression(raw[index]));
86 result.append(...nodesFromExpression(expressions[index]));
87 }
88 result.normalize();
89 return result;
90 };
91
92 /**
93 * Creates a new template tag builder with the provided attributes for
94 * for making Element’s with the `name` specified on this.
95 *
96 * As a shorthand, you can also use this function as a template tag
97 * itself, in which case it is treated as though its attributes were
98 * empty.
99 */
100 const nodeTagBuilder = function (attrsOrStrings, ...expressions) {
101 const { name, namespace } = this;
102 if (
103 Array.isArray(attrsOrStrings) && "raw" in attrsOrStrings &&
104 Array.isArray(attrsOrStrings.raw)
105 ) {
106 // The first argument is usable as a template string.
107 return createNode.call(
108 { attributes: {}, document, name, namespace },
109 attrsOrStrings,
110 ...expressions,
111 );
112 } else {
113 // The first argument is not a template string.
114 return createNode.bind({
115 attributes: { ...Object(attrsOrStrings) },
116 document,
117 name,
118 namespace,
119 });
120 }
121 };
122
123 /** Processes the provided expression and yields Nodes. */
124 const nodesFromExpression = function* (expression) {
125 if (expression == null) {
126 // The expression is nullish.
127 /* do nothing */
128 } else {
129 // The expression is not nullish.
130 const expressionIsNode = (() => {
131 try {
132 Reflect.get(Node.prototype, "nodeType", expression);
133 return true;
134 } catch {
135 return false;
136 }
137 })();
138 if (expressionIsNode) {
139 // The expression is already a `Node`.
140 yield expression;
141 } else if (Array.isArray(expression)) {
142 // The expression is an array of expressions.
143 for (const subexpression of expression) {
144 yield* nodesFromExpression(subexpression);
145 }
146 } else {
147 // The expression is something else.
148 yield document.createTextNode(`${expression}`);
149 }
150 }
151 };
This page took 0.059053 seconds and 3 git commands to generate.