Skip to content

Commit

Permalink
Work in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
WebReflection committed Dec 22, 2023
1 parent 77bbacf commit d036b4d
Show file tree
Hide file tree
Showing 16 changed files with 484 additions and 13 deletions.
12 changes: 9 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,14 @@ node_modules/
types/
cjs/*
!cjs/package.json
index.js
esm/init.js
init.js
keyed.js
!esm/keyed.js
!esm/dom/keyed.js
index.js
!esm/index.js
!esm/dom/index.js
node.js
init.js
esm/init.js
!esm/node.js
!esm/dom/node.js
38 changes: 38 additions & 0 deletions esm/dom/attribute.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ATTRIBUTE_NODE } from 'domconstants/constants';

import { escape } from 'html-escaper';

import Node from './node.js';

import { name, value, ownerElement, ownerDocument } from './symbols.js';

export default class Attribute extends Node {
constructor(nodeName, nodeValue = '', owner = null) {
super(ATTRIBUTE_NODE, owner?.[ownerDocument]);
this[ownerElement] = owner;
this[name] = nodeName;
this.value = nodeValue;
}
get name() {
return this[name];
}
get localName() {
return this[name];
}
get nodeName() {
return this[name];
}
get value() {
return this[value];
}
set value(any) {
this[value] = String(any);
}
get nodeValue() {
return this[value];
}
toString() {
const { [name]: key, [value]: val } = this;
return val === '' ? key : `${key}="${escape(val)}"`;
}
}
24 changes: 24 additions & 0 deletions esm/dom/character-data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import Node from './node.js';
import { nodeName, value } from './symbols.js';

export default class CharacterData extends Node {
constructor(type, name, data, owner) {
super(type, owner)[nodeName] = name;
this.data = data;
}
get data() {
return this[value];
}
set data(any) {
this[value] = String(any);
}
get nodeName() {
return this[nodeName];
}
get textContent() {
return this.data;
}
set textContent(data) {
this.data = data;
}
}
13 changes: 13 additions & 0 deletions esm/dom/comment.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { COMMENT_NODE } from 'domconstants/constants';

import CharacterData from './character-data.js';
import { value } from './symbols.js';

export default class Comment extends CharacterData {
constructor(data = '', owner = null) {
super(COMMENT_NODE, '#comment', data, owner);
}
toString() {
return `<!--${this[value]}-->`;
}
}
19 changes: 19 additions & 0 deletions esm/dom/document-type.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { DOCUMENT_TYPE_NODE } from 'domconstants/constants';

import Node from './node.js';
import { nodeName } from './symbols.js';

export default class DocumentType extends Node {
constructor(name, owner = null) {
super(DOCUMENT_TYPE_NODE, owner)[nodeName] = name;
}
get nodeName() {
return this[nodeName];
}
get name() {
return this[nodeName];
}
toString() {
return `<!DOCTYPE ${this[nodeName]}>`;
}
}
62 changes: 62 additions & 0 deletions esm/dom/document.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { DOCUMENT_NODE } from 'domconstants/constants';

import Element from './element.js';
import Attribute from './attribute.js';
import Text from './text.js';
import Comment from './text.js';
import DocumentType from './document-type.js';
import Parent from './parent.js';
import { setParentNode } from './utils.js';

import { childNodes, nodeName, ownerDocument } from './symbols.js';

const doctype = Symbol();
const documentElement = Symbol();
const head = Symbol();
const body = Symbol();

export default class Document extends Parent {
constructor(type = 'html') {
super(DOCUMENT_NODE, null)[nodeName] = '#document';
this[doctype] = setParentNode(new DocumentType(type), this);
this[documentElement] = setParentNode(new Element('html', this), this);
this[head] = setParentNode(new Element('head', this), this[documentElement]);
this[body] = setParentNode(new Element('body', this), this[documentElement]);
this[childNodes] = type ?
[this[doctype], this[documentElement]] :
[this[documentElement]]
;
this[documentElement][childNodes] = [this[head], this[body]];
}
get doctype() {
return this[doctype];
}
get documentElement() {
return this[documentElement];
}
get head() {
return this[head];
}
get body() {
return this[body];
}
createAttribute(name) {
const attribute = new Attribute(name);
attribute[ownerDocument] = this;
return attribute;
}
createElement(name, options = null) {
const element = new Element(name, this);
if (options?.is) element.setAttribute('is', options.is);
return element;
}
createComment(data) {
return new Comment(data, this);
}
createTextNode(data) {
return new Text(data, this);
}
toString() {
return this[childNodes].join('');
}
}
73 changes: 73 additions & 0 deletions esm/dom/element.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { ELEMENT_NODE } from 'domconstants/constants';
import { VOID_ELEMENTS } from 'domconstants/re';

import Attribute from './attribute.js';
import Parent from './parent.js';

import namedNodeMap from './named-node-map.js';

import { attributes, localName, childNodes, nodeType, ownerElement, parentNode } from './symbols.js';

const upperName = ({ [localName]: name }) => name.toUpperCase();

const getAttributes = element => (
element[attributes] || (element[attributes] = new Map)
);

export default class Element extends Parent {
constructor(name, owner = null) {
super(ELEMENT_NODE, owner)[localName] = name;
this[attributes] = null;
}
get attributes() {
return namedNodeMap(getAttributes(this));
}
get nodeName() {
return this[localName].toUpperCase();
}
get tagName() {
return this[localName].toUpperCase();
}
get innerHTML() {
return this[childNodes].join('');
}
get outerHTML() {
return this.toString();
}
getAttribute(name) {
const attribute = this[attributes]?.get(name);
return attribute ? attribute.value : null;
}
hasAttribute(name) {
return !!this[attributes]?.has(name);
}
setAttribute(name, value) {
const attributes = getAttributes(this);
const attribute = attributes.get(name);
if (attribute)
attribute.value = value;
else {
const attribute = new Attribute(name, value, this);
attributes.set(name, attribute);
}
}
removeAttribute(name) {
const attribute = this[attributes]?.get(name);
if (attribute) {
attribute[ownerElement] = null;
this[attributes].delete(name);
}
}
toString() {
const { [localName]: name, [childNodes]: nodes, [attributes]: attrs } = this;
const html = ['<', name];
if (attrs?.size) {
for (const attribute of attrs.values())
html.push(' ', attribute);
}
html.push('>', ...nodes);
if (!VOID_ELEMENTS.test(name))
html.push('</', name, '>');
return html.join('');
}
}
16 changes: 16 additions & 0 deletions esm/dom/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Document from './document.js';

export { Document };

const document = new Document;
const element = document.createElement('test', { is: 'some-test' });
console.log(element.isConnected, 'connected');
document.body.appendChild(element);
console.log(element.isConnected, 'connected');
element.setAttribute('hidden', '');
console.log(element.attributes.hidden.isConnected, 'connected');
const p = element.appendChild(document.createElement('p'));
p.appendChild(document.createTextNode('Hello & World'));
const s = element.appendChild(document.createElement('script'));
s.appendChild(document.createTextNode('alert("Hello & World")'));
console.log(document.toString());
36 changes: 36 additions & 0 deletions esm/dom/named-node-map.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const { from } = Array;
const { iterator } = Symbol;

const asString = (_, i) => String(i);
const isIndex = ({ size }, name) => /^\d+$/.test(name) && name < size;

const namedNodeMapHandler = {
get: (map, name) => {
if (name === 'length') return map.size;
if (name === iterator) return yieldAttributes.bind(map.values());
return map.get(name) || (
isIndex(map, name) ?
[...map.values()][name] :
void 0
);
},

has: (map, name) => (
name === 'length' ||
name === iterator ||
map.has(name) ||
isIndex(map, name)
),

ownKeys: map => [
...from({ length: map.size }, asString),
...map.keys(),
],
};

function* yieldAttributes() {
for (const attribute of this)
yield attribute;
}

export default attributes => new Proxy(attributes, namedNodeMapHandler);
49 changes: 49 additions & 0 deletions esm/dom/node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
ELEMENT_NODE,
ATTRIBUTE_NODE,
TEXT_NODE,
COMMENT_NODE,
DOCUMENT_NODE,
DOCUMENT_FRAGMENT_NODE,
} from 'domconstants/constants';

import { nodeType, ownerDocument, parentNode } from './symbols.js';
import { changeParentNode } from './utils.js';

export default class Node {
static {
this.ELEMENT_NODE = ELEMENT_NODE;
this.ATTRIBUTE_NODE = ATTRIBUTE_NODE;
this.TEXT_NODE = TEXT_NODE;
this.COMMENT_NODE = COMMENT_NODE;
this.DOCUMENT_NODE = DOCUMENT_NODE;
this.DOCUMENT_FRAGMENT_NODE = DOCUMENT_FRAGMENT_NODE;
}
constructor(type, owner) {
this[parentNode] = null;
this[nodeType] = type;
this[ownerDocument] = owner;
}
get parentNode() {
return this[parentNode];
}
get nodeType() {
return this[nodeType];
}
get ownerDocument() {
return this[ownerDocument];
}
get isConnected() {
let { [parentNode]: parent, [ownerDocument]: owner } = this;
while (parent && parent !== owner)
parent = parent[parentNode];
return parent === owner;
}
get parentElement() {
const { [parentNode]: parent } = this;
return parent?.[nodeType] === ELEMENT_NODE ? parent : null;
}
remove() {
changeParentNode(this, null);
}
}
Loading

0 comments on commit d036b4d

Please sign in to comment.