// 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";
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.
// 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({
},
});
+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: () => {
},
});
+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: () => {
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 ");
--- /dev/null
+// 🍋🏷 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";
--- /dev/null
+// 🍋🏷 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;
+ }
+ }
+ };
+}