├── .editorconfig ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── funding.yml ├── index.js ├── license ├── package.json ├── readme.md ├── test.js └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | main: 7 | name: ${{matrix.node}} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v3 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/hydrogen 21 | - node 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | *.d.ts 4 | .idea 5 | coverage/ 6 | node_modules/ 7 | yarn.lock 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .nyc_output/ 2 | coverage/ 3 | *.md 4 | -------------------------------------------------------------------------------- /funding.yml: -------------------------------------------------------------------------------- 1 | github: wooorm 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates an iterator that iterates over a list (through an item). 3 | * 4 | * @template {Item} [T=Item] 5 | */ 6 | class ItemIterator { 7 | /** 8 | * Create a new iterator. 9 | * 10 | * @param {T|null} item 11 | */ 12 | constructor(item) { 13 | /** @type {T|null} */ 14 | this.item = item 15 | } 16 | 17 | /** 18 | * Move to the next item. 19 | * 20 | * @returns {IteratorResult} 21 | */ 22 | next() { 23 | const value = this.item 24 | 25 | if (value) { 26 | this.item = value.next 27 | return {value, done: false} 28 | } 29 | 30 | return {value: null, done: true} 31 | } 32 | } 33 | 34 | /** 35 | * Double linked list item. 36 | */ 37 | export class Item { 38 | /** 39 | * Create a new linked list item. 40 | */ 41 | constructor() { 42 | /* eslint-disable no-unused-expressions */ 43 | /** 44 | * The following item or `null` otherwise. 45 | * 46 | * @type {this|null} 47 | */ 48 | this.next 49 | 50 | /** 51 | * The preceding item or `null` otherwise. 52 | * 53 | * @type {this|null} 54 | */ 55 | this.prev 56 | 57 | /** 58 | * The list this item belongs to or `null` otherwise. 59 | * 60 | * @type {List|null} 61 | */ 62 | this.list 63 | /* eslint-enable no-unused-expressions */ 64 | } 65 | 66 | /** 67 | * Add the given item **after** the operated on item in a list. 68 | * 69 | * Throws an error when the given item has no `detach`, `append`, or 70 | * `prepend` methods. 71 | * Returns `false` when the operated on item is not attached to a list, 72 | * otherwise the given item. 73 | * 74 | * @param {this} item 75 | * @returns {this|false} 76 | */ 77 | append(item) { 78 | const list = this.list 79 | 80 | if (!item || !item.append || !item.prepend || !item.detach) { 81 | throw new Error( 82 | 'An argument without append, prepend, or detach methods was given to `Item#append`.' 83 | ) 84 | } 85 | 86 | // If self is detached or appending ourselves, return false. 87 | if (!list || this === item) { 88 | return false 89 | } 90 | 91 | // Detach the appendee. 92 | item.detach() 93 | 94 | // If self has a next item… 95 | if (this.next) { 96 | item.next = this.next 97 | this.next.prev = item 98 | } 99 | 100 | // Connect the appendee. 101 | item.prev = this 102 | item.list = list 103 | 104 | // Set the next item of self to the appendee. 105 | this.next = item 106 | 107 | // If the the parent list has no last item or if self is the parent lists last 108 | // item, link the lists last item to the appendee. 109 | if (this === list.tail || !list.tail) { 110 | list.tail = item 111 | } 112 | 113 | list.size++ 114 | 115 | return item 116 | } 117 | 118 | /** 119 | * Add the given item **before** the operated on item in a list. 120 | * 121 | * Throws an error when the given item has no `detach`, `append`, or `prepend` 122 | * methods. 123 | * Returns `false` when the operated on item is not attached to a list, 124 | * otherwise the given item. 125 | * 126 | * @param {this} item 127 | * @returns {this|false} 128 | */ 129 | prepend(item) { 130 | const list = this.list 131 | 132 | if (!item || !item.append || !item.prepend || !item.detach) { 133 | throw new Error( 134 | 'An argument without append, prepend, or detach methods was given to `Item#prepend`.' 135 | ) 136 | } 137 | 138 | // If self is detached or prepending ourselves, return false. 139 | if (!list || this === item) { 140 | return false 141 | } 142 | 143 | // Detach the prependee. 144 | item.detach() 145 | 146 | // If self has a previous item... 147 | if (this.prev) { 148 | item.prev = this.prev 149 | this.prev.next = item 150 | } 151 | 152 | // Connect the prependee. 153 | item.next = this 154 | item.list = list 155 | 156 | // Set the previous item of self to the prependee. 157 | this.prev = item 158 | 159 | // If self is the first item in the parent list, link the lists first item to 160 | // the prependee. 161 | if (this === list.head) { 162 | list.head = item 163 | } 164 | 165 | // If the the parent list has no last item, link the lists last item to self. 166 | if (!list.tail) { 167 | list.tail = this 168 | } 169 | 170 | list.size++ 171 | 172 | return item 173 | } 174 | 175 | /** 176 | * Remove the operated on item from its parent list. 177 | * 178 | * Removes references to it on its parent `list`, and `prev` and `next` 179 | * items. 180 | * Relinks all references. 181 | * Returns the operated on item. 182 | * Even when it was already detached. 183 | * 184 | * @returns {this} 185 | */ 186 | detach() { 187 | const list = this.list 188 | 189 | if (!list) { 190 | return this 191 | } 192 | 193 | // If self is the last item in the parent list, link the lists last item to 194 | // the previous item. 195 | if (list.tail === this) { 196 | list.tail = this.prev 197 | } 198 | 199 | // If self is the first item in the parent list, link the lists first item to 200 | // the next item. 201 | if (list.head === this) { 202 | list.head = this.next 203 | } 204 | 205 | // If both the last and first items in the parent list are the same, remove 206 | // the link to the last item. 207 | if (list.tail === list.head) { 208 | list.tail = null 209 | } 210 | 211 | // If a previous item exists, link its next item to selfs next item. 212 | if (this.prev) { 213 | this.prev.next = this.next 214 | } 215 | 216 | // If a next item exists, link its previous item to selfs previous item. 217 | if (this.next) { 218 | this.next.prev = this.prev 219 | } 220 | 221 | // Remove links from self to both the next and previous items, and to the 222 | // parent list. 223 | this.prev = null 224 | this.next = null 225 | this.list = null 226 | 227 | list.size-- 228 | 229 | return this 230 | } 231 | } 232 | 233 | // type-coverage:ignore-next-line 234 | Item.prototype.next = null 235 | // type-coverage:ignore-next-line 236 | Item.prototype.prev = null 237 | // type-coverage:ignore-next-line 238 | Item.prototype.list = null 239 | 240 | /** 241 | * Double linked list. 242 | * 243 | * @template {Item} [T=Item] 244 | * @implements {Iterable} 245 | */ 246 | export class List { 247 | /** 248 | * Create a new `this` from the given array of items. 249 | * 250 | * Ignores `null` or `undefined` values. 251 | * Throws an error when a given item has no `detach`, `append`, or `prepend` 252 | * methods. 253 | * 254 | * @template {Item} [T=Item] 255 | * @param {Array} [items] 256 | */ 257 | static from(items) { 258 | /** @type {List} */ 259 | const list = new this() 260 | return appendAll(list, items) 261 | } 262 | 263 | /** 264 | * Create a new `this` from the given arguments. 265 | * 266 | * Ignores `null` or `undefined` values. 267 | * Throws an error when a given item has no `detach`, `append`, or `prepend` 268 | * methods. 269 | * 270 | * @template {Item} [T=Item] 271 | * @param {Array} items 272 | * @returns {List} 273 | */ 274 | static of(...items) { 275 | /** @type {List} */ 276 | const list = new this() 277 | return appendAll(list, items) 278 | } 279 | 280 | /** 281 | * Create a new list from the given items. 282 | * 283 | * Ignores `null` or `undefined` values. 284 | * Throws an error when a given item has no `detach`, `append`, or `prepend` 285 | * methods. 286 | * 287 | * @param {Array} items 288 | */ 289 | constructor(...items) { 290 | /* eslint-disable no-unused-expressions */ 291 | /** 292 | * The number of items in the list. 293 | * 294 | * @type {number} 295 | */ 296 | this.size 297 | 298 | /** 299 | * The first item in a list or `null` otherwise. 300 | * 301 | * @type {T|null} 302 | */ 303 | this.head 304 | 305 | /** 306 | * The last item in a list and `null` otherwise. 307 | * 308 | * > 👉 **Note**: a list with only one item has **no tail**, only a head. 309 | * 310 | * @type {T|null} 311 | */ 312 | this.tail 313 | /* eslint-enable no-unused-expressions */ 314 | 315 | appendAll(this, items) 316 | } 317 | 318 | /** 319 | * Append an item to a list. 320 | * 321 | * Throws an error when the given item has no `detach`, `append`, or `prepend` 322 | * methods. 323 | * Returns the given item. 324 | * 325 | * @param {T|null|undefined} [item] 326 | * @returns {T|false} 327 | */ 328 | append(item) { 329 | if (!item) { 330 | return false 331 | } 332 | 333 | if (!item.append || !item.prepend || !item.detach) { 334 | throw new Error( 335 | 'An argument without append, prepend, or detach methods was given to `List#append`.' 336 | ) 337 | } 338 | 339 | // If self has a last item, defer appending to the last items append method, 340 | // and return the result. 341 | if (this.tail) { 342 | return this.tail.append(item) 343 | } 344 | 345 | // If self has a first item, defer appending to the first items append method, 346 | // and return the result. 347 | if (this.head) { 348 | return this.head.append(item) 349 | } 350 | 351 | // …otherwise, there is no `tail` or `head` item yet. 352 | item.detach() 353 | item.list = this 354 | this.head = item 355 | this.size++ 356 | 357 | return item 358 | } 359 | 360 | /** 361 | * Prepend an item to a list. 362 | * 363 | * Throws an error when the given item has no `detach`, `append`, or `prepend` 364 | * methods. 365 | * Returns the given item. 366 | * 367 | * @param {T|null|undefined} [item] 368 | * @returns {T|false} 369 | */ 370 | prepend(item) { 371 | if (!item) { 372 | return false 373 | } 374 | 375 | if (!item.append || !item.prepend || !item.detach) { 376 | throw new Error( 377 | 'An argument without append, prepend, or detach methods was given to `List#prepend`.' 378 | ) 379 | } 380 | 381 | if (this.head) { 382 | return this.head.prepend(item) 383 | } 384 | 385 | item.detach() 386 | item.list = this 387 | this.head = item 388 | this.size++ 389 | 390 | return item 391 | } 392 | 393 | /** 394 | * Returns the items of the list as an array. 395 | * 396 | * This does *not* detach the items. 397 | * 398 | * > **Note**: `List` also implements an iterator. 399 | * > That means you can also do `[...list]` to get an array. 400 | */ 401 | toArray() { 402 | let item = this.head 403 | /** @type {Array} */ 404 | const result = [] 405 | 406 | while (item) { 407 | result.push(item) 408 | item = item.next 409 | } 410 | 411 | return result 412 | } 413 | 414 | /** 415 | * Creates an iterator from the list. 416 | * 417 | * @returns {ItemIterator} 418 | */ 419 | [Symbol.iterator]() { 420 | return new ItemIterator(this.head) 421 | } 422 | } 423 | 424 | // type-coverage:ignore-next-line 425 | List.prototype.size = 0 426 | // type-coverage:ignore-next-line 427 | List.prototype.tail = null 428 | // type-coverage:ignore-next-line 429 | List.prototype.head = null 430 | 431 | /** 432 | * Creates a new list from the items passed in. 433 | * 434 | * @template {List} TheList 435 | * @template {Item} [T=Item] 436 | * @param {TheList} list 437 | * @param {Array|undefined} [items] 438 | * @returns {TheList} 439 | */ 440 | function appendAll(list, items) { 441 | if (!items) { 442 | return list 443 | } 444 | 445 | if (items[Symbol.iterator]) { 446 | const iterator = items[Symbol.iterator]() 447 | /** @type {IteratorResult} */ 448 | let result 449 | 450 | while ((result = iterator.next()) && !result.done) { 451 | list.append(result.value) 452 | } 453 | } else { 454 | let index = -1 455 | 456 | while (++index < items.length) { 457 | const item = items[index] 458 | list.append(item) 459 | } 460 | } 461 | 462 | return list 463 | } 464 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2014 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "linked-list", 3 | "version": "3.1.0", 4 | "description": "Minimalistic linked lists", 5 | "license": "MIT", 6 | "keywords": [ 7 | "double", 8 | "linked", 9 | "list" 10 | ], 11 | "repository": "wooorm/linked-list", 12 | "bugs": "https://github.com/wooorm/linked-list/issues", 13 | "funding": { 14 | "type": "github", 15 | "url": "https://github.com/sponsors/wooorm" 16 | }, 17 | "author": "Titus Wormer (https://wooorm.com)", 18 | "contributors": [ 19 | "Titus Wormer (https://wooorm.com)", 20 | "Blake Embrey ", 21 | "Regev Brody " 22 | ], 23 | "sideEffects": false, 24 | "type": "module", 25 | "main": "index.js", 26 | "types": "index.d.ts", 27 | "files": [ 28 | "index.js", 29 | "index.d.ts" 30 | ], 31 | "devDependencies": { 32 | "@types/node": "^18.0.0", 33 | "c8": "^7.0.0", 34 | "prettier": "^2.0.0", 35 | "remark-cli": "^11.0.0", 36 | "remark-preset-wooorm": "^9.0.0", 37 | "type-coverage": "^2.0.0", 38 | "typescript": "^4.0.0", 39 | "xo": "^0.53.0" 40 | }, 41 | "scripts": { 42 | "prepack": "npm run build && npm run format", 43 | "build": "tsc --build --clean && tsc --build && type-coverage", 44 | "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", 45 | "test-api": "node --conditions development test.js", 46 | "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", 47 | "test": "npm run build && npm run format && npm run test-coverage" 48 | }, 49 | "prettier": { 50 | "tabWidth": 2, 51 | "useTabs": false, 52 | "singleQuote": true, 53 | "bracketSpacing": false, 54 | "semi": false, 55 | "trailingComma": "none" 56 | }, 57 | "xo": { 58 | "prettier": true, 59 | "rules": { 60 | "@typescript-eslint/ban-types": "off" 61 | } 62 | }, 63 | "remarkConfig": { 64 | "plugins": [ 65 | "preset-wooorm" 66 | ] 67 | }, 68 | "typeCoverage": { 69 | "atLeast": 100, 70 | "detail": true, 71 | "strict": true 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # linked-list 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![Downloads][downloads-badge]][downloads] 6 | [![Size][size-badge]][size] 7 | 8 | Small double [linked list][wiki]. 9 | 10 | ## Contents 11 | 12 | * [What is this?](#what-is-this) 13 | * [When should I use this?](#when-should-i-use-this) 14 | * [Install](#install) 15 | * [Use](#use) 16 | * [API](#api) 17 | * [`List([items…])`](#listitems) 18 | * [`Item()`](#item) 19 | * [Types](#types) 20 | * [Compatibility](#compatibility) 21 | * [Security](#security) 22 | * [Contribute](#contribute) 23 | * [License](#license) 24 | 25 | ## What is this? 26 | 27 | This package is a small double linked list. 28 | Items in linked lists know about their next sibling (the item after them). 29 | In double linked lists, items also know about their previous sibling (the item 30 | before them). 31 | 32 | ## When should I use this? 33 | 34 | You can use this project as a reference for how to implement a linked list but 35 | it’s also definitely possible to use it, directly or by subclassing its lists 36 | and items. 37 | 38 | ## Install 39 | 40 | This package is [ESM only][esm]. 41 | In Node.js (version 14.14+, 16.0+), install with [npm][]: 42 | 43 | ```sh 44 | npm install linked-list 45 | ``` 46 | 47 | In Deno with [`esm.sh`][esmsh]: 48 | 49 | ```js 50 | import {List, Item} from 'https://esm.sh/linked-list@3' 51 | ``` 52 | 53 | In browsers with [`esm.sh`][esmsh]: 54 | 55 | ```html 56 | 59 | ``` 60 | 61 | ## Use 62 | 63 | ```js 64 | import {List, Item} from 'linked-list' 65 | 66 | const item1 = new Item() 67 | const item2 = new Item() 68 | const item3 = new Item() 69 | const list = new List(item1, item2, item3) 70 | 71 | list.head // => item1 72 | list.head.next // => item2 73 | list.head.next.next // => item3 74 | list.head.next.prev // => item1 75 | list.tail // => item3 76 | list.tail.next // => `null` 77 | ``` 78 | 79 | Subclassing: 80 | 81 | ```js 82 | import {List, Item} from 'linked-list' 83 | 84 | class Tokens extends List { 85 | /** @param {string} delimiter */ 86 | join(delimiter) { 87 | return this.toArray().join(delimiter) 88 | } 89 | } 90 | 91 | class Token extends Item { 92 | /** @param {string} value */ 93 | constructor(value) { 94 | super() 95 | this.value = value 96 | } 97 | 98 | toString() { 99 | return this.value 100 | } 101 | } 102 | 103 | const dogs = new Token('dogs') 104 | const and = new Token('&') 105 | const cats = new Token('cats') 106 | const tokens = new Tokens(dogs, and, cats) 107 | 108 | console.log(tokens.join(' ')) // => 'dogs & cats' 109 | 110 | and.prepend(cats) 111 | and.append(dogs) 112 | 113 | console.log(tokens.join(' ') + '!') // => 'cats & dogs!' 114 | ``` 115 | 116 | ## API 117 | 118 | This package exports the identifiers `List` and `Item`. 119 | There is no default export. 120 | 121 | ### `List([items…])` 122 | 123 | ```js 124 | new List() 125 | new List(new Item(), new Item()) 126 | ``` 127 | 128 | Create a new list from the given items. 129 | 130 | Ignores `null` or `undefined` values. 131 | Throws an error when a given item has no `detach`, `append`, or `prepend` 132 | methods. 133 | 134 | #### `List.from([items])` 135 | 136 | ```js 137 | List.from() 138 | List.from([]) 139 | List.from([new Item(), new Item()]) 140 | ``` 141 | 142 | Create a new `this` from the given array of items. 143 | 144 | Ignores `null` or `undefined` values. 145 | Throws an error when a given item has no `detach`, `append`, or `prepend` 146 | methods. 147 | 148 | #### `List.of([items…])` 149 | 150 | ```js 151 | List.of() 152 | List.of(new Item(), new Item()) 153 | ``` 154 | 155 | Create a new `this` from the given arguments. 156 | 157 | Ignores `null` or `undefined` values. 158 | Throws an error when a given item has no `detach`, `append`, or `prepend` 159 | methods. 160 | 161 | #### `List#append(item)` 162 | 163 | ```js 164 | const list = new List() 165 | const item = new Item() 166 | 167 | console.log(list.head === null) // => true 168 | console.log(item.list === null) // => true 169 | 170 | list.append(item) 171 | 172 | console.log(list.head === item) // => true 173 | console.log(item.list === list) // => true 174 | ``` 175 | 176 | Append an item to a list. 177 | 178 | Throws an error when the given item has no `detach`, `append`, or `prepend` 179 | methods. 180 | Returns the given item. 181 | 182 | #### `List#prepend(item)` 183 | 184 | ```js 185 | const list = new List() 186 | const item = new Item() 187 | 188 | list.prepend(item) 189 | ``` 190 | 191 | Prepend an item to a list. 192 | 193 | Throws an error when the given item has no `detach`, `append`, or `prepend` 194 | methods. 195 | Returns the given item. 196 | 197 | #### `List#toArray()` 198 | 199 | ```js 200 | const item1 = new Item() 201 | const item2 = new Item() 202 | const list = new List(item1, item2) 203 | const array = list.toArray() 204 | 205 | console.log(array[0] === item1) // => true 206 | console.log(array[1] === item2) // => true 207 | console.log(array[0].next === item2) // => true 208 | console.log(array[1].prev === item1) // => true 209 | ``` 210 | 211 | Returns the items of the list as an array. 212 | 213 | This does *not* detach the items. 214 | 215 | > **Note**: `List` also implements an iterator. 216 | > That means you can also do `[...list]` to get an array. 217 | 218 | #### `List#head` 219 | 220 | ```js 221 | const item = new Item() 222 | const list = new List(item) 223 | 224 | console.log(list.head === item) // => true 225 | ``` 226 | 227 | The first item in a list or `null` otherwise. 228 | 229 | #### `List#tail` 230 | 231 | ```js 232 | const list = new List() 233 | const item1 = new Item() 234 | const item2 = new Item() 235 | 236 | console.log(list.tail === null) // => true 237 | 238 | list.append(item1) 239 | console.log(list.tail === null) // => true, see note. 240 | 241 | list.append(item2) 242 | console.log(list.tail === item2) // => true 243 | ``` 244 | 245 | The last item in a list and `null` otherwise. 246 | 247 | > 👉 **Note**: a list with only one item has **no tail**, only a head. 248 | 249 | #### `List#size` 250 | 251 | ```js 252 | const list = new List() 253 | const item1 = new Item() 254 | const item2 = new Item() 255 | 256 | console.log(list.size === 0) // => true 257 | 258 | list.append(item1) 259 | console.log(list.size === 1) // => true 260 | 261 | list.append(item2) 262 | console.log(list.size === 2) // => true 263 | ``` 264 | 265 | The number of items in the list. 266 | 267 | ### `Item()` 268 | 269 | ```js 270 | const item = new Item() 271 | ``` 272 | 273 | Create a new linked list item. 274 | 275 | #### `Item#append(item)` 276 | 277 | ```js 278 | const item1 = new Item() 279 | const item2 = new Item() 280 | 281 | new List().append(item1) 282 | 283 | console.log(item1.next === null) // => true 284 | 285 | item1.append(item2) 286 | console.log(item1.next === item2) // => true 287 | ``` 288 | 289 | Add the given item **after** the operated on item in a list. 290 | 291 | Throws an error when the given item has no `detach`, `append`, or `prepend` 292 | methods. 293 | Returns `false` when the operated on item is not attached to a list, otherwise 294 | the given item. 295 | 296 | #### `Item#prepend(item)` 297 | 298 | ```js 299 | const item1 = new Item() 300 | const item2 = new Item() 301 | 302 | new List().append(item1) 303 | 304 | console.log(item1.prev === null) // => true 305 | 306 | item1.prepend(item2) 307 | console.log(item1.prev === item2) // => true 308 | ``` 309 | 310 | Add the given item **before** the operated on item in a list. 311 | 312 | Throws an error when the given item has no `detach`, `append`, or `prepend` 313 | methods. 314 | Returns `false` when the operated on item is not attached to a list, otherwise 315 | the given item. 316 | 317 | #### `Item#detach()` 318 | 319 | ```js 320 | const item = new Item() 321 | const list = new List(item) 322 | 323 | console.log(item.list === list) // => true 324 | 325 | item.detach() 326 | console.log(item.list === null) // => true 327 | ``` 328 | 329 | Remove the operated on item from its parent list. 330 | 331 | Removes references to it on its parent `list`, and `prev` and `next` items. 332 | Relinks all references. 333 | Returns the operated on item. 334 | Even when it was already detached. 335 | 336 | #### `Item#next` 337 | 338 | ```js 339 | const item1 = new Item() 340 | const item2 = new Item() 341 | 342 | const list = new List(item1) 343 | 344 | console.log(item1.next === null) // => true 345 | console.log(item2.next === null) // => true 346 | 347 | item1.append(item2) 348 | 349 | console.log(item1.next === item2) // => true 350 | 351 | item1.detach() 352 | 353 | console.log(item1.next === null) // => true 354 | ``` 355 | 356 | The following item or `null` otherwise. 357 | 358 | #### `Item#prev` 359 | 360 | ```js 361 | const item1 = new Item() 362 | const item2 = new Item() 363 | 364 | const list = new List(item1) 365 | 366 | console.log(item1.prev === null) // => true 367 | console.log(item2.prev === null) // => true 368 | 369 | item1.append(item2) 370 | 371 | console.log(item2.prev === item1) // => true 372 | 373 | item2.detach() 374 | 375 | console.log(item2.prev === null) // => true 376 | ``` 377 | 378 | The preceding item or `null` otherwise. 379 | 380 | #### `Item#list` 381 | 382 | ```js 383 | const item = new Item() 384 | const list = new List() 385 | 386 | console.log(item.list === null) // => true 387 | 388 | list.append(item) 389 | 390 | console.log(item.list === list) // => true 391 | 392 | item.detach() 393 | 394 | console.log(item.list === null) // => true 395 | ``` 396 | 397 | The list this item belongs to or `null` otherwise. 398 | 399 | ## Types 400 | 401 | This package is fully typed with [TypeScript][]. 402 | It exports no additional types. 403 | 404 | ## Compatibility 405 | 406 | This package is at least compatible with all maintained versions of Node.js. 407 | As of now, that is Node.js 14.14+ and 16.0+. 408 | It also works in Deno and modern browsers. 409 | 410 | ## Security 411 | 412 | This package is safe. 413 | 414 | ## Contribute 415 | 416 | Yes please! 417 | See [How to Contribute to Open Source][contribute]. 418 | 419 | ## License 420 | 421 | [MIT][license] © [Titus Wormer][author] 422 | 423 | 424 | 425 | [build-badge]: https://github.com/wooorm/linked-list/workflows/main/badge.svg 426 | 427 | [build]: https://github.com/wooorm/linked-list/actions 428 | 429 | [coverage-badge]: https://img.shields.io/codecov/c/github/wooorm/linked-list.svg 430 | 431 | [coverage]: https://codecov.io/github/wooorm/linked-list 432 | 433 | [downloads-badge]: https://img.shields.io/npm/dm/linked-list.svg 434 | 435 | [downloads]: https://www.npmjs.com/package/linked-list 436 | 437 | [size-badge]: https://img.shields.io/bundlephobia/minzip/linked-list.svg 438 | 439 | [size]: https://bundlephobia.com/result?p=linked-list 440 | 441 | [npm]: https://docs.npmjs.com/cli/install 442 | 443 | [esmsh]: https://esm.sh 444 | 445 | [license]: license 446 | 447 | [author]: https://wooorm.com 448 | 449 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 450 | 451 | [typescript]: https://www.typescriptlang.org 452 | 453 | [contribute]: https://opensource.guide/how-to-contribute/ 454 | 455 | [wiki]: https://wikipedia.org/wiki/Linked_list 456 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import test from 'node:test' 3 | import {List, Item} from './index.js' 4 | 5 | const own = {}.hasOwnProperty 6 | 7 | test('List [List]', async function (t) { 8 | await t.test('@constructor', function () { 9 | assert.equal(typeof List.of, 'function', 'should have an `of` method') 10 | assert.equal(typeof List.from, 'function', 'should have a `from` method') 11 | }) 12 | 13 | await t.test('of [List.of]', async function (t) { 14 | /** 15 | * @extends {List} 16 | */ 17 | class C extends List {} 18 | 19 | assert.ok( 20 | List.of() instanceof List, 21 | 'should return an instance of self when *no* arguments are given' 22 | ) 23 | 24 | assert.equal(List.of().size, 0, 'should be empty') 25 | 26 | assert.ok(!own.call(List.of(null), 'head'), 'should ignore `null` values') 27 | assert.ok( 28 | !own.call(List.of(undefined), 'head'), 29 | 'should ignore `undefined` values' 30 | ) 31 | 32 | assert.ok( 33 | List.of(new Item()) instanceof List, 34 | 'should return an instance of self when arguments are given (1)' 35 | ) 36 | 37 | assert.equal(List.of(new Item()).size, 1, 'should have a proper size') 38 | 39 | assert.ok( 40 | C.of(new Item()) instanceof C, 41 | 'should return an instance of self when arguments are given (2)' 42 | ) 43 | 44 | assert.throws(function () { 45 | // @ts-expect-error: non-item. 46 | List.of({}) 47 | }, 'should throw an error when an invalid item is given') 48 | 49 | await t.test( 50 | 'should add (“append”) items in the order they were given', 51 | function () { 52 | const item = new Item() 53 | const item1 = new Item() 54 | const item2 = new Item() 55 | const list = List.of(item, item1, item2) 56 | 57 | assert.equal(list.size, 3) 58 | 59 | assert.equal(list.head, item) 60 | assert.equal(list.head.next, item1) 61 | assert.equal(list.head.next.next, item2) 62 | 63 | assert.equal(list.tail, item2) 64 | assert.equal(list.tail.prev, item1) 65 | assert.equal(list.tail.prev.prev, item) 66 | } 67 | ) 68 | }) 69 | 70 | await t.test('from [List.from]', async function (t) { 71 | /** 72 | * @extends {List} 73 | */ 74 | class C extends List {} 75 | 76 | assert.ok( 77 | List.from() instanceof List, 78 | 'should return an instance of self when *no* arguments are given (1)' 79 | ) 80 | 81 | assert.equal(List.from().size, 0, 'should be empty') 82 | 83 | assert.ok( 84 | C.from() instanceof C, 85 | 'should return an instance of self when *no* arguments are given (2)' 86 | ) 87 | 88 | assert.ok( 89 | !own.call(List.from([null]), 'head'), 90 | 'should ignore `null` values' 91 | ) 92 | 93 | assert.ok( 94 | !own.call(List.from([undefined]), 'head'), 95 | 'should ignore `undefined` values' 96 | ) 97 | 98 | assert.ok( 99 | List.from([new Item()]) instanceof List, 100 | 'should return an instance of self when items are given (1)' 101 | ) 102 | 103 | assert.equal(List.from([new Item()]).size, 1, 'should have a proper size') 104 | 105 | assert.ok( 106 | C.from([new Item()]) instanceof C, 107 | 'should return an instance of self when items are given (2)' 108 | ) 109 | 110 | assert.throws(function () { 111 | // @ts-expect-error: non-item. 112 | List.from([{}]) 113 | }, 'should throw an error when an invalid item is given') 114 | 115 | await t.test( 116 | 'should add (“append”) items in the order they were given', 117 | function () { 118 | const item = new Item() 119 | const item1 = new Item() 120 | const item2 = new Item() 121 | const list = List.from([item, item1, item2]) 122 | 123 | assert.equal(list.size, 3) 124 | 125 | assert.equal(list.head, item) 126 | assert.equal(list.head.next, item1) 127 | assert.equal(list.head.next.next, item2) 128 | 129 | assert.equal(list.tail, item2) 130 | assert.equal(list.tail.prev, item1) 131 | assert.equal(list.tail.prev.prev, item) 132 | } 133 | ) 134 | 135 | await t.test( 136 | 'should add items from an array with `Symbol.iterator`', 137 | function () { 138 | const items = [new Item(), new Item(), null, new Item()] 139 | // Remove iterator to test array branch. 140 | // @ts-expect-error: that’s the test. 141 | items[Symbol.iterator] = undefined 142 | const list = List.from(items) 143 | 144 | assert.equal(list.size, 3) 145 | 146 | assert(list.head, 'should have a `head`') 147 | assert.equal(list.head, items[0]) 148 | assert(list.head.next, 'should have a `head.next`') 149 | assert.equal(list.head.next, items[1]) 150 | assert.equal(list.head.next.next, items[3]) 151 | 152 | assert(list.tail, 'should have a `tail`') 153 | assert.equal(list.tail, items[3]) 154 | assert(list.tail.prev, 'should have a `tail.prev`') 155 | assert.equal(list.tail.prev, items[1]) 156 | assert.equal(list.tail.prev.prev, items[0]) 157 | } 158 | ) 159 | }) 160 | 161 | await t.test('@instance', async function (t) { 162 | const list = new List() 163 | assert.equal(list.head, null, 'should have a `head` property set to `null`') 164 | 165 | assert.equal( 166 | new List().tail, 167 | null, 168 | 'should have a `tail` property set to `null`' 169 | ) 170 | 171 | assert.equal(new List().size, 0, 'should be empty') 172 | 173 | assert.equal( 174 | typeof new List().prepend, 175 | 'function', 176 | 'should have a `prepend` method' 177 | ) 178 | 179 | assert.equal( 180 | typeof new List().append, 181 | 'function', 182 | 'should have an `append` method' 183 | ) 184 | 185 | assert.equal( 186 | typeof new List().toArray, 187 | 'function', 188 | 'should have an `toArray` method' 189 | ) 190 | 191 | await t.test('prepend [List#prepend]', function () { 192 | assert.equal( 193 | new List().prepend(), 194 | false, 195 | 'should return false when no item is given' 196 | ) 197 | 198 | let list = new List() 199 | list.prepend() 200 | assert.equal(list.size, 0, 'should have 0 size of no item is given') 201 | 202 | let item = new Item() 203 | 204 | assert.equal( 205 | new List().prepend(item), 206 | item, 207 | 'should return the given item' 208 | ) 209 | 210 | list = new List() 211 | 212 | assert.throws(function () { 213 | // @ts-expect-error: non-item. 214 | list.prepend({}) 215 | }, 'should throw an error when an invalid item is given') 216 | 217 | list = new List() 218 | item = new Item() 219 | list.prepend(item) 220 | 221 | assert.equal(list.size, 1, 'should have proper size after prepend') 222 | 223 | assert.equal(list.head, item, 'should set `@head` to the first prependee') 224 | 225 | list = new List() 226 | item = new Item() 227 | list.prepend(item) 228 | 229 | assert.equal( 230 | list.tail, 231 | null, 232 | 'shouldn’t set `@tail` to the first prependee' 233 | ) 234 | 235 | let other = new Item() 236 | list.prepend(other) 237 | 238 | assert.equal( 239 | list.head, 240 | other, 241 | 'should set `@head` to further prependees (1)' 242 | ) 243 | 244 | assert.equal(list.size, 2, 'should update size after 2nd prepend') 245 | 246 | assert.equal( 247 | list.tail, 248 | item, 249 | 'should set `@tail` to the first prependee (1)' 250 | ) 251 | 252 | other = new Item() 253 | list.prepend(other) 254 | 255 | assert.equal( 256 | list.head, 257 | other, 258 | 'should set `@head` to further prependedees (2)' 259 | ) 260 | 261 | assert.equal( 262 | list.tail, 263 | item, 264 | 'should set `@tail` to the first prependee (2)' 265 | ) 266 | 267 | assert.equal(list.size, 3, 'should update size after 2nd prepend') 268 | 269 | list = new List() 270 | const otherList = new List() 271 | item = new Item() 272 | 273 | list.prepend(item) 274 | otherList.prepend(item) 275 | assert.equal( 276 | list.size, 277 | 0, 278 | 'should update size after item moved to new list' 279 | ) 280 | assert.equal( 281 | otherList.size, 282 | 1, 283 | 'should update size after item moved from different list' 284 | ) 285 | assert.equal( 286 | list.head, 287 | null, 288 | 'should detach the previous parent list of a prependee' 289 | ) 290 | 291 | assert.equal( 292 | otherList.head, 293 | item, 294 | 'should attach a prependee to a new list' 295 | ) 296 | }) 297 | 298 | await t.test('append [List#append]', function () { 299 | assert.equal( 300 | new List().append(), 301 | false, 302 | 'should return false when no item is given' 303 | ) 304 | 305 | let list = new List() 306 | list.append() 307 | assert.equal(list.size, 0, 'should have 0 size of no item is given') 308 | 309 | let item = new Item() 310 | 311 | assert.equal( 312 | new List().append(item), 313 | item, 314 | 'should return the given item' 315 | ) 316 | 317 | list = new List() 318 | list.append(item) 319 | 320 | assert.equal(list.size, 1, 'should have proper size after append') 321 | 322 | list = new List() 323 | 324 | assert.throws(function () { 325 | // @ts-expect-error: non-item. 326 | list.append({}) 327 | }, 'should throw an error when an invalid item is given') 328 | 329 | list = new List() 330 | item = new Item() 331 | list.append(item) 332 | 333 | assert.equal(list.head, item, 'should set `@head` to the first appendee') 334 | 335 | list = new List() 336 | item = new Item() 337 | list.append(item) 338 | 339 | assert.equal( 340 | list.tail, 341 | null, 342 | 'shouldn’t set `@tail` to the first appendee' 343 | ) 344 | 345 | let other = new Item() 346 | list.append(other) 347 | 348 | assert.equal(list.size, 2, 'should update size after 2nd append') 349 | 350 | assert.equal( 351 | list.tail, 352 | other, 353 | 'should set `@tail` to further appendedees (1)' 354 | ) 355 | 356 | assert.equal( 357 | list.head, 358 | item, 359 | 'should set `@head` to the first appendee (1)' 360 | ) 361 | 362 | other = new Item() 363 | list.append(other) 364 | 365 | assert.equal( 366 | list.tail, 367 | other, 368 | 'should set `@tail` to further appendees (2)' 369 | ) 370 | 371 | assert.equal( 372 | list.head, 373 | item, 374 | 'should set `@head` to the first appendee (2)' 375 | ) 376 | 377 | assert.equal(list.size, 3, 'should update size after 2nd append') 378 | 379 | list = new List() 380 | const otherList = new List() 381 | item = new Item() 382 | 383 | list.append(item) 384 | otherList.append(item) 385 | 386 | assert.equal( 387 | list.size, 388 | 0, 389 | 'should update size after item moved to new list' 390 | ) 391 | assert.equal( 392 | otherList.size, 393 | 1, 394 | 'should update size after item moved from different list' 395 | ) 396 | 397 | assert.equal( 398 | list.head, 399 | null, 400 | 'should detach the previous parent list of an appendee' 401 | ) 402 | 403 | assert.equal( 404 | otherList.head, 405 | item, 406 | 'should attach an appendee to a new list' 407 | ) 408 | }) 409 | 410 | await t.test('toArray [List#toArray]', function () { 411 | assert.ok( 412 | Array.isArray(new List(new Item()).toArray()), 413 | 'should return an array' 414 | ) 415 | 416 | assert.ok( 417 | Array.isArray(new List().toArray()), 418 | 'should return an array, even when ' + 419 | 'the operated on list has no items' 420 | ) 421 | 422 | const list = new List(new Item(), new Item(), new Item()) 423 | const result = list.toArray() 424 | 425 | assert.equal(result[0], list.head, 'should return a sorted array (1)') 426 | assert.equal( 427 | result[1], 428 | // @ts-expect-error: exists. 429 | list.head.next, 430 | 'should return a sorted array (2)' 431 | ) 432 | assert.equal(result[2], list.tail, 'should return a sorted array (3)') 433 | }) 434 | 435 | await t.test('@@iterator [List#@@iterator]', function () { 436 | const list = new List(new Item(), new Item(), new Item()) 437 | const result = Array.from(list) 438 | 439 | assert.equal(result[0], list.head, 'should return a sorted array (1)') 440 | assert.equal( 441 | result[1], 442 | // @ts-expect-error: exists. 443 | list.head.next, 444 | 'should return a sorted array (2)' 445 | ) 446 | assert.equal(result[2], list.tail, 'should return a sorted array (3)') 447 | }) 448 | }) 449 | }) 450 | 451 | test('Item [List.Item]', async function (t) { 452 | assert.equal( 453 | new Item().list, 454 | null, 455 | 'should have a `list` property set to `null`' 456 | ) 457 | assert.equal( 458 | new Item().prev, 459 | null, 460 | 'should have a `prev` property set to `null`' 461 | ) 462 | assert.equal( 463 | new Item().next, 464 | null, 465 | 'should have a `next` property set to `null`' 466 | ) 467 | assert.equal( 468 | typeof new Item().prepend, 469 | 'function', 470 | 'should have a `prepend` method' 471 | ) 472 | assert.equal( 473 | typeof new Item().append, 474 | 'function', 475 | 'should have a `append` method' 476 | ) 477 | assert.equal( 478 | typeof new Item().detach, 479 | 'function', 480 | 'should have a `detach` method' 481 | ) 482 | 483 | await t.test('prepend [List.Item#prepend]', function () { 484 | let item = new Item() 485 | let other = new Item() 486 | 487 | assert.equal( 488 | item.prepend(other), 489 | false, 490 | 'should return false when the operated on instance is not attached' 491 | ) 492 | 493 | assert.equal(item.prev, null, 'should do nothing if `item` is detached (1)') 494 | assert.equal( 495 | other.next, 496 | null, 497 | 'should do nothing if `item` is detached (2)' 498 | ) 499 | 500 | assert.throws(function () { 501 | // @ts-expect-error: invalid value. 502 | item.prepend(null) 503 | }, 'should throw an error when an invalid item is given (1)') 504 | 505 | assert.throws(function () { 506 | // @ts-expect-error: invalid value. 507 | item.prepend({}) 508 | }, 'should throw an error when an invalid item is given (2)') 509 | 510 | item = new Item() 511 | other = new Item() 512 | let list = new List() 513 | list.append(item) 514 | 515 | assert.equal( 516 | item.prepend(item), 517 | false, 518 | 'should return false when the item tries to prepend itself' 519 | ) 520 | 521 | assert.equal(item.prev, null, 'should do nothing if single `item` (1)') 522 | assert.equal(item.next, null, 'should do nothing if single `item` (2)') 523 | 524 | assert.equal( 525 | item.prepend(other), 526 | other, 527 | 'should return the given item when ' + 528 | 'the operated on instance is ' + 529 | 'attached' 530 | ) 531 | 532 | assert.equal(list.size, 2, 'should update size after prepend on item') 533 | 534 | item = new Item() 535 | const otherList = new List(item) 536 | list = new List(new Item()) 537 | 538 | assert(list.head, 'should attach given item') 539 | list.head.prepend(item) 540 | 541 | assert.equal( 542 | otherList.size, 543 | 0, 544 | 'should update size after prepend on item to a different list' 545 | ) 546 | 547 | assert.equal( 548 | otherList.head, 549 | null, 550 | 'should detach the previous parent list of a given item' 551 | ) 552 | 553 | assert.equal( 554 | item.list, 555 | list, 556 | 'should attach the given item to the operated on item’s list' 557 | ) 558 | 559 | assert.equal( 560 | list.head, 561 | item, 562 | 'should set the given item as the parent list’s `head` when the operated on item is the current `head`' 563 | ) 564 | 565 | assert(list.tail, 'should attach given item') 566 | assert.equal( 567 | list.tail, 568 | list.head.next, 569 | 'should set the operated on item as the parent list’s `tail` when the operated on item is the current `head`' 570 | ) 571 | 572 | assert.equal( 573 | list.tail.prev, 574 | item, 575 | 'should set the operated on item’s `prev` property to the given item' 576 | ) 577 | 578 | assert.equal( 579 | list.head.next, 580 | list.tail, 581 | 'should set the given item’s `next` property to the operated on item' 582 | ) 583 | 584 | const otherItem = list.tail 585 | item = new Item() 586 | otherItem.prepend(item) 587 | 588 | assert.equal(list.tail, otherItem, 'should not remove the tail') 589 | 590 | assert.equal(item.next, otherItem, 'should set `next` on the prependee') 591 | 592 | assert.equal(otherItem.prev, item, 'should set `prev` on the context') 593 | }) 594 | 595 | await t.test('append [List.Item#append]', function () { 596 | let item = new Item() 597 | let other = new Item() 598 | 599 | assert.equal( 600 | item.append(other), 601 | false, 602 | 'should return false when the operated on instance is not attached' 603 | ) 604 | 605 | assert.equal(item.prev, null, 'should do nothing if `item` is detached (1)') 606 | assert.equal( 607 | other.next, 608 | null, 609 | 'should do nothing if `item` is detached (2)' 610 | ) 611 | 612 | assert.throws(function () { 613 | // @ts-expect-error: invalid value. 614 | item.append(null) 615 | }, 'should throw an error when an invalid item is given (1)') 616 | 617 | assert.throws(function () { 618 | // @ts-expect-error: invalid value. 619 | item.append({}) 620 | }, 'should throw an error when an invalid item is given (2)') 621 | 622 | item = new Item() 623 | other = new Item() 624 | let list = new List() 625 | list.append(item) 626 | 627 | assert.equal( 628 | item.append(item), 629 | false, 630 | 'should return false when the item tries to append itself' 631 | ) 632 | 633 | assert.equal(item.prev, null, 'should do nothing if single `item` (1)') 634 | assert.equal(item.next, null, 'should do nothing if single `item` (2)') 635 | 636 | assert.equal( 637 | item.append(other), 638 | other, 639 | 'should return the given item when ' + 640 | 'the operated on instance is ' + 641 | 'attached' 642 | ) 643 | 644 | assert.equal(list.size, 2, 'should update size after append on item') 645 | 646 | item = new Item() 647 | const otherList = new List(item) 648 | list = new List(new Item()) 649 | 650 | assert(list.head, 'should attach given item') 651 | list.head.append(item) 652 | 653 | assert.equal( 654 | otherList.size, 655 | 0, 656 | 'should update size after append on item to a different list' 657 | ) 658 | 659 | assert.equal( 660 | otherList.head, 661 | null, 662 | 'should detach the previous parent list of a given item' 663 | ) 664 | 665 | assert.equal( 666 | item.list, 667 | list, 668 | 'should attach the given item to the operated on item’s list' 669 | ) 670 | 671 | assert.equal( 672 | list.tail, 673 | item, 674 | 'should set the given item as the parent list’s `tail` when the operated on item is the current `tail`' 675 | ) 676 | 677 | assert.equal( 678 | list.tail.prev, 679 | list.head, 680 | 'should keep the operated on item as the parent list’s `head` when the operated on item is the current `head`' 681 | ) 682 | 683 | const otherItem = list.head 684 | item = new Item() 685 | otherItem.append(item) 686 | 687 | assert.equal(list.head, otherItem, 'should not remove the head') 688 | 689 | assert.equal(otherItem.next, item, 'should set `next` on the context') 690 | 691 | assert.equal(item.prev, otherItem, 'should set `prev` on the appendee') 692 | }) 693 | 694 | await t.test('detach [List.Item#detach]', function () { 695 | let item = new Item() 696 | let list = new List() 697 | 698 | list.append(item) 699 | 700 | assert.equal(item.detach(), item, 'should return self') 701 | 702 | assert.equal(list.size, 0, 'should update size after detached item') 703 | 704 | assert.equal( 705 | item.detach(), 706 | item, 707 | 'should return self, even when the item is not attached' 708 | ) 709 | 710 | assert.equal( 711 | list.size, 712 | 0, 713 | 'should not update size after detaching already detached item' 714 | ) 715 | 716 | item = new Item() 717 | let other = new Item() 718 | list = new List() 719 | 720 | list.append(item) 721 | list.append(other) 722 | 723 | item.detach() 724 | 725 | assert.equal(list.size, 1, 'should update size after detached item') 726 | 727 | assert.equal( 728 | list.head, 729 | other, 730 | 'should set the item’s `next` property to the parent list’s `head` when the item is its current `head`' 731 | ) 732 | 733 | item = new Item() 734 | other = new Item() 735 | let other2 = new Item() 736 | list = new List() 737 | 738 | list.append(item) 739 | list.append(other) 740 | list.append(other2) 741 | 742 | other2.detach() 743 | 744 | assert.equal(list.size, 2, 'should update size after detached item') 745 | 746 | assert.equal( 747 | list.tail, 748 | other, 749 | 'should set the item’s `prev` property to the parent list’s `tail` when the item is its current `tail`' 750 | ) 751 | 752 | item = new Item() 753 | other = new Item() 754 | list = new List() 755 | 756 | list.append(item) 757 | list.append(other) 758 | 759 | other.detach() 760 | 761 | assert.equal(list.size, 1, 'should update size after detached item') 762 | 763 | assert.equal( 764 | list.tail, 765 | null, 766 | 'should set the parent list’s `tail` to `null` when the item is its current `tail` and its `prev` property is the current `tail`' 767 | ) 768 | 769 | item = new Item() 770 | other = new Item() 771 | other2 = new Item() 772 | list = new List() 773 | 774 | list.append(item) 775 | list.append(other) 776 | list.append(other2) 777 | 778 | other.detach() 779 | 780 | assert.equal(list.size, 2, 'should update size after detached item') 781 | 782 | assert.equal( 783 | item.next, 784 | other2, 785 | 'should set the previous item’s `next` ' + 786 | 'property to the current item’s `next` ' + 787 | 'property' 788 | ) 789 | 790 | item = new Item() 791 | other = new Item() 792 | other2 = new Item() 793 | list = new List() 794 | 795 | list.append(item) 796 | list.append(other) 797 | list.append(other2) 798 | 799 | other.detach() 800 | 801 | assert.equal(list.size, 2, 'should update size after detached item') 802 | 803 | assert.equal( 804 | other2.prev, 805 | item, 806 | 'should set the next item’s `prev` property to ' + 807 | 'the current item’s `prev` property' 808 | ) 809 | 810 | item = new Item() 811 | list = new List() 812 | 813 | list.append(item) 814 | item.detach() 815 | 816 | assert.equal( 817 | item.list, 818 | null, 819 | 'should set the item’s `list` property to `null`' 820 | ) 821 | 822 | item = new Item() 823 | other = new Item() 824 | list = new List() 825 | 826 | list.append(other) 827 | list.append(item) 828 | item.detach() 829 | 830 | assert.equal( 831 | item.prev, 832 | null, 833 | 'should set the item’s `prev` property to `null`' 834 | ) 835 | 836 | item = new Item() 837 | other = new Item() 838 | list = new List() 839 | 840 | list.append(item) 841 | list.append(other) 842 | item.detach() 843 | 844 | assert.equal( 845 | item.next, 846 | null, 847 | 'should set the item’s `next` property to `null`' 848 | ) 849 | }) 850 | }) 851 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/**.js"], 3 | "exclude": ["coverage", "node_modules"], 4 | "compilerOptions": { 5 | "checkJs": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "lib": ["es2020"], 11 | "module": "node16", 12 | "newLine": "lf", 13 | "skipLibCheck": true, 14 | "strict": true, 15 | "target": "es2020" 16 | } 17 | } 18 | --------------------------------------------------------------------------------