import { xhtmlNamespace } from "./names.js";
-/**
- * Create a D·O·M Element from a tagged template.
- *
- * Usage :—
- *
- * ```js
- * Lemon("elementName", "namespace")`content`;
- * // or
- * Lemon("elementName", "namespace")({ attribute: "value" })`content`;
- * ```
- *
- * Content may, via substitutions, include additional nodes. As a
- * convenience, if the namespace is not defined, it is the X·H·T·M·L
- * namespace. As a convenience, `Lemon.elementName` is a shorthand for
- * `Lemon("element-name").` As a convenience, substitutions may take
- * the form of an array of nodes.
- */
-const Lemon = new Proxy(
- Object.defineProperties(
- Object.setPrototypeOf(
- function (name, namespace = xhtmlNamespace) {
- return Object.setPrototypeOf(
- nodeTagBuilder.bind({
- name: `${name}`,
- namespace: `${namespace}`,
- }),
- Lemon.prototype,
- );
- },
- Function,
- ),
+const {
+ /**
+ * Create a D·O·M Element from a tagged template.
+ *
+ * Usage :—
+ *
+ * ```js
+ * Lemon("elementName", "namespace")`content`;
+ * // or
+ * Lemon("elementName", "namespace")({ attr: "value" })`content`;
+ * ```
+ *
+ * Content may, via substitutions, include additional nodes. As a
+ * convenience, if the namespace is not defined, it is the X·H·T·M·L
+ * namespace. As a convenience, `Lemon.elementName` is a shorthand
+ * for `Lemon("element-name").` As a convenience, substitutions may
+ * take the form of an array of nodes.
+ *
+ * By default, `globalThis.document` is used as the owner document
+ * for new nodes. To pick a different document, supply a different
+ * `document` on the `this` value when calling. You can use `bind` to
+ * simplify this in some cases :—
+ *
+ * ```js
+ * const MyLemon = Lemon.bind({ document });
+ * MyLemon.p`words`; // same as Lemon.call({ document }, "p")`words`
+ * ```
+ */
+ Lemon,
+} = (() => {
+ const lemonProxyHandler = Object.assign(
+ Object.create(null),
{
- name: { value: "Lemon" },
- prototype: { value: Object.create(Function.prototype) },
- },
- ),
- {
- /** If `P` doesn’t exist on `O`, calls `O` with `P` instead. */
- get(O, P, Receiver) {
- if (typeof P != "string" || Reflect.has(O, P)) {
- return Reflect.get(O, P, Receiver);
- } else if (typeof O == "function") {
- return O(
- Array.from(function* () {
+ /** If `P` doesn’t exist on `O`, calls `O` with `P` instead. */
+ get(O, P, Receiver) {
+ if (typeof P != "string" || Reflect.has(O, P)) {
+ return Reflect.get(O, P, Receiver);
+ } else if (typeof O == "function") {
+ return O([...function* () {
for (const character of P) {
yield /[A-Z]/u.test(character)
? `-${character.toLowerCase()}`
: character;
}
- }()).join(""),
- );
- } else {
- return undefined;
- }
+ }()].join(""));
+ } else {
+ return undefined;
+ }
+ },
},
- },
-);
+ );
+ return {
+ Lemon: new Proxy(
+ Object.defineProperties(
+ Object.setPrototypeOf(
+ function (name, namespace = xhtmlNamespace) {
+ return Object.setPrototypeOf(
+ nodeTagBuilder.bind({
+ context: this ?? globalThis,
+ name: `${name}`,
+ namespace: `${namespace}`,
+ }),
+ Lemon.prototype,
+ );
+ },
+ Function,
+ ),
+ {
+ name: { value: "Lemon" },
+ prototype: { value: Object.create(Function.prototype) },
+ bind: {
+ value: Object.defineProperties(
+ function (thisArg, ...args) {
+ return new Proxy(
+ Function.prototype.bind.call(
+ this,
+ thisArg,
+ ...args,
+ ),
+ lemonProxyHandler,
+ );
+ },
+ { name: { value: "bind" } },
+ ),
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ },
+ },
+ ),
+ lemonProxyHandler,
+ ),
+ };
+})();
export default Lemon;
/**
* expressions.
*/
const createNode = function (strings, ...expressions) {
- const { attributes, name, namespace } = this;
const { raw } = strings;
+ const { context, name, namespace, attributes } = this;
+ const document = context.document ?? globalThis.document;
const result = document.createElementNS(namespace, name);
for (const [attName, attValue] of Object.entries(attributes)) {
result.setAttribute(attName, `${attValue}`);
}
const maxIndex = Math.max(raw.length, expressions.length);
for (let index = 0; index < maxIndex; ++index) {
- result.append(...nodesFromExpression(raw[index]));
- result.append(...nodesFromExpression(expressions[index]));
+ result.append(
+ ...nodesFromExpression.call(context, raw[index]),
+ ...nodesFromExpression.call(context, expressions[index]),
+ );
}
result.normalize();
return result;
* empty.
*/
const nodeTagBuilder = function (attrsOrStrings, ...expressions) {
- const { name, namespace } = this;
+ const { context, name, namespace } = this;
if (
Array.isArray(attrsOrStrings) && "raw" in attrsOrStrings &&
Array.isArray(attrsOrStrings.raw)
) {
// The first argument is usable as a template string.
return createNode.call(
- { attributes: {}, document, name, namespace },
+ { context, name, namespace, attributes: {} },
attrsOrStrings,
...expressions,
);
} else {
// The first argument is not a template string.
return createNode.bind({
- attributes: { ...Object(attrsOrStrings) },
- document,
+ context,
name,
namespace,
+ attributes: { ...Object(attrsOrStrings) },
});
}
};
/** Processes the provided expression and yields Nodes. */
const nodesFromExpression = function* (expression) {
+ const document = this.document ?? globalThis.document;
+ const nodePrototype = Object.getPrototypeOf(
+ Object.getPrototypeOf(document.createDocumentFragment()),
+ );
if (expression == null) {
// The expression is nullish.
/* do nothing */
} else {
// The expression is not nullish.
const expressionIsNode = (() => {
- try {
- Reflect.get(Node.prototype, "nodeType", expression);
- return true;
+ // Due to how D·O·M‐to‐Ecmascript bindings work, attempting to
+ // call a D·O·M method or accessor on a value which is not
+ // actually of the appropriate type will throw.
+ //
+ // This I·I·F·E uses that fact to test whether the provided
+ // expression is already a Node.
+ try { // throws unless this is a Node
+ return !!Reflect.get(nodePrototype, "nodeType", expression);
} catch {
return false;
}
} else if (Array.isArray(expression)) {
// The expression is an array of expressions.
for (const subexpression of expression) {
- yield* nodesFromExpression(subexpression);
+ yield* nodesFromExpression.call(this, subexpression);
}
} else {
// The expression is something else.