]> Lady’s Gitweb - Lemon/commitdiff
Allow document specification and binding 0.1.0
authorLady <redacted>
Sat, 6 Aug 2022 21:10:08 +0000 (14:10 -0700)
committerLady <redacted>
Sat, 29 Apr 2023 03:36:31 +0000 (20:36 -0700)
The document for created nodes is now taken from the `document`
property on `this`. A `bind` method is added to `Lemon` to allow for
binding of context without losing the proxy behaviours of the original.

Testing dependencies were refactored into a `./window` module and the
dependency on a `globalThis.Node` object was removed.

dev-deps.js
mod.js
mod.test.js
window/deps.js [new file with mode: 0644]
window/mod.js [new file with mode: 0644]

index 9a670e71dfd578ae2ea8a7cc52e24b7154ae252c..95bf219777ebc6682b91dc4e693d5608646b734c 100644 (file)
@@ -7,95 +7,8 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
 
-import { xhtmlNamespace } from "./names.js";
-import {
-  DOMImplementation,
-  Node,
-} from "https://esm.sh/@xmldom/xmldom@0.8.2/lib/dom.js";
+import "./window/mod.js";
 
-{ // Polyfill document.
-  globalThis.document = new DOMImplementation().createDocument(
-    xhtmlNamespace,
-    "html",
-    null,
-  );
-}
-
-{ // Polyfill `element.append()`.
-  Object.getPrototypeOf(
-    globalThis.document.documentElement,
-  ).append = function (...children) {
-    for (const child of children) {
-      this.appendChild(
-        typeof child === "string"
-          ? this.ownerDocument.createTextNode(child)
-          : child,
-      );
-    }
-  };
-}
-
-{ // Polyfill `Node` and apply patches to `Node.nodeType` and
-  // `Node.normalize`.
-  globalThis.Node = Node;
-  const originalNodeTypeGetter = Object.getOwnPropertyDescriptor(
-    Node.prototype,
-    "nodeType",
-  )?.get ?? (() => {
-    const originalNodeTypeValue = Node.prototype.nodeType;
-    return () => originalNodeTypeValue;
-  })();
-  Object.defineProperty(Node.prototype, "nodeType", {
-    configurable: true,
-    enumerable: true,
-    /**
-     * To simulate restrictions on calling `nodeType` only on actual
-     * Nodes, check for inheritance first.
-     */
-    get() {
-      if (!(this instanceof Node)) {
-        // This is not a Node.
-        throw new TypeError("nodeType requires this be a Node.");
-      } else {
-        // This is a Node; walk the prototype chain and attempt to get
-        // the node type.
-        for (
-          //deno-lint-ignore no-this-alias
-          let target = this;
-          target != null && target != Node.prototype;
-          target = Object.getPrototypeOf(target)
-        ) {
-          if (Object.hasOwn(target, "nodeType")) {
-            return Reflect.get(target, "nodeType", this);
-          } else {
-            continue;
-          }
-        }
-        return originalNodeTypeGetter.call(this);
-      }
-    },
-    set: undefined,
-  });
-  Node.prototype.normalize = function () { // modified from xmldom
-    let child = this.firstChild;
-    while (child) {
-      const next = child.nextSibling;
-      if (
-        next && next.nodeType == Node.TEXT_NODE &&
-        child.nodeType == Node.TEXT_NODE
-      ) {
-        this.removeChild(next);
-        child.appendData(next.data);
-      } else {
-        if (child.nodeType == Node.TEXT_NODE && !child.data) {
-          this.removeChild(child);
-        } else {
-          child.normalize();
-        }
-        child = next;
-      }
-    }
-  };
-}
+export { DOMImplementation } from "./window/deps.js";
 
 export { assertStrictEquals } from "https://deno.land/std@0.134.0/testing/asserts.ts";
