# The idea behind LinkeDOM More than a developer had some difficulty understanding why, or how, *linkedom* is fast, or it uses less RAM, and the quick/short answer, is that no *array* manipulation, retention, or even creation, ever happens, and all operations are basically "*a property update*" away. The purpose of this page though, is to to walk anyone interested into the idea behind *linkedom* via some explaining metaphor, but also a few working examples ... so, let's start with the metaphors! ### what is a `char`? As obvious as this question might sound, the main concept behind *linkedom* is based on how `char` and `string` differ, at least in the good old `C` world. A char is a *single quoted* representation of a letter defined by the [ASCII](https://en.wikipedia.org/wiki/ASCII#Character_set) standard, packing a whole *byte* with it, so that it can represent up to *256* values, or `0` to `255` inclusive. ```js (255).toString(2); // "11111111" a whole byte ``` ### ... and what is a `string`? Still taking `C` as reference, a *string* is an array of *char* that contains *at least* one *char* in it, which is its ending, also known as `NUL` char, which has code, and `int` value, equal to `0`. A *string* is also always double-quoted. ```C // pseudo C code and types int length(string value) { int len = 0; // note we don't check for null, we check for 0 for (int i = 0; value[i] != 0; len++) {} return len; } // strings must be double quoted string empty = ""; string full = "any"; // 0 length(empty); //3 length(full); // true empty[0] == 0; // true, true, true, true full[0] == 'a'; full[1] == 'n'; full[2] == 'y'; full[3] == 0; // some random memory/byte we should not access empty[1] == ?; full[4] == ?; ``` Accordingly, a *string* is represented by a starting point, which is the string reference itself, and an ending point, the `NUL` char with value `0`. The real length, in terms of memory allocation, of a string, is then always *its number of chars + 1*, but we don't, usually, get to ever see, or consider, its closing char. ### and how is this relevant? Even if the *DOM* is effectively seen, crawled, and manipulated as a tree, we actually represent it as a *string* all the time: ```js // is this really a tree, or just a string? const parsedAsTree = `content
`; ``` The *DOM* tree is just a representation of the markup language defined via the string, but its parsing is linear too, as "*there is no tree*" in strings. Now, similarly with the difference we have between *char* and *string*, where the latter is an extension of the first kind, the *DOM* is represented by *Node* and *Element*, where *Node* is a single entity with a single value, and *Element* is an extension that, conveniently, can also contain other *elements* in it. ```js ` content
` // Element -> Node -> Node -> Element->End End ``` Instead of a `NUL` char, each *Element* can have an *End* node that defines the boundaries of such element itself, and differently from a *string*, an *Element* can have within its boundaries other *Element* nodes (while a *string* cannot have inner strings). Representing this concept as string, let's assume an element starts as `[`, ends as `]`, an attribute is `@`, and text is `!`, so that previous markup, could be written as `"[@![]]"`: ```js " [ @ ! [ ] ]" // Element -> Node -> Node -> Element->End End ``` Still confused? Fair enough, so let's see in practice how these concepts can get applied to linked *nodes*. ## The Node Class The most basic details that a *node* should expose are: * the node `type`, so we can distinguish between different kind of nodes * the node `prev`, so we can define, or reach, the previous linked node, if any * the node `next`, so we can define, or reach, the next linked node, if any ```js class Node { static ELEMENT = 1; static ATTRIBUTE = 2; static TEXT = 3; constructor(type) { this.type = type; this.prev = null; this.next = null; } } ``` With this basic *class*, we could already define the previous *HTML* example as a *left to right* linked list: ```js const {ELEMENT, ATTRIBUTE, TEXT} = Node; // content
const tag = new Node(ELEMENT); const id = new Node(ATTRIBUTE); tag.next = id; const content = new Node(TEXT); id.next = content; const br = new Node(ELEMENT); content.next = br; // crawling the let next = tag; while (next !== null) { console.log(next.type); next = next.next; } // 1, 2, 3, 1 ``` This is a basic demonstration that, while we consider that *HTML* a tree, with an outer container and two child nodes, the *DOM* can be crawled linearly, as long as there is a *next* node to point at. - - - **Side note** this is also how *template literals tag* based libraries, such as [µhtml](https://github.com/WebReflection/uhtml#readme) or [µhtml-ssr](https://github.com/WebReflection/uhtml-ssr#readme), parse the template content, and relate interpolations to the right place: the traversing is always linear! - - - Back to the topic, since we'd like to reuse that `tag` reference, and since we want to be able to crawl anything back, we need to setup each node as *double linked list* instead. ```js const {ELEMENT, ATTRIBUTE, TEXT} = Node; const tag = new Node(ELEMENT); const id = new Node(ATTRIBUTE); tag.next = id; id.prev = tag; const content = new Node(TEXT); id.next = content; content.prev = id; const br = new Node(ELEMENT); content.next = br; br.prev = content; ``` Because we are developers, and we don't like to repeat ourselves or stress our fingers typing, here a more *DRY* approach: ```js const {ELEMENT, ATTRIBUTE, TEXT} = Node; const setAdjacent = (before, after) => { before.next = after; after.prev = before; }; const tag = new Node(ELEMENT); const id = new Node(ATTRIBUTE); setAdjacent(tag, id); const content = new Node(TEXT); setAdjacent(id, content); const br = new Node(ELEMENT); setAdjacent(content, br); ``` Great ... but something is still missing: we don't really have any special meaning for *attributes*, *text*, or *elements*, except a numeric `type` ... but that doesn't really tell us much, isn't it? Let's see how we can improve there. ## The Text Class The *text* node is like the string's *char*: it doesn't have boundaries and it represents only *one* value. ```js class Text extends Node { constructor(value = "") { super(Node.TEXT); this.value = value; } } // example const content = new Text("some content"); content.type; // 3 => Node.TEXT content.value; // "some content" ``` Just perfect: it's a *node* that carries some *text*! ## The Attribute Class Differently form *text*, *attributes* are still a single *node* that cannot contain any children, but these have also a name, beside a value. ```js class Attribute extends Node { constructor(name, value = "") { super(Node.ATTRIBUTE); this.name = name; this.value = value; } } // example const attr = new Attribute("id"); attr.name; // "id" attr.value; // "" attr.value = "unique"; ``` Wonderful! We have all child nodes we need to represent the content of an *element*, which is indeed the next class we are going to define. ## The Element Class Like *string* in *C*, *elements* need to define their own boundaries, so that each *element* should have a reference to its ending boundary, like the `NUL` char is for strings. Vice-versa, even if not strictly necessary, it's convenient to have such end boundary able to reach its starting reference, so that we don't need to crawl back all *previous* nodes each time we are walking *right to left*, instead of *left to right*, and we can skip-jump after the end, or before the start, with ease. ```js class End extends Node { constructor(start) { // explained later super(Node.ELEMENT * -1); this.start = start; } } class Element extends Node { constructor(name) { super(Node.ELEMENT); this.name = name; this.end = new End(this); // important! setAdjacent(this, this.end); } } // example const el = new Element('tag'); el.prev; // null el.next === el.end; // true el.end.prev === el; // true el.end.next; // null ``` The amount of extra operations might be overwhelming, compared to what we needed with `Text` and `Attribute`, but this structure is pretty much "*the linkedom key to success*": * the `End` `type` is recognizable as the opposite of its opening: `-1` * the `End` `type` only needs a pointer to its starting node * the `Element` `name` defines the *tag* name * the `Element` `end` defines its *right-most* boundary * both `Element` and `End` are immediately adjacent per each new *element* With these classes and basic properties, we can now fully represent, traverse, or serialize back, the initial string: ```js // reproducing // content
const tag = new Element('tag'); const id = new Attribute('id', 'value'); // place this node before the element end setAdjacent(id, tag.end); setAdjacent(tag, id); const content = new Text('content'); // place this node before the element end // and after last known node setAdjacent(content, tag.end); setAdjacent(id, content); // place this element before the element end // and after last known node const br = new Element('br'); setAdjacent(br.end, tag.end); setAdjacent(content, br); // let's write the initial output const output = []; let next = tag; let isOpen = false; while (next) { switch (next.type) { case Node.ELEMENT: if (isOpen) output.push('>'); isOpen = true; output.push('<', next.name); break; case Node.ATTRIBUTE: output.push(' ', next.name, '="', next.value, '"'); break; case Node.TEXT: if (isOpen) { isOpen = false; output.push('>'); } output.push(next.value); break; case (Node.ELEMENT * -1): if (isOpen) { isOpen = false; output.push(' />'); } else output.push(''); break; } next = next.next; } console.log(output.join('')); // content
// 🎉 ``` ## Some simplification or shortcut Technically, all the primitives that fuel *linkedom* are already in place, but few things can be simplified further. As example, dance to set a *node* between a previous *node* and another *node*, can be simplified, but we also would like to be able to remove a *node* and set its boundaries as `null`, so that `null`, as either previous or next value, should be expected: ```js const setAdjacent = (before, after) => { if (before !== null) before.next = after; if (after !== null) after.prev = before; }; const setBoundaries = (before, current, after) => { setAdjacent(before, current); // skip to the node end if needed if (current.type === Node.ELEMENT) current = current.end; setAdjacent(current, after); }; // previous example rewritten const tag = new Element('tag'); const id = new Attribute('id', 'value'); setBoundaries(tag, id, tag.end); const content = new Text('content'); setBoundaries(id, content, tag.end); const br = new Element('br'); setBoundaries(content, br, tag.end); ``` The `setBoundaries` helper can be used for pretty much any kind of operation. As example, given an *element*, we could always perform an `appendChild(node)` like this: `setBoundaries(el.end.prev, node, el.end)`, as the `el.end.prev` is always either the `el` itself, or its last node. Please note though, that the amount of helpers are more than just this one, so that `getNext(el)` or `getPrev(el)` might ensure that if `end.prev` is the ending node, the returned reference would be `end.prev.start` instead, to mimic a `previousSibling` operation. ### Shortcuts The most useful shortcut that makes it possible to avoid backward loops to find a `parentNode`, is the `parentNode` property itself. This property is `null` by default for all nodes, but it changes every time a node gets appended, or removed. Accordingly, even if seen as *yet another pointer*, for pragmatism sake, the `parentNode` will always point at the starting *element* that contains the current one, if any. This property is also useful as very *cheap* check to know if a node `isConnected` or not. For *attributes* though, the `ownerElement` is the easiest way to retrieve their parent node, and while it's not necessary to have *attributes* always linked before any *element* content, *linkedom* uses this convention so that it's easy to skip where the content starts, but also easier to create a string out of the linked list that each *element* represents. ```js const childNodes = element => { const nodeList = []; let {next, end} = element; while (next !== end) { if (next.type !== Node.ATTRIBUTE) { nodeList.push(next); if (next.type === Node.ELEMENT) next = next.end; } next = next.next; } return nodeList; }; ``` All these little helpers are super cheap, and always a *while* loop away, most of the time confined within *elements* boundaries. ## The End While everything else is literally published in this repository, so that it wouldn't make much sense to comment on everything written in here, I hope this page helped understanding how "*linkedom magic*" works, why it's linearly fast, or predictable, in terms of performance, and maybe help you recycle this idea to "*flatten out*" other *trees*, used here and there in the software industry. Last, but not least, I'll leave some utility and class here, so that'd be easier to play around with this pattern/idea 👋 ```js // Utilities const setAdjacent = (before, after) => { if (before !== null) before.next = after; if (after !== null) after.prev = before; }; const setBoundaries = (before, current, after) => { setAdjacent(before, current); if (current.type === Node.ELEMENT) current = current.end; setAdjacent(current, after); }; const toString = node => { const output = []; let isOpen = false; while (node) { switch (node.type) { case Node.ELEMENT: if (isOpen) output.push('>'); isOpen = true; output.push('<', node.name); break; case Node.ATTRIBUTE: output.push(' ', node.name, '="', node.value, '"'); break; case Node.TEXT: if (isOpen) { isOpen = false; output.push('>'); } output.push(node.value); break; case Node.END: if (isOpen) { isOpen = false; output.push(' />'); } else output.push(''); break; } node = node.next; } return output.join(''); }; const append = (element, ...nodes) => { const {end} = element; for (const node of nodes) setBoundaries(end.prev, node, end); }; // Classes class Node { static END = -1; static ELEMENT = 1; static ATTRIBUTE = 2; static TEXT = 3; constructor(type) { this.type = type; this.prev = null; this.next = null; } } class End extends Node { constructor(start) { super(Node.END); this.start = start; } } class Element extends Node { constructor(name) { super(Node.ELEMENT); this.name = name; this.end = new End(this); setAdjacent(this, this.end); } } class Attribute extends Node { constructor(name, value = "") { super(Node.ATTRIBUTE); this.name = name; this.value = value; } } class Text extends Node { constructor(value = "") { super(Node.TEXT); this.value = value; } } ```