{"id":13724456,"url":"https://github.com/WebReflection/linkedom","last_synced_at":"2025-05-07T18:32:18.635Z","repository":{"id":36950816,"uuid":"321113412","full_name":"WebReflection/linkedom","owner":"WebReflection","description":"A triple-linked lists based DOM implementation.","archived":false,"fork":false,"pushed_at":"2024-10-22T10:48:44.000Z","size":3506,"stargazers_count":1690,"open_issues_count":32,"forks_count":83,"subscribers_count":12,"default_branch":"main","last_synced_at":"2024-11-08T17:52:42.371Z","etag":null,"topics":["dom","ssr","web"],"latest_commit_sha":null,"homepage":"https://webreflection.medium.com/linkedom-a-jsdom-alternative-53dd8f699311","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"isc","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/WebReflection.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-12-13T16:51:36.000Z","updated_at":"2024-11-08T17:47:44.000Z","dependencies_parsed_at":"2023-01-17T07:46:27.201Z","dependency_job_id":"40778c67-21ab-45b0-a436-06c8fa77ebde","html_url":"https://github.com/WebReflection/linkedom","commit_stats":{"total_commits":510,"total_committers":35,"mean_commits":"14.571428571428571","dds":0.09019607843137256,"last_synced_commit":"18c8571499652ea737a88f63e35dc88263d487e1"},"previous_names":[],"tags_count":208,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WebReflection%2Flinkedom","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WebReflection%2Flinkedom/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WebReflection%2Flinkedom/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/WebReflection%2Flinkedom/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/WebReflection","download_url":"https://codeload.github.com/WebReflection/linkedom/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":224636536,"owners_count":17344564,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["dom","ssr","web"],"created_at":"2024-08-03T01:01:57.496Z","updated_at":"2024-11-14T14:30:56.784Z","avatar_url":"https://github.com/WebReflection.png","language":"HTML","readme":"# 🔗 linkedom\n\n[![Downloads](https://img.shields.io/npm/dm/linkedom.svg)](https://www.npmjs.com/package/linkedom) [![Build Status](https://travis-ci.com/WebReflection/linkedom.svg?branch=main)](https://travis-ci.com/WebReflection/linkedom) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/linkedom/badge.svg?branch=main)](https://coveralls.io/github/WebReflection/linkedom?branch=main)\n\n\u003csup\u003e**Social Media Photo by [JJ Ying](https://unsplash.com/@jjying) on [Unsplash](https://unsplash.com/)**\u003c/sup\u003e\n\n### This is not a crawler!\n\nLinkeDOM is a [triple-linked list](#data-structure) based DOM-like namespace, for DOM-less environments, with the following goals:\n\n  * **avoid** maximum callstack/recursion or **crashes**, even under heaviest conditions.\n  * guarantee **linear performance** from small to big documents.\n  * be **close to the** current **DOM standard**, but [not too close](https://github.com/WebReflection/linkedom#faq).\n\n```js\nimport {DOMParser, parseHTML} from 'linkedom';\n\n// Standard way: text/html, text/xml, image/svg+xml, etc...\n// const document = (new DOMParser).parseFromString(html, 'text/html');\n\n// Simplified way for HTML\nconst {\n  // note, these are *not* globals\n  window, document, customElements,\n  HTMLElement,\n  Event, CustomEvent\n  // other exports ..\n} = parseHTML(`\n  \u003c!doctype html\u003e\n  \u003chtml lang=\"en\"\u003e\n    \u003chead\u003e\n      \u003ctitle\u003eHello SSR\u003c/title\u003e\n    \u003c/head\u003e\n    \u003cbody\u003e\n      \u003cform\u003e\n        \u003cinput name=\"user\"\u003e\n        \u003cbutton\u003e\n          Submit\n        \u003c/button\u003e\n      \u003c/form\u003e\n    \u003c/body\u003e\n  \u003c/html\u003e\n`);\n\n// builtin extends compatible too 👍\ncustomElements.define('custom-element', class extends HTMLElement {\n  connectedCallback() {\n    console.log('it works 🥳');\n  }\n});\n\ndocument.body.appendChild(\n  document.createElement('custom-element')\n);\n\ndocument.toString();\n// the SSR ready document\n\ndocument.querySelectorAll('form, input[name], button');\n// the NodeList of elements\n// CSS Selector via CSSselect\n```\n\n### What's New\n\n  * in `v0.11` a new `linkedom/worker` export has been added. This works with [deno](https://deno.land/), Web, and Service Workers, and it's not strictly coupled with NodeJS. Please note, this export does not include `canvas` module, and the `performance` is retrieved from the `globalThis` context.\n\n### Serializing as JSON\n\n*LinkeDOM* uses a blazing fast [JSDON serializer](https://github.com/WebReflection/jsdon#readme), and nodes, as well as whole documents, can be retrieved back via `parseJSON(value)`.\n\n```js\n// any node can be serialized\nconst array = toJSON(document);\n\n// somewhere else ...\nimport {parseJSON} from 'linkedom';\n\nconst document = parseJSON(array);\n```\n\nPlease note that *Custom Elements* won't be upgraded, unless the resulting nodes are imported via `document.importNode(nodeOrFragment, true)`.\n\nAlternatively, `JSDON.fromJSON(array, document)` is able to initialize right away *Custom Elements* associated with the passed `document`.\n\n\n### Simulating JSDOM Bootstrap\n\nThis module is based on [DOMParser](https://developer.mozilla.org/en-US/docs/Web/API/DOMParser) API, hence it creates a *new* `document` each time `new DOMParser().parseFromString(...)` is invoked.\n\nAs there's *no global pollution* whatsoever, to retrieve classes and features associated to the `document` returned by `parseFromString`, you need to access its `defaultView` property, which is a special proxy that lets you get *pseudo-global-but-not-global* properties and classes.\n\nAlternatively, you can use the `parseHTML` utility which returns a pseudo *window* object with all the public references you need.\n\n```js\n// facade to a generic JSDOM bootstrap\nimport {parseHTML} from 'linkedom';\nfunction JSDOM(html) { return parseHTML(html); }\n\n// now you can do the same as you would with JSDOM\nconst {document, window} = new JSDOM('\u003ch1\u003eHello LinkeDOM 👋\u003c/h1\u003e');\n```\n\n\n## Data Structure\n\nThe triple-linked list data structure is explained below in [How does it work?](#how-does-it-work), the [Deep Dive](./deep-dive.md), and the [presentation on Speakeasy JS](https://www.youtube.com/watch?v=PEESaD7Qkxs).\n\n\n## F.A.Q.\n\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eWhy \"not too close\"?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\n*LinkeDOM* has zero intention to:\n\n  * implement all things *JSDOM* already implemented. If you need a library which goal is to be 100% standard compliant, please [use JSDOM](https://github.com/jsdom/jsdom) because *LinkeDOM* doesn't want to be neirly as bloated nor as slow as *JSDOM* is\n  * implement features not interesting for *Server Side Rendering*. If you need to pretend your NodeJS, Worker, or any other environment, is a browser, please [use JSDOM](https://github.com/jsdom/jsdom)\n  * other points listed, or not, in the followung *F.A.Q.s*: this project will always prefer the minimal/fast approach over 100% compliant behavior. Again, if you are looking for 100% compliant behavior and you are not willing to have any compromise in the DOM, this is **not** the project you are looking for\n\nThat's it, the rule of thumb is: do I want to be able to render anything, and as fast as possible, in a DOM-less env? *LinkeDOM* is great!\n\nDo I need a 100% spec compliant env that simulate a browser? I rather use *cypress* or *JSDOM* then, as *LinkeDOM* is not meant to be a replacement for neither projects.\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eAre live collections supported?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\nThe *TL;DR* answer is **no**. Live collections are considered legacy, are slower, have side effects, and it's not intention of *LinkeDOM* to support these, including:\n\n  * `getElementsByTagName` does not update when nodes are added or removed\n  * `getElementsByClassName` does not update when nodes are added or removed\n  * `childNodes`, if trapped once, does not update when nodes are added or removed\n  * `children`, if trapped once, does not update when nodes are added or removed\n  * `attributes`, if trapped once, does not update when attributes are added or removed\n  * `document.all`, if trapped once, does not update when attributes are added or removed\n\nIf any code you are dealing with does something like this:\n\n```js\nconst {children} = element;\nwhile (children.length)\n  target.appendChild(children[0]);\n```\n\nit will cause an infinite loop, as the `children` reference won't side-effect when nodes are moved.\n\nYou can solve this in various ways though:\n\n```js\n// the modern approach (suggested)\ntarget.append(...element.children);\n\n// the check for firstElement/Child approach (good enough)\nwhile (element.firstChild)\n  target.appendChild(element.firstChild);\n\n// the convert to array approach (slow but OK)\nconst list = [].slice.call(element.children);\nwhile (list.length)\n  target.appendChild(list.shift());\n\n// the zero trap approach (inefficient)\nwhile (element.childNodes.length)\n  target.appendChild(element.childNodes[0]);\n```\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\n\u003cdetails\u003e\n  \u003csummary\u003e\u003cstrong\u003eAre childNodes and children always same?\u003c/strong\u003e\u003c/summary\u003e\n  \u003cdiv\u003e\n\n**Nope**, these are discovered each time, so when heavy usage of these *lists* is needed, but no mutation is meant, just trap these once and use these like a frozen array.\n\n```js\nfunction eachChildNode({childNodes}, callback) {\n  for (const child of childNodes) {\n    callback(child);\n    if (child.nodeType === child.ELEMENT_NODE)\n      eachChildNode(child, callback);\n  }\n}\n\neachChildNode(document, console.log);\n```\n\n  \u003c/div\u003e\n\u003c/details\u003e\n\n\n\n## How does it work?\n\nAll nodes are linked on both sides, and all elements consist of 2 nodes, also linked in between.\n\nAttributes are always at the beginning of an element, while zero or more extra nodes can be found before the end.\n\nA fragment is a special element without boundaries, or parent node.\n\n```\nNode:             ← node →\nAttr\u003cNode\u003e:       ← attr →          ↑ ownerElement?\nText\u003cNode\u003e:       ← text →          ↑ parentNode?\nComment\u003cNode\u003e:    ← comment →       ↑ parentNode?\nElement\u003cNode\u003e:    ← start ↔ end →   ↑ parentNode?\n\nFragment\u003cElement\u003e:  start ↔ end\n\nElement example:\n\n        parentNode? (as shortcut for a linked list of previous nodes)\n            ↑\n            ├────────────────────────────────────────────┐\n            │                                            ↓\n  node? ← start → attr* → text* → comment* → element* → end → node?\n            ↑                                            │\n            └────────────────────────────────────────────┘\n\n\nFragment example:\n\n            ┌────────────────────────────────────────────┐\n            │                                            ↓\n          start → attr* → text* → comment* → element* → end\n            ↑                                            │\n            └────────────────────────────────────────────┘\n```\n\nIf this is not clear, feel free to **[read more in the deep dive page](./deep-dive.md)**.\n\n\n### Why is this better?\n\nMoving *N* nodes from a container, being it either an *Element* or a *Fragment*, requires the following steps:\n\n  * update the first *left* link of the moved segment\n  * update the last *right* link of the moved segment\n  * connect the *left* side, if any, of the moved node at the beginning of the segment, with the *right* side, if any, of the node at the end of such segment\n  * update the *parentNode* of the segment to either *null*, or the new *parentNode*\n\nAs result, there are no array operations, and no memory operations, and everything is kept in sync by updating a few properties, so that removing `3714` sparse `\u003cdiv\u003e` elements in a *12M* document, as example, takes as little as *3ms*, while appending a whole fragment takes close to *0ms*.\n\nTry `npm run benchmark:html` to see it yourself.\n\nThis structure also allows programs to avoid issues such as \"*Maximum call stack size exceeded*\" \u003csup\u003e\u003csub\u003e(basicHTML)\u003c/sub\u003e\u003c/sup\u003e, or \"*JavaScript heap out of memory*\" crashes \u003csup\u003e\u003csub\u003e(JSDOM)\u003c/sub\u003e\u003c/sup\u003e, thanks to its reduced usage of memory and zero stacks involved, hence scaling better from small to very big documents.\n\n### Are *childNodes* and *children* always computed?\n\nAs everything is a `while(...)` loop away, by default this module does not cache anything, specially because caching requires state invalidation for each container, returned queries, and so on. However, you can import `linkedom/cached` instead, as long as you [understand its constraints](https://github.com/WebReflection/linkedom#cached-vs-not-cached).\n\n\n## Parsing VS Node Types\n\nThis module parses, and works, only with the following `nodeType`:\n\n  * `ELEMENT_NODE`\n  * `ATTRIBUTE_NODE`\n  * `TEXT_NODE`\n  * `COMMENT_NODE`\n  * `DOCUMENT_NODE`\n  * `DOCUMENT_FRAGMENT_NODE`\n  * `DOCUMENT_TYPE_NODE`\n\nEverything else, at least for the time being, is considered *YAGNI*, and it won't likely ever land in this project, as there's no goal to replicate deprecated features of this aged Web.\n\n\n\n## Cached VS Not Cached\n\nThis module exports both `linkedom` and `linkedom/cached`, which are basically the exact same thing, except the cached version outperforms `linkedom` in these scenarios:\n\n  * the document, or any of its elements, are rarely changed, as opposite of frequently mutated or manipulated\n  * the use-case needs many repeated *CSS* selectors, over a sporadically mutated \"*tree*\"\n  * the generic DOM mutation time is *not* a concern (each, removal or change requires a whole document cache invalidation)\n  * the *RAM* is *not* a concern (all cached results are held into *NodeList* arrays until changes happen)\n\nOn the other hand, the basic, *non-cached*, module, grants the following:\n\n  * minimal amount of *RAM* needed, given any task to perform, as nothing is ever retained on *RAM*\n  * linear fast performance for any *every-time-new* structure, such as those created via `importNode` or `cloneNode` (i.e. template literals based libraries)\n  * much faster DOM manipulation, without side effect caused by cache invalidation\n\n\n\n## Benchmarks\n\nTo run the benchmark locally, please follow these commands:\n\n```sh\ngit clone https://github.com/WebReflection/linkedom.git\n\ncd linkedom/test\nnpm i\n\ncd ..\nnpm i\n\nnpm run benchmark\n```\n","funding_links":[],"categories":["HTML","Packages"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FWebReflection%2Flinkedom","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FWebReflection%2Flinkedom","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FWebReflection%2Flinkedom/lists"}