]> Lady’s Gitweb - Lemon/blob - mod.js
Allow document specification and binding
[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 const {
13 /**
14 * Create a D·O·M Element from a tagged template.
15 *
16 * Usage :—
17 *
18 * ```js
19 * Lemon("elementName", "namespace")`content`;
20 * // or
21 * Lemon("elementName", "namespace")({ attr: "value" })`content`;
22 * ```
23 *
24 * Content may, via substitutions, include additional nodes. As a
25 * convenience, if the namespace is not defined, it is the X·H·T·M·L
26 * namespace. As a convenience, `Lemon.elementName` is a shorthand
27 * for `Lemon("element-name").` As a convenience, substitutions may
28 * take the form of an array of nodes.
29 *
30 * By default, `globalThis.document` is used as the owner document
31 * for new nodes. To pick a different document, supply a different
32 * `document` on the `this` value when calling. You can use `bind` to
33 * simplify this in some cases :—
34 *
35 * ```js
36 * const MyLemon = Lemon.bind({ document });
37 * MyLemon.p`words`; // same as Lemon.call({ document }, "p")`words`
38 * ```
39 */
40 Lemon,
41 } = (() => {
42 const lemonProxyHandler = Object.assign(
43 Object.create(null),
44 {
45 /** If `P` doesn’t exist on `O`, calls `O` with `P` instead. */
46 get(O, P, Receiver) {
47 if (typeof P != "string" || Reflect.has(O, P)) {
48 return Reflect.get(O, P, Receiver);
49 } else if (typeof O == "function") {
50 return O([...function* () {
51 for (const character of P) {
52 yield /[A-Z]/u.test(character)
53 ? `-${character.toLowerCase()}`
54 : character;
55 }
56 }()].join(""));
57 } else {
58 return undefined;
59 }
60 },
61 },
62 );
63 return {
64 Lemon: new Proxy(
65 Object.defineProperties(
66 Object.setPrototypeOf(
67 function (name, namespace = xhtmlNamespace) {
68 return Object.setPrototypeOf(
69 nodeTagBuilder.bind({
70 context: this ?? globalThis,
71 name: `${name}`,
72 namespace: `${namespace}`,
73 }),
74 Lemon.prototype,
75 );
76 },
77 Function,
78 ),
79 {
80 name: { value: "Lemon" },
81 prototype: { value: Object.create(Function.prototype) },
82 bind: {
83 value: Object.defineProperties(
84 function (thisArg, ...args) {
85 return new Proxy(
86 Function.prototype.bind.call(
87 this,
88 thisArg,
89 ...args,
90 ),
91 lemonProxyHandler,
92 );
93 },
94 { name: { value: "bind" } },
95 ),
96 writable: true,
97 enumerable: false,
98 configurable: true,
99 },
100 },
101 ),
102 lemonProxyHandler,
103 ),
104 };
105 })();
106 export default Lemon;
107
108 /**
109 * Creates an Element with `name`, `namespace`, and `attributes` drawn
110 * from this object and content determined by the provided strings and
111 * expressions.
112 */
113 const createNode = function (strings, ...expressions) {
114 const { raw } = strings;
115 const { context, name, namespace, attributes } = this;
116 const document = context.document ?? globalThis.document;
117 const result = document.createElementNS(namespace, name);
118 for (const [attName, attValue] of Object.entries(attributes)) {
119 result.setAttribute(attName, `${attValue}`);
120 }
121 const maxIndex = Math.max(raw.length, expressions.length);
122 for (let index = 0; index < maxIndex; ++index) {
123 result.append(
124 ...nodesFromExpression.call(context, raw[index]),
125 ...nodesFromExpression.call(context, expressions[index]),
126 );
127 }
128 result.normalize();
129 return result;
130 };
131
132 /**
133 * Creates a new template tag builder with the provided attributes for
134 * for making Element’s with the `name` specified on this.
135 *
136 * As a shorthand, you can also use this function as a template tag
137 * itself, in which case it is treated as though its attributes were
138 * empty.
139 */
140 const nodeTagBuilder = function (attrsOrStrings, ...expressions) {
141 const { context, name, namespace } = this;
142 if (
143 Array.isArray(attrsOrStrings) && "raw" in attrsOrStrings &&
144 Array.isArray(attrsOrStrings.raw)
145 ) {
146 // The first argument is usable as a template string.
147 return createNode.call(
148 { context, name, namespace, attributes: {} },
149 attrsOrStrings,
150 ...expressions,
151 );
152 } else {
153 // The first argument is not a template string.
154 return createNode.bind({
155 context,
156 name,
157 namespace,
158 attributes: { ...Object(attrsOrStrings) },
159 });
160 }
161 };
162
163 /** Processes the provided expression and yields Nodes. */
164 const nodesFromExpression = function* (expression) {
165 const document = this.document ?? globalThis.document;
166 const nodePrototype = Object.getPrototypeOf(
167 Object.getPrototypeOf(document.createDocumentFragment()),
168 );
169 if (expression == null) {
170 // The expression is nullish.
171 /* do nothing */
172 } else {
173 // The expression is not nullish.
174 const expressionIsNode = (() => {
175 // Due to how D·O·M‐to‐Ecmascript bindings work, attempting to
176 // call a D·O·M method or accessor on a value which is not
177 // actually of the appropriate type will throw.
178 //
179 // This I·I·F·E uses that fact to test whether the provided
180 // expression is already a Node.
181 try { // throws unless this is a Node
182 return !!Reflect.get(nodePrototype, "nodeType", expression);
183 } catch {
184 return false;
185 }
186 })();
187 if (expressionIsNode) {
188 // The expression is already a `Node`.
189 yield expression;
190 } else if (Array.isArray(expression)) {
191 // The expression is an array of expressions.
192 for (const subexpression of expression) {
193 yield* nodesFromExpression.call(this, subexpression);
194 }
195 } else {
196 // The expression is something else.
197 yield document.createTextNode(`${expression}`);
198 }
199 }
200 };
This page took 0.060719 seconds and 5 git commands to generate.