]>
Lady’s Gitweb - Lemon/blob - mod.js
2 // ====================================================================
4 // Copyright © 2022 Lady [@ Lady’s Computer].
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/>.
10 import { xhtmlNamespace
} from "./names.js";
14 * Create a D·O·M Element from a tagged template.
19 * Lemon("elementName", "namespace")`content`;
21 * Lemon("elementName", "namespace")({ attr: "value" })`content`;
23 * Lemon("elementName", "namespace").attr("value")`content`;
26 * Content may, via substitutions, include additional nodes. As a
27 * convenience, if the namespace is not defined, it is the X·H·T·M·L
28 * namespace. As a convenience, `Lemon.elementName` is a shorthand
29 * for `Lemon("element-name").` As a convenience, substitutions may
30 * take the form of an array of nodes.
32 * By default, `globalThis.document` is used as the owner document
33 * for new nodes. To pick a different document, supply a different
34 * `document` on the `this` value when calling. You can use `bind` to
35 * simplify this in some cases :—
38 * const MyLemon = Lemon.bind({ document });
39 * MyLemon.p`words`; // same as Lemon.call({ document }, "p")`words`
44 const lemonProxyHandler
= Object
.assign(Object
.create(null), {
45 /** If `P` doesn’t exist on `O`, calls `O` with `P` instead. */
47 if (typeof P
!= "string" || Reflect
.has(O
, P
)) {
48 // P is not a string or else exists on O.
49 return Reflect
.get(O
, P
, Receiver
);
50 } else if (typeof O
== "function") {
51 // P is a string and O is a function.
52 return O(toNodeName(P
));
54 // P is a string and O is not a function.
62 Object
.defineProperties(
63 Object
.setPrototypeOf(
64 function (name
, namespace = xhtmlNamespace
) {
65 return makeLemon
.call({
66 context
: this ?? globalThis
,
68 namespace: `${namespace ?? ""}`,
69 attributes
: Object
.create(null),
75 name
: { value
: "Lemon" },
84 Object(getMemo(this).attributes
),
93 const { name
} = getMemo(this);
94 return name
.substring(name
.indexOf(":") + 1);
102 return getMemo(this).namespace || null;
110 return getMemo(this).context
?.document
??
119 return getMemo(this).name
;
129 value
: Object
.defineProperties(
130 function (thisArg
, ...args
) {
132 Function
.prototype.bind
.call(
140 { name
: { value
: "bind" } },
152 export default Lemon
;
154 /** An object with a retrievable memo value. */
155 class Memoized
extends function ($) {
161 * Remembers the provided memo as a private class feature on the
164 constructor(object
, memo
) {
169 /** Retrieves the remembered memo from this object. */
176 * Creates an Element with `name`, `namespace`, and `attributes` drawn
177 * from this object and content determined by the provided strings and
180 const createNode = function (strings
, ...expressions
) {
181 const { raw
} = strings
;
182 const { context
, name
, namespace, attributes
} = this;
183 const document
= context
?.document
?? globalThis
.document
;
184 const result
= document
.createElementNS(namespace, name
);
185 for (const [attName
, attValue
] of Object
.entries(attributes
)) {
186 result
.setAttribute(attName
, `${attValue}`);
188 const maxIndex
= Math
.max(raw
.length
, expressions
.length
);
189 for (let index
= 0; index
< maxIndex
; ++index
) {
191 ...nodesFromExpression
.call(context
, raw
[index
]),
192 ...nodesFromExpression
.call(context
, expressions
[index
]),
199 /** Retrieves the remembered memoized value from this object. */
200 const getMemo
= ($) => Reflect
.get(Memoized
.prototype, "memo", $);
202 /** Returns a new Lemon proxy function bound to this value. */
203 const makeLemon = function (attName
= null) {
204 const { attributes
: oldAttributes
, ...restOfThis
} = this;
205 const boundThis
= Object
.assign(
209 attributes
: attName
== null
210 ? Object(oldAttributes
)
211 : mergeAttributes(oldAttributes
, { [attName
]: "" }),
216 Object
.defineProperties(
217 Object
.setPrototypeOf(
218 nodeTagBuilder
.bind(boundThis
, attName
),
223 value
: attName
== null
224 ? toStartTag
.call(boundThis
)
225 : toStartTag
.call(boundThis
) +
226 ` /* pending ${attName} */`,
228 length
: { value
: 1 },
235 * If `P` doesn’t exist on `O`, returns a bound `makeLemon`
238 get(O
, P
, Receiver
) {
239 if (typeof P
!= "string" || Reflect
.has(O
, P
)) {
240 // P is not a string or else exists on O.
241 return Reflect
.get(O
, P
, Receiver
);
243 // P is a string which does not exist on O.
244 return makeLemon
.call(boundThis
, toNodeName(P
));
255 * Merges the provided attributes objects onto a new object and returns
258 * There is special handling when the attribute value is empty or when
259 * it matches the attribute name. A false attribute value deletes the
260 * attribute, while an undefined one is treated as the empty string.
262 const mergeAttributes
= (base
, others
) => {
263 const attributes
= Object
.assign(Object
.create(null), Object(base
));
264 for (const [attName
, attValue
] of Object
.entries(others
)) {
265 if (attValue
=== false) {
266 delete attributes
[attName
];
268 const value
= attValue
=== true || attValue
=== undefined
271 if (attName
in attributes
) {
272 const oldValue
= attributes
[attName
];
273 if (value
== "" || value
== attName
&& value
== oldValue
) {
276 attributes
[attName
] = oldValue
277 ? `${oldValue} ${value}`
281 attributes
[attName
] = value
;
289 * Returns a new template tag builder with the provided attributes for
290 * for making Element’s with the `name` specified on this.
292 * As a shorthand, you can also use this function as a template tag
293 * itself, in which case it is treated as though its attributes were
296 const nodeTagBuilder = function (
301 const { attributes
: oldAttributes
, ...restOfThis
} = this;
302 if (Object(attrsOrStrings
) !== attrsOrStrings
) {
303 // attrsOrStrings is a primitive.
304 if (pending
!= null) {
305 return makeLemon
.call({
307 attributes
: mergeAttributes(oldAttributes
, {
308 [pending
]: attrsOrStrings
,
313 "Cannot set attribute value when no name is pending.",
317 Array
.isArray(attrsOrStrings
) && "raw" in attrsOrStrings
&&
318 Array
.isArray(attrsOrStrings
.raw
)
320 // attrsOrStrings is usable as a template string.
321 return createNode
.call(
322 { ...restOfThis
, attributes
: Object(oldAttributes
) },
327 // attrsOrStrings is not a template string.
328 return makeLemon
.call({
330 attributes
: mergeAttributes(oldAttributes
, attrsOrStrings
),
335 /** Processes the provided expression and yields Nodes. */
336 const nodesFromExpression
= function* (expression
) {
337 const document
= this?.document
?? globalThis
.document
;
338 const nodePrototype
= Object
.getPrototypeOf(
339 Object
.getPrototypeOf(document
.createDocumentFragment()),
341 if (expression
== null) {
342 // The expression is nullish.
345 // The expression is not nullish.
346 const expressionIsNode
= (() => {
347 // Due to how D·O·M‐to‐Ecmascript bindings work, attempting to
348 // call a D·O·M method or accessor on a value which is not
349 // actually of the appropriate type will throw.
351 // This I·I·F·E uses that fact to test whether the provided
352 // expression is already a Node.
353 try { // throws unless this is a Node
354 return !!Reflect
.get(nodePrototype
, "nodeType", expression
);
359 if (expressionIsNode
) {
360 // The expression is already a `Node`.
362 } else if (Array
.isArray(expression
)) {
363 // The expression is an array of expressions.
364 for (const subexpression
of expression
) {
365 yield* nodesFromExpression
.call(this, subexpression
);
368 // The expression is something else.
369 yield document
.createTextNode(`${expression}`);
375 * Returns the provided value escaped such that it is usable in a
376 * double‐quoted attribute value string.
378 const toAttributeValue
= (value
) =>
379 `${value}`.replaceAll(/[\x22\x26\x3C\x3E]/gu, ($) => {
392 /** Returns an X·M·L start tag derived from this.*/
393 const toStartTag = function () {
394 const name
= `${this.name}`;
395 const namespace = `${this.namespace ?? ""}`;
396 const attributes
= Object(this.attributes
);
401 // A namespace was specified.
402 const colonIndex
= name
.indexOf(":");
403 if (colonIndex
!= -1) {
404 // The node name has a prefix.
405 yield ` xmlns:${name.substring(0, colonIndex)}="${
406 toAttributeValue(namespace)
409 // The node name is unprefixed.
410 yield ` xmlns="${toAttributeValue(namespace)}"`;
413 // No namespace was specified.
416 for (const [attName
, attValue
] of Object
.entries(attributes
)) {
417 // Iterate over attributes and serialize them.
418 yield ` ${attName}="${toAttributeValue(attValue)}"`;
426 * Returns a node name derived from the provided Ecmascript property
427 * name by lowercasing uppercase ascii letters and prefixing them with
430 const toNodeName
= (ecmascriptName
) =>
433 for (const character
of ecmascriptName
) {
434 yield /[A-Z]/u.test(character
)
435 ? `-${character.toLowerCase()}`
This page took 0.086314 seconds and 5 git commands to generate.