diff --git a/mod.js b/mod.js
index 51acb773424c3b1d4da29c90690d7f8541ed2c70..e6913e30539aff7a0f1ec87db1bc26901288331e 100644 (file)
--- a/mod.js
+++ b/mod.js
 
 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;
 
 /**
@@ -74,16 +111,19 @@ 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;
@@ -98,39 +138,48 @@ const createNode = function (strings, ...expressions) {
  * 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;
       }
@@ -141,7 +190,7 @@ const nodesFromExpression = function* (expression) {
     } 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.
index 1c950ab19b47075b49c9a57186a3414c99d14ef9..8af8f73901c351b89b97423a951e0199f99d1355 100644 (file)
@@ -7,7 +7,7 @@
 // License, v. 2.0. If a copy of the MPL was not distributed with this
 // file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
 
-import { assertStrictEquals } from "./dev-deps.js";
+import { assertStrictEquals, DOMImplementation } from "./dev-deps.js";
 import Lemon from "./mod.js";
 
 Deno.test({
@@ -19,6 +19,20 @@ Deno.test({
   },
 });
 
+Deno.test({
+  name:
+    "Lemon can be used to make a new element in a specified document.",
+  fn: () => {
+    const doc = new DOMImplementation().createDocument(
+      null,
+      "福",
+      null,
+    );
+    const elt = Lemon.call({ document: doc }, "xxx")({})``;
+    assertStrictEquals(elt.ownerDocument, doc);
+  },
+});
+
 Deno.test({
   name: "Lemon uses X·H·T·M·L as the default namespace.",
   fn: () => {
@@ -58,6 +72,22 @@ Deno.test({
   },
 });
 
+Deno.test({
+  name: "Bound Lemons are also proxied.",
+  fn: () => {
+    const doc = new DOMImplementation().createDocument(
+      null,
+      "福",
+      null,
+    );
+    const myLemon = Lemon.bind({ document: doc });
+    const elt = myLemon.creamPie({})``;
+    assertStrictEquals(typeof elt, "object");
+    assertStrictEquals(elt.localName, "cream-pie");
+    assertStrictEquals(elt.ownerDocument, doc);
+  },
+});
+
 Deno.test({
   name: "Lemon tags can be destructured.",
   fn: () => {
@@ -90,8 +120,9 @@ Deno.test({
 Deno.test({
   name: "Lemon tags support all kinds of expression.",
   fn: () => {
-    const elt = Lemon.xxx({})` a ${null} b ${Lemon.creamPie({})
-      `\t`}${" c "}`;
+    const elt = Lemon.xxx({})` a ${null} b ${Lemon.creamPie(
+      {},
+    )`\t`}${" c "}`;
     assertStrictEquals(elt.childNodes.length, 3);
     assertStrictEquals(elt.childNodes[0].nodeType, 3);
     assertStrictEquals(elt.childNodes[0].textContent, " a  b ");
diff --git a/window/deps.js b/window/deps.js
new file mode 100644 (file)
index 0000000..f1d4237
--- /dev/null
@@ -0,0 +1,14 @@
+// 🍋🏷 Lemon ∷ window/deps.js
+// ====================================================================
+//
+// Copyright © 2022 Lady [@ Lady’s Computer].
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+
+export {
+  DOMImplementation,
+  Node,
+} from "https://esm.sh/@xmldom/xmldom@0.8.2/lib/dom.js";
+export { xhtmlNamespace } from "../names.js";
diff --git a/window/mod.js b/window/mod.js
new file mode 100644 (file)
index 0000000..5e97d4f
--- /dev/null
@@ -0,0 +1,101 @@
+// 🍋🏷 Lemon ∷ window/mod.js
+// ====================================================================
+//
+// Copyright © 2022 Lady [@ Lady’s Computer].
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at <https://mozilla.org/MPL/2.0/>.
+
+import { DOMImplementation, Node, xhtmlNamespace } from "./deps.js";
+
+{ // Polyfill document.
+  globalThis.document = new DOMImplementation().createDocument(
+    xhtmlNamespace,
+    "html",
+    null,
+  );
+  const { documentElement } = document;
+  documentElement.appendChild(
+    document.createElementNS(xhtmlNamespace, "head"),
+  );
+  documentElement.appendChild(
+    document.createElementNS(xhtmlNamespace, "body"),
+  );
+}
+
+{ // Polyfill `element.append`.
+  Object.getPrototypeOf(
+    globalThis.document.documentElement,
+  ).append = function (...children) {
+    for (const child of children) {
+      this.appendChild(
+        typeof child === "string"
+          ? this.ownerDocument.createTextNode(child)
+          : child,
+      );
+    }
+  };
+}
+
+{ // Apply patches to `Node.nodeType` and `Node.normalize`.
+  const { TEXT_NODE, prototype: nodePrototype } = Node;
+  const originalNodeTypeGetter = Object.getOwnPropertyDescriptor(
+    nodePrototype,
+    "nodeType",
+  )?.get ?? (() => {
+    const originalNodeTypeValue = nodePrototype.nodeType;
+    return () => originalNodeTypeValue;
+  })();
+  Object.defineProperty(nodePrototype, "nodeType", {
+    configurable: true,
+    enumerable: true,
+    /**
+     * To simulate restrictions on calling `nodeType` only on actual
+     * Nodes, check for inheritance first.
+     */
+    get() {
+      if (!(this instanceof Node)) {
+        // This is not a Node.
+        throw new TypeError("nodeType requires this be a Node.");
+      } else {
+        // This is a Node; walk the prototype chain and attempt to get
+        // the node type.
+        for (
+          //deno-lint-ignore no-this-alias
+          let target = this;
+          target != null && target != nodePrototype;
+          target = Object.getPrototypeOf(target)
+        ) {
+          if (Object.hasOwn(target, "nodeType")) {
+            return Reflect.get(target, "nodeType", this);
+          } else {
+            continue;
+          }
+        }
+        return originalNodeTypeGetter.call(this);
+      }
+    },
+    set: undefined,
+  });
+  nodePrototype.normalize = function () {
+    let child = this.firstChild;
+    while (child) {
+      const next = child.nextSibling;
+      if (
+        next && next.nodeType == TEXT_NODE &&
+        child.nodeType == TEXT_NODE
+      ) {
+        this.removeChild(next);
+        child.appendData(next.data);
+      } else {
+        if (child.nodeType == TEXT_NODE && !child.data) {
+          this.removeChild(child);
+        } else {
+          child.normalize();
+        }
+        child = next;
+      }
+    }
+  };
+}
This page took 0.038398 seconds and 4 git commands to generate.