1
0

run npm install to generate a package lock

This commit is contained in:
sashinexists
2024-12-07 13:18:31 +11:00
parent e7d08a91b5
commit 23437d228e
2501 changed files with 290663 additions and 0 deletions

1
node_modules/.bin/marked generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../marked/bin/marked.js

1
node_modules/.bin/ori generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../@weborigami/origami/src/cli/cli.js

1
node_modules/.bin/semver generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../semver/bin/semver.js

1
node_modules/.bin/smartypants generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../smartypants/bin/smartypants.js

1
node_modules/.bin/smartypantsu generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../smartypants/bin/smartypantsu.js

1
node_modules/.bin/yaml generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../yaml/bin.mjs

361
node_modules/.package-lock.json generated vendored Normal file
View File

@@ -0,0 +1,361 @@
{
"name": "sashinexists",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
"integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
"integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4"
}
},
"node_modules/@weborigami/async-tree": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@weborigami/async-tree/-/async-tree-0.2.1.tgz",
"integrity": "sha512-RH+RCOkT6Oy8GtSKsGUs87izjUTetq8fJ64c7LrqsssEsgMi/ENMdnnO4kVHPW4mvqSMUQqoK0WUyP9hjSrhVQ==",
"dependencies": {
"@weborigami/types": "0.2.1"
}
},
"node_modules/@weborigami/language": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@weborigami/language/-/language-0.2.1.tgz",
"integrity": "sha512-syp5L5kHdt4dW4nkjTQa2VMBS94qn+DA524wWY0XpbpyC5h1YMA3s+eOQ29PhdiB9n2xjRhu+ZjxDuLhsv2YaQ==",
"dependencies": {
"@weborigami/async-tree": "0.2.1",
"@weborigami/types": "0.2.1",
"watcher": "2.3.1"
}
},
"node_modules/@weborigami/origami": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@weborigami/origami/-/origami-0.2.1.tgz",
"integrity": "sha512-nKJWvasGw469qnC1KOF2uayhjlWdOBXo/NRmp3Z64jDYiKYQ60o9uH0CY3b8wB33mRvu3YREoajFNlVcwvnyqQ==",
"dependencies": {
"@weborigami/async-tree": "0.2.1",
"@weborigami/language": "0.2.1",
"@weborigami/types": "0.2.1",
"exif-parser": "0.1.12",
"graphviz-wasm": "3.0.2",
"highlight.js": "11.10.0",
"marked": "14.1.2",
"marked-gfm-heading-id": "4.1.0",
"marked-highlight": "2.1.4",
"marked-smartypants": "1.1.8",
"sharp": "0.33.5",
"yaml": "2.5.1"
},
"bin": {
"ori": "src/cli/cli.js"
}
},
"node_modules/@weborigami/types": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@weborigami/types/-/types-0.2.1.tgz",
"integrity": "sha512-+386SPOxoo1Whx3Ga+5XanXVhAz/IdwfFuFGSVhO39qj46FwG4ZEU6qqTRZpNW5tX3ABkO9DvAjqoCmapDxaXQ=="
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"license": "MIT",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/decode-base64": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/decode-base64/-/decode-base64-3.0.1.tgz",
"integrity": "sha512-IWgiXlMAdm9c4RrOnvkFxYpfZRlOys4Wxnc/QT72hVLUZKCr7RPkfamgn2GXysCo06Zd4TGZyKaPHO4soBgSAg==",
"dependencies": {
"node-buffer-encoding": "^1.0.1"
}
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/dettle": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/dettle/-/dettle-1.0.4.tgz",
"integrity": "sha512-ktaWiLYYc/ajSa819+HxwABpqtk3dGIAmo5CbHvT3B6XyQSM7VNGDvCPNu94Ptc+Ti4tjTvAKRUCXU/lrVG4WQ=="
},
"node_modules/exif-parser": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz",
"integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="
},
"node_modules/function-once": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/function-once/-/function-once-3.0.0.tgz",
"integrity": "sha512-WEhgu9PE55sHFf+SBg3lI8+CWpsqReLcsp3g12XhwSJJgnodpSpHk6StvpeVcKuHAFCAdttLrslJRFDSdLDf4g=="
},
"node_modules/github-slugger": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz",
"integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==",
"license": "ISC"
},
"node_modules/graphviz-wasm": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/graphviz-wasm/-/graphviz-wasm-3.0.2.tgz",
"integrity": "sha512-aaO9dxQ7LTk9zvelCajRRSdSB+ZLniL5DNSkuLXAfIAbVvYCqlLmxz4zWC2LBbNNT+w6vp8Du8SvAVxH1bSVBQ==",
"dependencies": {
"decode-base64": "^3.0.1",
"function-once": "^3.0.0"
}
},
"node_modules/highlight.js": {
"version": "11.10.0",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.10.0.tgz",
"integrity": "sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
"license": "MIT"
},
"node_modules/marked": {
"version": "14.1.2",
"resolved": "https://registry.npmjs.org/marked/-/marked-14.1.2.tgz",
"integrity": "sha512-f3r0yqpz31VXiDB/wj9GaOB0a2PRLQl6vJmXiFrniNwjkKdvakqJRULhjFKJpxOchlCRiG5fcacoUZY5Xa6PEQ==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/marked-gfm-heading-id": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/marked-gfm-heading-id/-/marked-gfm-heading-id-4.1.0.tgz",
"integrity": "sha512-xRvV65Fnpq1krNspnyGsBvP0Y6h7/FrJ6U6y4e6zCWffiC1KxFFxFUKVu8ufMHop2xdvpwyWj5jPeA5W5x/6Zw==",
"license": "MIT",
"dependencies": {
"github-slugger": "^2.0.0"
},
"peerDependencies": {
"marked": ">=13 <15"
}
},
"node_modules/marked-highlight": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/marked-highlight/-/marked-highlight-2.1.4.tgz",
"integrity": "sha512-D1GOkcdzP+1dzjoColL7umojefFrASDuLeyaHS0Zr/Uo9jkr1V6vpLRCzfi1djmEaWyK0SYMFtHnpkZ+cwFT1w==",
"license": "MIT",
"peerDependencies": {
"marked": ">=4 <15"
}
},
"node_modules/marked-smartypants": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/marked-smartypants/-/marked-smartypants-1.1.8.tgz",
"integrity": "sha512-2n8oSjL2gSkH6M0dSdRIyLgqqky03iKQkdmoaylmIzwIhYTW204S7ry6zP2iqwSl0zSlJH2xmWgxlZ/4XB1CdQ==",
"license": "MIT",
"dependencies": {
"smartypants": "^0.2.2"
},
"peerDependencies": {
"marked": ">=4 <15"
}
},
"node_modules/node-buffer-encoding": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/node-buffer-encoding/-/node-buffer-encoding-1.0.2.tgz",
"integrity": "sha512-v2QFjf04xWb5Q7cyzbi8qEwe2vw2xJBXT7+pMOLA02+KJZlcJ/6syFYiH96ClXKfOG/kyBeysAuewJ7zfAUYKQ=="
},
"node_modules/promise-make-counter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/promise-make-counter/-/promise-make-counter-1.0.1.tgz",
"integrity": "sha512-R1JGFIgSJDpNV/JXxytAx6K79noEpcBiZXWDa3ic9WEMpBZbUdVVQjlA266SCicJ9CGqd70iGbbzbjRKbGU1Jg==",
"dependencies": {
"promise-make-naked": "^3.0.0"
}
},
"node_modules/promise-make-naked": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/promise-make-naked/-/promise-make-naked-3.0.0.tgz",
"integrity": "sha512-h71wwAMB2udFnlPmcxQMqKl6CckNLVKdk/ROtFivE6/VmW+rQKV0DWlGJ6VphRIoq22Tkonvdi3F+jlm5XDlow=="
},
"node_modules/semver": {
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sharp": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
"integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.3",
"semver": "^7.6.3"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.33.5",
"@img/sharp-darwin-x64": "0.33.5",
"@img/sharp-libvips-darwin-arm64": "1.0.4",
"@img/sharp-libvips-darwin-x64": "1.0.4",
"@img/sharp-libvips-linux-arm": "1.0.5",
"@img/sharp-libvips-linux-arm64": "1.0.4",
"@img/sharp-libvips-linux-s390x": "1.0.4",
"@img/sharp-libvips-linux-x64": "1.0.4",
"@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
"@img/sharp-libvips-linuxmusl-x64": "1.0.4",
"@img/sharp-linux-arm": "0.33.5",
"@img/sharp-linux-arm64": "0.33.5",
"@img/sharp-linux-s390x": "0.33.5",
"@img/sharp-linux-x64": "0.33.5",
"@img/sharp-linuxmusl-arm64": "0.33.5",
"@img/sharp-linuxmusl-x64": "0.33.5",
"@img/sharp-wasm32": "0.33.5",
"@img/sharp-win32-ia32": "0.33.5",
"@img/sharp-win32-x64": "0.33.5"
}
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"license": "MIT",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/smartypants": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/smartypants/-/smartypants-0.2.2.tgz",
"integrity": "sha512-TzobUYoEft/xBtb2voRPryAUIvYguG0V7Tt3de79I1WfXgCwelqVsGuZSnu3GFGRZhXR90AeEYIM+icuB/S06Q==",
"license": "BSD-3-Clause",
"bin": {
"smartypants": "bin/smartypants.js",
"smartypantsu": "bin/smartypantsu.js"
}
},
"node_modules/stubborn-fs": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-1.2.5.tgz",
"integrity": "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g=="
},
"node_modules/tiny-readdir": {
"version": "2.7.3",
"resolved": "https://registry.npmjs.org/tiny-readdir/-/tiny-readdir-2.7.3.tgz",
"integrity": "sha512-ae1CPk7/MRhdaSIfjytuCoCjcykCNfSH36MsD2Qq8A27apaVUV0nthOcCEjiBTTloBObq2ffvm0BycUayMWh3A==",
"dependencies": {
"promise-make-counter": "^1.0.1"
}
},
"node_modules/watcher": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/watcher/-/watcher-2.3.1.tgz",
"integrity": "sha512-d3yl+ey35h05r5EFP0TafE2jsmQUJ9cc2aernRVyAkZiu0J3+3TbNugNcqdUJDoWOfL2p+bNsN427stsBC/HnA==",
"dependencies": {
"dettle": "^1.0.2",
"stubborn-fs": "^1.2.5",
"tiny-readdir": "^2.7.2"
}
},
"node_modules/yaml": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
"integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
}
}
}

46
node_modules/@img/sharp-libvips-linux-x64/README.md generated vendored Normal file
View File

@@ -0,0 +1,46 @@
# `@img/sharp-libvips-linux-x64`
Prebuilt libvips and dependencies for use with sharp on Linux (glibc) x64.
## Licensing
This software contains third-party libraries
used under the terms of the following licences:
| Library | Used under the terms of |
|---------------|-----------------------------------------------------------------------------------------------------------|
| aom | BSD 2-Clause + [Alliance for Open Media Patent License 1.0](https://aomedia.org/license/patent-license/) |
| cairo | Mozilla Public License 2.0 |
| cgif | MIT Licence |
| expat | MIT Licence |
| fontconfig | [fontconfig Licence](https://gitlab.freedesktop.org/fontconfig/fontconfig/blob/main/COPYING) (BSD-like) |
| freetype | [freetype Licence](https://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/FTL.TXT) (BSD-like) |
| fribidi | LGPLv3 |
| glib | LGPLv3 |
| harfbuzz | MIT Licence |
| highway | Apache-2.0 License, BSD 3-Clause |
| lcms | MIT Licence |
| libarchive | BSD 2-Clause |
| libexif | LGPLv3 |
| libffi | MIT Licence |
| libheif | LGPLv3 |
| libimagequant | [BSD 2-Clause](https://github.com/lovell/libimagequant/blob/main/COPYRIGHT) |
| libnsgif | MIT Licence |
| libpng | [libpng License](https://github.com/pnggroup/libpng/blob/master/LICENSE) |
| librsvg | LGPLv3 |
| libspng | [BSD 2-Clause, libpng License](https://github.com/randy408/libspng/blob/master/LICENSE) |
| libtiff | [libtiff License](https://gitlab.com/libtiff/libtiff/blob/master/LICENSE.md) (BSD-like) |
| libvips | LGPLv3 |
| libwebp | New BSD License |
| libxml2 | MIT Licence |
| mozjpeg | [zlib License, IJG License, BSD-3-Clause](https://github.com/mozilla/mozjpeg/blob/master/LICENSE.md) |
| pango | LGPLv3 |
| pixman | MIT Licence |
| proxy-libintl | LGPLv3 |
| zlib-ng | [zlib Licence](https://github.com/zlib-ng/zlib-ng/blob/develop/LICENSE.md) |
Use of libraries under the terms of the LGPLv3 is via the
"any later version" clause of the LGPLv2 or LGPLv2.1.
Please report any errors or omissions via
https://github.com/lovell/sharp-libvips/issues/new

View File

@@ -0,0 +1,221 @@
/* glibconfig.h
*
* This is a generated file. Please modify 'glibconfig.h.in'
*/
#ifndef __GLIBCONFIG_H__
#define __GLIBCONFIG_H__
#include <glib/gmacros.h>
#include <limits.h>
#include <float.h>
#define GLIB_HAVE_ALLOCA_H
#define GLIB_STATIC_COMPILATION 1
#define GOBJECT_STATIC_COMPILATION 1
#define GIO_STATIC_COMPILATION 1
#define GMODULE_STATIC_COMPILATION 1
#define GI_STATIC_COMPILATION 1
#define G_INTL_STATIC_COMPILATION 1
#define FFI_STATIC_BUILD 1
/* Specifies that GLib's g_print*() functions wrap the
* system printf functions. This is useful to know, for example,
* when using glibc's register_printf_function().
*/
#define GLIB_USING_SYSTEM_PRINTF
G_BEGIN_DECLS
#define G_MINFLOAT FLT_MIN
#define G_MAXFLOAT FLT_MAX
#define G_MINDOUBLE DBL_MIN
#define G_MAXDOUBLE DBL_MAX
#define G_MINSHORT SHRT_MIN
#define G_MAXSHORT SHRT_MAX
#define G_MAXUSHORT USHRT_MAX
#define G_MININT INT_MIN
#define G_MAXINT INT_MAX
#define G_MAXUINT UINT_MAX
#define G_MINLONG LONG_MIN
#define G_MAXLONG LONG_MAX
#define G_MAXULONG ULONG_MAX
typedef signed char gint8;
typedef unsigned char guint8;
typedef signed short gint16;
typedef unsigned short guint16;
#define G_GINT16_MODIFIER "h"
#define G_GINT16_FORMAT "hi"
#define G_GUINT16_FORMAT "hu"
typedef signed int gint32;
typedef unsigned int guint32;
#define G_GINT32_MODIFIER ""
#define G_GINT32_FORMAT "i"
#define G_GUINT32_FORMAT "u"
#define G_HAVE_GINT64 1 /* deprecated, always true */
typedef signed long gint64;
typedef unsigned long guint64;
#define G_GINT64_CONSTANT(val) (val##L)
#define G_GUINT64_CONSTANT(val) (val##UL)
#define G_GINT64_MODIFIER "l"
#define G_GINT64_FORMAT "li"
#define G_GUINT64_FORMAT "lu"
#define GLIB_SIZEOF_VOID_P 8
#define GLIB_SIZEOF_LONG 8
#define GLIB_SIZEOF_SIZE_T 8
#define GLIB_SIZEOF_SSIZE_T 8
typedef signed long gssize;
typedef unsigned long gsize;
#define G_GSIZE_MODIFIER "l"
#define G_GSSIZE_MODIFIER "l"
#define G_GSIZE_FORMAT "lu"
#define G_GSSIZE_FORMAT "li"
#define G_MAXSIZE G_MAXULONG
#define G_MINSSIZE G_MINLONG
#define G_MAXSSIZE G_MAXLONG
typedef gint64 goffset;
#define G_MINOFFSET G_MININT64
#define G_MAXOFFSET G_MAXINT64
#define G_GOFFSET_MODIFIER G_GINT64_MODIFIER
#define G_GOFFSET_FORMAT G_GINT64_FORMAT
#define G_GOFFSET_CONSTANT(val) G_GINT64_CONSTANT(val)
#define G_POLLFD_FORMAT "%d"
#define GPOINTER_TO_INT(p) ((gint) (glong) (p))
#define GPOINTER_TO_UINT(p) ((guint) (gulong) (p))
#define GINT_TO_POINTER(i) ((gpointer) (glong) (i))
#define GUINT_TO_POINTER(u) ((gpointer) (gulong) (u))
typedef signed long gintptr;
typedef unsigned long guintptr;
#define G_GINTPTR_MODIFIER "l"
#define G_GINTPTR_FORMAT "li"
#define G_GUINTPTR_FORMAT "lu"
#define GLIB_MAJOR_VERSION 2
#define GLIB_MINOR_VERSION 81
#define GLIB_MICRO_VERSION 1
#define G_OS_UNIX
#define G_VA_COPY va_copy
#define G_VA_COPY_AS_ARRAY 1
#define G_HAVE_ISO_VARARGS 1
/* gcc-2.95.x supports both gnu style and ISO varargs, but if -ansi
* is passed ISO vararg support is turned off, and there is no work
* around to turn it on, so we unconditionally turn it off.
*/
#if __GNUC__ == 2 && __GNUC_MINOR__ == 95
# undef G_HAVE_ISO_VARARGS
#endif
#define G_HAVE_GROWING_STACK 0
#ifndef _MSC_VER
# define G_HAVE_GNUC_VARARGS 1
#endif
#if defined(__SUNPRO_C) && (__SUNPRO_C >= 0x590)
#define G_GNUC_INTERNAL __attribute__((visibility("hidden")))
#elif defined(__SUNPRO_C) && (__SUNPRO_C >= 0x550)
#define G_GNUC_INTERNAL __hidden
#elif defined (__GNUC__) && defined (G_HAVE_GNUC_VISIBILITY)
#define G_GNUC_INTERNAL __attribute__((visibility("hidden")))
#else
#define G_GNUC_INTERNAL
#endif
#define G_THREADS_ENABLED
#define G_THREADS_IMPL_POSIX
#define G_ATOMIC_LOCK_FREE
#define GINT16_TO_LE(val) ((gint16) (val))
#define GUINT16_TO_LE(val) ((guint16) (val))
#define GINT16_TO_BE(val) ((gint16) GUINT16_SWAP_LE_BE (val))
#define GUINT16_TO_BE(val) (GUINT16_SWAP_LE_BE (val))
#define GINT32_TO_LE(val) ((gint32) (val))
#define GUINT32_TO_LE(val) ((guint32) (val))
#define GINT32_TO_BE(val) ((gint32) GUINT32_SWAP_LE_BE (val))
#define GUINT32_TO_BE(val) (GUINT32_SWAP_LE_BE (val))
#define GINT64_TO_LE(val) ((gint64) (val))
#define GUINT64_TO_LE(val) ((guint64) (val))
#define GINT64_TO_BE(val) ((gint64) GUINT64_SWAP_LE_BE (val))
#define GUINT64_TO_BE(val) (GUINT64_SWAP_LE_BE (val))
#define GLONG_TO_LE(val) ((glong) GINT64_TO_LE (val))
#define GULONG_TO_LE(val) ((gulong) GUINT64_TO_LE (val))
#define GLONG_TO_BE(val) ((glong) GINT64_TO_BE (val))
#define GULONG_TO_BE(val) ((gulong) GUINT64_TO_BE (val))
#define GINT_TO_LE(val) ((gint) GINT32_TO_LE (val))
#define GUINT_TO_LE(val) ((guint) GUINT32_TO_LE (val))
#define GINT_TO_BE(val) ((gint) GINT32_TO_BE (val))
#define GUINT_TO_BE(val) ((guint) GUINT32_TO_BE (val))
#define GSIZE_TO_LE(val) ((gsize) GUINT64_TO_LE (val))
#define GSSIZE_TO_LE(val) ((gssize) GINT64_TO_LE (val))
#define GSIZE_TO_BE(val) ((gsize) GUINT64_TO_BE (val))
#define GSSIZE_TO_BE(val) ((gssize) GINT64_TO_BE (val))
#define G_BYTE_ORDER G_LITTLE_ENDIAN
#define GLIB_SYSDEF_POLLIN =1
#define GLIB_SYSDEF_POLLOUT =4
#define GLIB_SYSDEF_POLLPRI =2
#define GLIB_SYSDEF_POLLHUP =16
#define GLIB_SYSDEF_POLLERR =8
#define GLIB_SYSDEF_POLLNVAL =32
/* No way to disable deprecation warnings for macros, so only emit deprecation
* warnings on platforms where usage of this macro is broken */
#if defined(__APPLE__) || defined(_MSC_VER) || defined(__CYGWIN__)
#define G_MODULE_SUFFIX "so" GLIB_DEPRECATED_MACRO_IN_2_76
#else
#define G_MODULE_SUFFIX "so"
#endif
typedef int GPid;
#define G_PID_FORMAT "i"
#define GLIB_SYSDEF_AF_UNIX 1
#define GLIB_SYSDEF_AF_INET 2
#define GLIB_SYSDEF_AF_INET6 10
#define GLIB_SYSDEF_MSG_OOB 1
#define GLIB_SYSDEF_MSG_PEEK 2
#define GLIB_SYSDEF_MSG_DONTROUTE 4
#define G_DIR_SEPARATOR '/'
#define G_DIR_SEPARATOR_S "/"
#define G_SEARCHPATH_SEPARATOR ':'
#define G_SEARCHPATH_SEPARATOR_S ":"
#undef G_HAVE_FREE_SIZED
G_END_DECLS
#endif /* __GLIBCONFIG_H__ */

View File

@@ -0,0 +1 @@
module.exports = __dirname;

Binary file not shown.

42
node_modules/@img/sharp-libvips-linux-x64/package.json generated vendored Normal file
View File

@@ -0,0 +1,42 @@
{
"name": "@img/sharp-libvips-linux-x64",
"version": "1.0.4",
"description": "Prebuilt libvips and dependencies for use with sharp on Linux (glibc) x64",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
"repository": {
"type": "git",
"url": "git+https://github.com/lovell/sharp-libvips.git",
"directory": "npm/linux-x64"
},
"license": "LGPL-3.0-or-later",
"funding": {
"url": "https://opencollective.com/libvips"
},
"preferUnplugged": true,
"publishConfig": {
"access": "public"
},
"files": [
"lib",
"versions.json"
],
"type": "commonjs",
"exports": {
"./lib": "./lib/index.js",
"./package": "./package.json",
"./versions": "./versions.json"
},
"config": {
"glibc": ">=2.26"
},
"os": [
"linux"
],
"libc": [
"glibc"
],
"cpu": [
"x64"
]
}

View File

@@ -0,0 +1,30 @@
{
"aom": "3.9.1",
"archive": "3.7.4",
"cairo": "1.18.0",
"cgif": "0.4.1",
"exif": "0.6.24",
"expat": "2.6.2",
"ffi": "3.4.6",
"fontconfig": "2.15.0",
"freetype": "2.13.2",
"fribidi": "1.0.15",
"glib": "2.81.1",
"harfbuzz": "9.0.0",
"heif": "1.18.2",
"highway": "1.2.0",
"imagequant": "2.4.1",
"lcms": "2.16",
"mozjpeg": "4.1.5",
"pango": "1.54.0",
"pixman": "0.43.4",
"png": "1.6.43",
"proxy-libintl": "0.4",
"rsvg": "2.58.93",
"spng": "0.7.4",
"tiff": "4.6.0",
"vips": "8.15.3",
"webp": "1.4.0",
"xml": "2.13.3",
"zlib-ng": "2.2.1"
}

191
node_modules/@img/sharp-linux-x64/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,191 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, "control" means (i) the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising
permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
"Object" form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
"submitted" means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
2. Grant of Copyright License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
3. Grant of Patent License.
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
4. Redistribution.
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
5. Submission of Contributions.
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
6. Trademarks.
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
7. Disclaimer of Warranty.
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
8. Limitation of Liability.
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability.
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets "[]" replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same "printed page" as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

18
node_modules/@img/sharp-linux-x64/README.md generated vendored Normal file
View File

@@ -0,0 +1,18 @@
# `@img/sharp-linux-x64`
Prebuilt sharp for use with Linux (glibc) x64.
## Licensing
Copyright 2013 Lovell Fuller and others.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
[https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

BIN
node_modules/@img/sharp-linux-x64/lib/sharp-linux-x64.node generated vendored Executable file

Binary file not shown.

46
node_modules/@img/sharp-linux-x64/package.json generated vendored Normal file
View File

@@ -0,0 +1,46 @@
{
"name": "@img/sharp-linux-x64",
"version": "0.33.5",
"description": "Prebuilt sharp for use with Linux (glibc) x64",
"author": "Lovell Fuller <npm@lovell.info>",
"homepage": "https://sharp.pixelplumbing.com",
"repository": {
"type": "git",
"url": "git+https://github.com/lovell/sharp.git",
"directory": "npm/linux-x64"
},
"license": "Apache-2.0",
"funding": {
"url": "https://opencollective.com/libvips"
},
"preferUnplugged": true,
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.0.4"
},
"files": [
"lib"
],
"publishConfig": {
"access": "public"
},
"type": "commonjs",
"exports": {
"./sharp.node": "./lib/sharp-linux-x64.node",
"./package": "./package.json"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"config": {
"glibc": ">=2.26"
},
"os": [
"linux"
],
"libc": [
"glibc"
],
"cpu": [
"x64"
]
}

1
node_modules/@weborigami/async-tree/ReadMe.md generated vendored Normal file
View File

@@ -0,0 +1 @@
This library contains definitions for asynchronous trees backed by standard JavaScript classes like `Object` and `Map` and standard browser APIs such as the [Origin Private File System](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system). The library also includes collections of helpers for common tree operations.

4
node_modules/@weborigami/async-tree/browser.js generated vendored Normal file
View File

@@ -0,0 +1,4 @@
// Exports for browser
export * from "./shared.js";
export { default as BrowserFileTree } from "./src/drivers/BrowserFileTree.js";

72
node_modules/@weborigami/async-tree/index.ts generated vendored Normal file
View File

@@ -0,0 +1,72 @@
import type { AsyncTree } from "@weborigami/types";
export * from "./main.js";
export type KeyFn = (key: any, innerTree: AsyncTree) => any;
/**
* An object with a non-trivial `toString` method.
*
* TODO: We want to deliberately exclude the base `Object` class because its
* `toString` method return non-useful strings like `[object Object]`. How can
* we declare that in TypeScript?
*/
export type HasString = {
toString(): string;
};
/**
* A packed value is one that can be written to a file via fs.writeFile or into
* an HTTP response via response.write, or readily converted to such a form.
*/
export type Packed = (ArrayBuffer | Buffer | ReadableStream | string | String | TypedArray) & {
unpack?(): Promise<any>;
};
export type PlainObject = {
[key: string]: any;
};
export type ReduceFn = (values: any[], keys: any[], tree: AsyncTree) => Promise<any>;
export type StringLike = string | HasString;
export type NativeTreelike =
any[] |
AsyncTree |
Function |
Map<any, any> |
PlainObject |
Set<any>;
export type Treelike =
NativeTreelike |
Unpackable<NativeTreelike>;
export type TreeMapOptions = {
deep?: boolean;
description?: string;
needsSourceValue?: boolean;
inverseKey?: KeyFn;
key?: KeyFn;
value?: ValueKeyFn;
};
export type TreeTransform = (treelike: Treelike) => AsyncTree;
export type TypedArray =
Float32Array |
Float64Array |
Int8Array |
Int16Array |
Int32Array |
Uint8Array |
Uint8ClampedArray |
Uint16Array |
Uint32Array;
export type Unpackable<T> = {
unpack(): Promise<T>
};
export type ValueKeyFn = (value: any, key: any, innerTree: AsyncTree) => any;

5
node_modules/@weborigami/async-tree/main.js generated vendored Normal file
View File

@@ -0,0 +1,5 @@
// Exports for Node.js
export * from "./shared.js";
export { default as FileTree } from "./src/drivers/FileTree.js";
export * as extension from "./src/extension.js";

20
node_modules/@weborigami/async-tree/package.json generated vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "@weborigami/async-tree",
"version": "0.2.1",
"description": "Asynchronous tree drivers based on standard JavaScript classes",
"type": "module",
"main": "./main.js",
"browser": "./browser.js",
"types": "./index.ts",
"devDependencies": {
"@types/node": "22.7.4",
"typescript": "5.6.2"
},
"dependencies": {
"@weborigami/types": "0.2.1"
},
"scripts": {
"test": "node --test --test-reporter=spec",
"typecheck": "tsc"
}
}

32
node_modules/@weborigami/async-tree/shared.js generated vendored Normal file
View File

@@ -0,0 +1,32 @@
// Exports for both Node.js and browser
export { default as calendarTree } from "./src/drivers/calendarTree.js";
export { default as DeepMapTree } from "./src/drivers/DeepMapTree.js";
export { default as DeferredTree } from "./src/drivers/DeferredTree.js";
export { default as ExplorableSiteTree } from "./src/drivers/ExplorableSiteTree.js";
export { default as FunctionTree } from "./src/drivers/FunctionTree.js";
export { default as MapTree } from "./src/drivers/MapTree.js";
export { default as SetTree } from "./src/drivers/SetTree.js";
export { default as SiteTree } from "./src/drivers/SiteTree.js";
export { DeepObjectTree, ObjectTree, Tree } from "./src/internal.js";
export * as jsonKeys from "./src/jsonKeys.js";
export { default as cache } from "./src/operations/cache.js";
export { default as cachedKeyFunctions } from "./src/operations/cachedKeyFunctions.js";
export { default as concat } from "./src/operations/concat.js";
export { default as deepMerge } from "./src/operations/deepMerge.js";
export { default as deepReverse } from "./src/operations/deepReverse.js";
export { default as deepTake } from "./src/operations/deepTake.js";
export { default as deepValues } from "./src/operations/deepValues.js";
export { default as deepValuesIterator } from "./src/operations/deepValuesIterator.js";
export { default as group } from "./src/operations/group.js";
export { default as invokeFunctions } from "./src/operations/invokeFunctions.js";
export { default as keyFunctionsForExtensions } from "./src/operations/keyFunctionsForExtensions.js";
export { default as map } from "./src/operations/map.js";
export { default as merge } from "./src/operations/merge.js";
export { default as reverse } from "./src/operations/reverse.js";
export { default as scope } from "./src/operations/scope.js";
export { default as sort } from "./src/operations/sort.js";
export { default as take } from "./src/operations/take.js";
export * as symbols from "./src/symbols.js";
export * as trailingSlash from "./src/trailingSlash.js";
export * from "./src/utilities.js";

24
node_modules/@weborigami/async-tree/src/Tree.d.ts generated vendored Normal file
View File

@@ -0,0 +1,24 @@
import type { AsyncMutableTree, AsyncTree } from "@weborigami/types";
import { PlainObject, ReduceFn, Treelike, TreeMapOptions, ValueKeyFn } from "../index.ts";
export function assign(target: Treelike, source: Treelike): Promise<AsyncTree>;
export function clear(AsyncTree: AsyncMutableTree): Promise<void>;
export function entries(AsyncTree: AsyncTree): Promise<IterableIterator<any>>;
export function forEach(AsyncTree: AsyncTree, callbackfn: (value: any, key: any) => Promise<void>): Promise<void>;
export function from(obj: any, options?: { deep?: boolean, parent?: AsyncTree|null }): AsyncTree;
export function has(AsyncTree: AsyncTree, key: any): Promise<boolean>;
export function isAsyncMutableTree(obj: any): obj is AsyncMutableTree;
export function isAsyncTree(obj: any): obj is AsyncTree;
export function isTraversable(obj: any): boolean;
export function isTreelike(obj: any): obj is Treelike;
export function map(tree: Treelike, options: TreeMapOptions|ValueKeyFn): AsyncTree;
export function mapReduce(tree: Treelike, mapFn: ValueKeyFn | null, reduceFn: ReduceFn): Promise<any>;
export function paths(tree: Treelike, base?: string): string[];
export function plain(tree: Treelike): Promise<PlainObject>;
export function root(tree: Treelike): AsyncTree;
export function remove(AsyncTree: AsyncMutableTree, key: any): Promise<boolean>;
export function toFunction(tree: Treelike): Function;
export function traverse(tree: Treelike, ...keys: any[]): Promise<any>;
export function traverseOrThrow(tree: Treelike, ...keys: any[]): Promise<any>;
export function traversePath(tree: Treelike, path: string): Promise<any>;
export function values(AsyncTree: AsyncTree): Promise<IterableIterator<any>>;

497
node_modules/@weborigami/async-tree/src/Tree.js generated vendored Normal file
View File

@@ -0,0 +1,497 @@
import DeferredTree from "./drivers/DeferredTree.js";
import FunctionTree from "./drivers/FunctionTree.js";
import MapTree from "./drivers/MapTree.js";
import SetTree from "./drivers/SetTree.js";
import { DeepObjectTree, ObjectTree } from "./internal.js";
import * as symbols from "./symbols.js";
import * as trailingSlash from "./trailingSlash.js";
import * as utilities from "./utilities.js";
import {
castArrayLike,
isPacked,
isPlainObject,
isUnpackable,
toPlainValue,
} from "./utilities.js";
/**
* Helper functions for working with async trees
*
* @typedef {import("../index.ts").PlainObject} PlainObject
* @typedef {import("../index.ts").ReduceFn} ReduceFn
* @typedef {import("../index.ts").Treelike} Treelike
* @typedef {import("../index.ts").ValueKeyFn} ValueKeyFn
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
*/
const treeModule = this;
/**
* Apply the key/values pairs from the source tree to the target tree.
*
* If a key exists in both trees, and the values in both trees are
* subtrees, then the subtrees will be merged recursively. Otherwise, the
* value from the source tree will overwrite the value in the target tree.
*
* @param {AsyncMutableTree} target
* @param {AsyncTree} source
*/
export async function assign(target, source) {
const targetTree = from(target);
const sourceTree = from(source);
if (!isAsyncMutableTree(targetTree)) {
throw new TypeError("Target must be a mutable asynchronous tree");
}
// Fire off requests to update all keys, then wait for all of them to finish.
const keys = Array.from(await sourceTree.keys());
const promises = keys.map(async (key) => {
const sourceValue = await sourceTree.get(key);
if (isAsyncTree(sourceValue)) {
const targetValue = await targetTree.get(key);
if (isAsyncMutableTree(targetValue)) {
// Both source and target are trees; recurse.
await assign(targetValue, sourceValue);
return;
}
}
// Copy the value from the source to the target.
await /** @type {any} */ (targetTree).set(key, sourceValue);
});
await Promise.all(promises);
return targetTree;
}
/**
* Removes all entries from the tree.
*
* @param {AsyncMutableTree} tree
*/
export async function clear(tree) {
const keys = Array.from(await tree.keys());
const promises = keys.map((key) => tree.set(key, undefined));
await Promise.all(promises);
}
/**
* Returns a new `Iterator` object that contains a two-member array of `[key,
* value]` for each element in the specific node of the tree.
*
* @param {AsyncTree} tree
*/
export async function entries(tree) {
const keys = Array.from(await tree.keys());
const promises = keys.map(async (key) => [key, await tree.get(key)]);
return Promise.all(promises);
}
/**
* Calls callbackFn once for each key-value pair present in the specific node of
* the tree.
*
* @param {AsyncTree} tree
* @param {Function} callbackFn
*/
export async function forEach(tree, callbackFn) {
const keys = Array.from(await tree.keys());
const promises = keys.map(async (key) => {
const value = await tree.get(key);
return callbackFn(value, key);
});
await Promise.all(promises);
}
/**
* Attempts to cast the indicated object to an async tree.
*
* If the object is a plain object, it will be converted to an ObjectTree. The
* optional `deep` option can be set to `true` to convert a plain object to a
* DeepObjectTree. The optional `parent` parameter will be used as the default
* parent of the new tree.
*
* @param {Treelike | Object} object
* @param {{ deep?: boolean, parent?: AsyncTree|null }} [options]
* @returns {AsyncTree}
*/
export function from(object, options = {}) {
const deep = options.deep ?? object[symbols.deep];
let tree;
if (isAsyncTree(object)) {
// Argument already supports the tree interface.
// @ts-ignore
return object;
} else if (typeof object === "function") {
tree = new FunctionTree(object);
} else if (object instanceof Map) {
tree = new MapTree(object);
} else if (object instanceof Set) {
tree = new SetTree(object);
} else if (isPlainObject(object) || object instanceof Array) {
tree = deep ? new DeepObjectTree(object) : new ObjectTree(object);
} else if (isUnpackable(object)) {
async function AsyncFunction() {} // Sample async function
tree =
object.unpack instanceof AsyncFunction.constructor
? // Async unpack: return a deferred tree.
new DeferredTree(object.unpack, { deep })
: // Synchronous unpack: cast the result of unpack() to a tree.
from(object.unpack());
} else if (object && typeof object === "object") {
// An instance of some class.
tree = new ObjectTree(object);
} else if (
typeof object === "string" ||
typeof object === "number" ||
typeof object === "boolean"
) {
// A primitive value; box it into an object and construct a tree.
const boxed = utilities.box(object);
tree = new ObjectTree(boxed);
} else {
throw new TypeError("Couldn't convert argument to an async tree");
}
if (!tree.parent && options.parent) {
tree.parent = options.parent;
}
return tree;
}
/**
* Returns a boolean indicating whether the specific node of the tree has a
* value for the given `key`.
*
* @param {AsyncTree} tree
* @param {any} key
*/
export async function has(tree, key) {
const value = await tree.get(key);
return value !== undefined;
}
/**
* Return true if the indicated object is an async tree.
*
* @param {any} obj
* @returns {obj is AsyncTree}
*/
export function isAsyncTree(obj) {
return (
obj !== null &&
typeof obj === "object" &&
typeof obj.get === "function" &&
typeof obj.keys === "function" &&
// JavaScript Map look like trees but can't be extended the same way, so we
// report false.
!(obj instanceof Map)
);
}
/**
* Return true if the indicated object is an async mutable tree.
*
* @param {any} obj
* @returns {obj is AsyncMutableTree}
*/
export function isAsyncMutableTree(obj) {
return (
isAsyncTree(obj) && typeof (/** @type {any} */ (obj).set) === "function"
);
}
/**
* Return true if the object can be traversed via the `traverse()` method. The
* object must be either treelike or a packed object with an `unpack()` method.
*
* @param {any} object
*/
export function isTraversable(object) {
return (
isTreelike(object) ||
(isPacked(object) && /** @type {any} */ (object).unpack instanceof Function)
);
}
/**
* Returns true if the indicated object can be directly treated as an
* asynchronous tree. This includes:
*
* - An object that implements the AsyncTree interface (including
* AsyncTree instances)
* - A function
* - An `Array` instance
* - A `Map` instance
* - A `Set` instance
* - A plain object
*
* Note: the `from()` method accepts any JavaScript object, but `isTreelike`
* returns `false` for an object that isn't one of the above types.
*
* @param {any} obj
* @returns {obj is Treelike}
*/
export function isTreelike(obj) {
return (
isAsyncTree(obj) ||
obj instanceof Array ||
obj instanceof Function ||
obj instanceof Map ||
obj instanceof Set ||
isPlainObject(obj)
);
}
/**
* Return a new tree with deeply-mapped values of the original tree.
*
* @param {Treelike} treelike
* @param {ValueKeyFn} valueFn
*/
export { default as map } from "./operations/map.js";
/**
* Map and reduce a tree.
*
* This is done in as parallel fashion as possible. Each of the tree's values
* will be requested in an async call, then those results will be awaited
* collectively. If a mapFn is provided, it will be invoked to convert each
* value to a mapped value; otherwise, values will be used as is. When the
* values have been obtained, all the values and keys will be passed to the
* reduceFn, which should consolidate those into a single result.
*
* @param {Treelike} treelike
* @param {ValueKeyFn|null} valueFn
* @param {ReduceFn} reduceFn
*/
export async function mapReduce(treelike, valueFn, reduceFn) {
const tree = from(treelike);
// We're going to fire off all the get requests in parallel, as quickly as
// the keys come in. We call the tree's `get` method for each key, but
// *don't* wait for it yet.
const keys = Array.from(await tree.keys());
const promises = keys.map((key) =>
tree.get(key).then((value) =>
// If the value is a subtree, recurse.
isAsyncTree(value)
? mapReduce(value, valueFn, reduceFn)
: valueFn
? valueFn(value, key, tree)
: value
)
);
// Wait for all the promises to resolve. Because the promises were captured
// in the same order as the keys, the values will also be in the same order.
const values = await Promise.all(promises);
// Reduce the values to a single result.
return reduceFn(values, keys, tree);
}
/**
* Returns slash-separated paths for all values in the tree.
*
* @param {Treelike} treelike
* @param {string?} base
*/
export async function paths(treelike, base = "") {
const tree = from(treelike);
const result = [];
for (const key of await tree.keys()) {
const separator = trailingSlash.has(base) ? "" : "/";
const valuePath = base ? `${base}${separator}${key}` : key;
const value = await tree.get(key);
if (await isAsyncTree(value)) {
const subPaths = await paths(value, valuePath);
result.push(...subPaths);
} else {
result.push(valuePath);
}
}
return result;
}
/**
* Converts an asynchronous tree into a synchronous plain JavaScript object.
*
* The result's keys will be the tree's keys cast to strings. Any tree value
* that is itself a tree will be similarly converted to a plain object.
*
* Any trailing slashes in keys will be removed.
*
* @param {Treelike} treelike
* @returns {Promise<PlainObject|Array>}
*/
export async function plain(treelike) {
return mapReduce(treelike, toPlainValue, (values, keys, tree) => {
// Special case for an empty tree: if based on array, return array.
if (tree instanceof ObjectTree && keys.length === 0) {
return /** @type {any} */ (tree).object instanceof Array ? [] : {};
}
// Normalize slashes in keys.
keys = keys.map(trailingSlash.remove);
return castArrayLike(keys, values);
});
}
/**
* Removes the value for the given key from the specific node of the tree.
*
* Note: The corresponding `Map` method is `delete`, not `remove`. However,
* `delete` is a reserved word in JavaScript, so this uses `remove` instead.
*
* @param {AsyncMutableTree} tree
* @param {any} key
*/
export async function remove(tree, key) {
const exists = await has(tree, key);
if (exists) {
await tree.set(key, undefined);
return true;
} else {
return false;
}
}
/**
* Walk up the `parent` chain to find the root of the tree.
*
* @param {AsyncTree} tree
*/
export function root(tree) {
let current = from(tree);
while (current.parent) {
current = current.parent;
}
return current;
}
/**
* Returns a function that invokes the tree's `get` method.
*
* @param {Treelike} treelike
* @returns {Function}
*/
export function toFunction(treelike) {
const tree = from(treelike);
return tree.get.bind(tree);
}
/**
* Return the value at the corresponding path of keys.
*
* @this {any}
* @param {Treelike} treelike
* @param {...any} keys
*/
export async function traverse(treelike, ...keys) {
try {
// Await the result here so that, if the path doesn't exist, the catch
// block below will catch the exception.
return await traverseOrThrow.call(this, treelike, ...keys);
} catch (/** @type {any} */ error) {
if (error instanceof TraverseError) {
return undefined;
} else {
throw error;
}
}
}
/**
* Return the value at the corresponding path of keys. Throw if any interior
* step of the path doesn't lead to a result.
*
* @this {AsyncTree|null|undefined}
* @param {Treelike} treelike
* @param {...any} keys
*/
export async function traverseOrThrow(treelike, ...keys) {
// Start our traversal at the root of the tree.
/** @type {any} */
let value = treelike;
let position = 0;
// If traversal operation was called with a `this` context, use that as the
// target for function calls.
const target = this === treeModule ? undefined : this;
// Process all the keys.
const remainingKeys = keys.slice();
let key;
while (remainingKeys.length > 0) {
if (value === undefined) {
throw new TraverseError("A null or undefined value can't be traversed", {
tree: treelike,
keys,
position,
});
}
// If the value is packed and can be unpacked, unpack it.
if (isUnpackable(value)) {
value = await value.unpack();
}
if (value instanceof Function) {
// Value is a function: call it with the remaining keys.
const fn = value;
// We'll take as many keys as the function's length, but at least one.
let fnKeyCount = Math.max(fn.length, 1);
const args = remainingKeys.splice(0, fnKeyCount);
key = null;
value = await fn.call(target, ...args);
} else if (isTraversable(value) || typeof value === "object") {
// Value is some other treelike object: cast it to a tree.
const tree = from(value);
// Get the next key.
key = remainingKeys.shift();
// Get the value for the key.
value = await tree.get(key);
} else {
// Value can't be traversed
throw new TraverseError("Tried to traverse a value that's not treelike", {
tree: treelike,
keys,
position,
});
}
position++;
}
return value;
}
/**
* Given a slash-separated path like "foo/bar", traverse the keys "foo/" and
* "bar" and return the resulting value.
*
* @param {Treelike} tree
* @param {string} path
*/
export async function traversePath(tree, path) {
const keys = utilities.keysFromPath(path);
return traverse(tree, ...keys);
}
// Error class thrown by traverseOrThrow()
class TraverseError extends ReferenceError {
constructor(message, options) {
super(message);
this.name = "TraverseError";
Object.assign(this, options);
}
}
/**
* Return the values in the specific node of the tree.
*
* @param {AsyncTree} tree
*/
export async function values(tree) {
const keys = Array.from(await tree.keys());
const promises = keys.map(async (key) => tree.get(key));
return Promise.all(promises);
}

View File

@@ -0,0 +1,176 @@
import { Tree } from "../internal.js";
import * as trailingSlash from "../trailingSlash.js";
import {
hiddenFileNames,
isStringLike,
naturalOrder,
setParent,
} from "../utilities.js";
const TypedArray = Object.getPrototypeOf(Uint8Array);
/**
* A tree of files backed by a browser-hosted file system such as the standard
* Origin Private File System or the (as of October 2023) experimental File
* System Access API.
*
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
* @implements {AsyncMutableTree}
*/
export default class BrowserFileTree {
/**
* Construct a tree of files backed by a browser-hosted file system.
*
* The directory handle can be obtained via any of the [methods that return a
* FileSystemDirectoryHandle](https://developer.mozilla.org/en-US/docs/Web/API/FileSystemDirectoryHandle).
* If no directory is supplied, the tree is rooted at the Origin Private File
* System for the current site.
*
* @param {FileSystemDirectoryHandle} [directoryHandle]
*/
constructor(directoryHandle) {
/** @type {FileSystemDirectoryHandle}
* @ts-ignore */
this.directory = directoryHandle;
}
async get(key) {
if (key == null) {
// Reject nullish key.
throw new ReferenceError(
`${this.constructor.name}: Cannot get a null or undefined key.`
);
}
if (key === "") {
// Can't have a file with no name
return undefined;
}
// Remove trailing slash if present
key = trailingSlash.remove(key);
const directory = await this.getDirectory();
// Try the key as a subfolder name
try {
const subfolderHandle = await directory.getDirectoryHandle(key);
const value = Reflect.construct(this.constructor, [subfolderHandle]);
setParent(value, this);
return value;
} catch (error) {
if (
!(
error instanceof DOMException &&
(error.name === "NotFoundError" || error.name === "TypeMismatchError")
)
) {
throw error;
}
}
// Try the key as a file name
try {
const fileHandle = await directory.getFileHandle(key);
const file = await fileHandle.getFile();
const buffer = file.arrayBuffer();
setParent(buffer, this);
return buffer;
} catch (error) {
if (!(error instanceof DOMException && error.name === "NotFoundError")) {
throw error;
}
}
return undefined;
}
// Return the directory handle, creating it if necessary. We can't create the
// default value in the constructor because we need to await it.
async getDirectory() {
this.directory ??= await navigator.storage.getDirectory();
return this.directory;
}
async keys() {
const directory = await this.getDirectory();
let keys = [];
// @ts-ignore
for await (const entryKey of directory.keys()) {
// Check if the entry is a subfolder
const baseKey = trailingSlash.remove(entryKey);
const subfolderHandle = await directory
.getDirectoryHandle(baseKey)
.catch(() => null);
const isSubfolder = subfolderHandle !== null;
const key = trailingSlash.toggle(entryKey, isSubfolder);
keys.push(key);
}
// Filter out unhelpful file names.
keys = keys.filter((key) => !hiddenFileNames.includes(key));
keys.sort(naturalOrder);
return keys;
}
async set(key, value) {
const baseKey = trailingSlash.remove(key);
const directory = await this.getDirectory();
if (value === undefined) {
// Delete file.
try {
await directory.removeEntry(baseKey);
} catch (error) {
// If the file didn't exist, ignore the error.
if (
!(error instanceof DOMException && error.name === "NotFoundError")
) {
throw error;
}
}
return this;
}
// Treat null value as empty string; will create an empty file.
if (value === null) {
value = "";
}
// True if fs.writeFile can directly write the value to a file.
let isWriteable =
value instanceof ArrayBuffer ||
value instanceof TypedArray ||
value instanceof DataView ||
value instanceof Blob;
if (!isWriteable && isStringLike(value)) {
// Value has a meaningful `toString` method, use that.
value = String(value);
isWriteable = true;
}
if (isWriteable) {
// Write file.
const fileHandle = await directory.getFileHandle(baseKey, {
create: true,
});
const writable = await fileHandle.createWritable();
await writable.write(value);
await writable.close();
} else if (Tree.isTreelike(value)) {
// Treat value as a tree and write it out as a subdirectory.
const subdirectory = await directory.getDirectoryHandle(baseKey, {
create: true,
});
const destTree = Reflect.construct(this.constructor, [subdirectory]);
await Tree.assign(destTree, value);
} else {
const typeName = value?.constructor?.name ?? "unknown";
throw new TypeError(`Cannot write a value of type ${typeName} as ${key}`);
}
return this;
}
}

View File

@@ -0,0 +1,23 @@
import { Tree } from "../internal.js";
import MapTree from "./MapTree.js";
export default class DeepMapTree extends MapTree {
async get(key) {
let value = await super.get(key);
if (value instanceof Map) {
value = Reflect.construct(this.constructor, [value]);
}
if (Tree.isAsyncTree(value) && !value.parent) {
value.parent = this;
}
return value;
}
/** @returns {boolean} */
isSubtree(value) {
return value instanceof Map || Tree.isAsyncTree(value);
}
}

View File

@@ -0,0 +1,19 @@
import { ObjectTree, Tree } from "../internal.js";
import { isPlainObject } from "../utilities.js";
export default class DeepObjectTree extends ObjectTree {
async get(key) {
let value = await super.get(key);
if (value instanceof Array || isPlainObject(value)) {
value = Reflect.construct(this.constructor, [value]);
}
return value;
}
/** @returns {boolean} */
isSubtree(value) {
return (
value instanceof Array || isPlainObject(value) || Tree.isAsyncTree(value)
);
}
}

View File

@@ -0,0 +1,81 @@
import { Tree } from "../internal.js";
/**
* A tree that is loaded lazily.
*
* This is useful in situations that must return a tree synchronously. If
* constructing the tree requires an asynchronous operation, this class can be
* used as a wrapper that can be returned immediately. The tree will be loaded
* the first time the keys() or get() functions are called.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @implements {AsyncTree}
*/
export default class DeferredTree {
/**
* @param {Function|Promise<any>} loader
* @param {{ deep?: boolean }} [options]
*/
constructor(loader, options) {
this.loader = loader;
this.treePromise = null;
this._tree = null;
this._parentUntilLoaded = null;
this._deep = options?.deep;
}
async get(key) {
const tree = await this.tree();
return tree.get(key);
}
async loadResult() {
if (!(this.loader instanceof Promise)) {
this.loader = this.loader();
}
return this.loader;
}
async keys() {
const tree = await this.tree();
return tree.keys();
}
// A deferred tree's parent generally comes from the loaded tree. However, if
// someone tries to get or set the parent before the tree is loaded, we store
// that parent reference and apply it once the tree is loaded.
get parent() {
return this._tree?.parent ?? this._parentUntilLoaded;
}
set parent(parent) {
if (this._tree && !this._tree.parent) {
this._tree.parent = parent;
} else {
this._parentUntilLoaded = parent;
}
}
async tree() {
if (this._tree) {
return this._tree;
}
// Use a promise to ensure the treelike is only converted to a tree once.
this.treePromise ??= this.loadResult().then((treelike) => {
const options =
this._deep !== undefined ? { deep: this._deep } : undefined;
this._tree = Tree.from(treelike, options);
if (this._parentUntilLoaded) {
// Now that the tree has been loaded, we can set its parent if it hasn't
// already been set.
if (!this._tree.parent) {
this._tree.parent = this._parentUntilLoaded;
}
this._parentUntilLoaded = null;
}
return this._tree;
});
return this.treePromise;
}
}

View File

@@ -0,0 +1,52 @@
import SiteTree from "./SiteTree.js";
/**
* A [SiteTree](SiteTree.html) that implements the [JSON Keys](jsonKeys.html)
* protocol. This enables a `keys()` method that can return the keys of a site
* route even though such a mechanism is not built into the HTTP protocol.
*/
export default class ExplorableSiteTree extends SiteTree {
constructor(...args) {
super(...args);
this.serverKeysPromise = undefined;
}
async getServerKeys() {
// We use a promise to ensure we only check for keys once.
const href = new URL(".keys.json", this.href).href;
this.serverKeysPromise ??= fetch(href)
.then((response) => (response.ok ? response.text() : null))
.then((text) => {
try {
return text ? JSON.parse(text) : null;
} catch (error) {
// Got a response, but it's not JSON. Most likely the site doesn't
// actually have a .keys.json file, and is returning a Not Found page,
// but hasn't set the correct 404 status code.
return null;
}
});
return this.serverKeysPromise;
}
/**
* Returns the keys of the site route. For this to work, the route must have a
* `.keys.json` file that contains a JSON array of string keys.
*
* @returns {Promise<Iterable<string>>}
*/
async keys() {
const serverKeys = await this.getServerKeys();
return serverKeys ?? [];
}
processResponse(response) {
// If the response was redirected to a route that ends with a slash, and the
// site is an explorable site, we return a tree for the new route.
if (response.ok && response.redirected && response.url.endsWith("/")) {
return Reflect.construct(this.constructor, [response.url]);
}
return super.processResponse(response);
}
}

View File

@@ -0,0 +1,268 @@
import * as fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { Tree } from "../internal.js";
import * as trailingSlash from "../trailingSlash.js";
import {
getRealmObjectPrototype,
hiddenFileNames,
isPacked,
isPlainObject,
naturalOrder,
setParent,
} from "../utilities.js";
/**
* A file system tree via the Node file system API.
*
* File values are returned as Uint8Array instances. The underlying Node fs API
* returns file contents as instances of the node-specific Buffer class, but
* that class has some incompatible method implementations; see
* https://nodejs.org/api/buffer.html#buffers-and-typedarrays. For greater
* compatibility, files are returned as standard Uint8Array instances instead.
*
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
* @implements {AsyncMutableTree}
*/
export default class FileTree {
/**
* @param {string|URL} location
*/
constructor(location) {
if (location instanceof URL) {
location = location.href;
} else if (
!(
typeof location === "string" ||
/** @type {any} */ (location) instanceof String
)
) {
throw new TypeError(
`FileTree constructor needs a string or URL, received an instance of ${
/** @type {any} */ (location)?.constructor?.name
}`
);
}
this.dirname = location.startsWith("file://")
? fileURLToPath(location)
: path.resolve(process.cwd(), location);
this.parent = null;
}
async get(key) {
if (key == null) {
// Reject nullish key
throw new ReferenceError(
`${this.constructor.name}: Cannot get a null or undefined key.`
);
}
if (key === "") {
// Can't have a file with no name
return undefined;
}
// Remove trailing slash if present
key = trailingSlash.remove(key);
const filePath = path.resolve(this.dirname, key);
let stats;
try {
stats = await fs.stat(filePath);
} catch (/** @type {any} */ error) {
if (error.code === "ENOENT" /* File not found */) {
return undefined;
}
throw error;
}
let value;
if (stats.isDirectory()) {
// Return subdirectory as a tree
value = Reflect.construct(this.constructor, [filePath]);
} else {
// Return file contents as a standard Uint8Array
const buffer = await fs.readFile(filePath);
value = Uint8Array.from(buffer);
}
setParent(value, this);
return value;
}
/**
* Enumerate the names of the files/subdirectories in this directory.
*/
async keys() {
let entries;
try {
entries = await fs.readdir(this.dirname, { withFileTypes: true });
} catch (/** @type {any} */ error) {
if (error.code !== "ENOENT") {
throw error;
}
entries = [];
}
// Add slashes to directory names.
let names = await Promise.all(
entries.map(async (entry) =>
trailingSlash.toggle(entry.name, await isDirectory(entry, this.dirname))
)
);
// Filter out unhelpful file names.
names = names.filter((name) => !hiddenFileNames.includes(name));
// Node fs.readdir sort order appears to be unreliable; see, e.g.,
// https://github.com/nodejs/node/issues/3232.
names.sort(naturalOrder);
return names;
}
get path() {
return this.dirname;
}
async set(key, value) {
// Where are we going to write this value?
const stringKey = key != null ? String(key) : "";
const baseKey = trailingSlash.remove(stringKey);
const destPath = path.resolve(this.dirname, baseKey);
if (value === undefined) {
// Delete the file or directory.
let stats;
try {
stats = await stat(destPath);
} catch (/** @type {any} */ error) {
if (error.code === "ENOENT" /* File not found */) {
return this;
}
throw error;
}
if (stats?.isDirectory()) {
// Delete directory.
await fs.rm(destPath, { recursive: true });
} else if (stats) {
// Delete file.
await fs.unlink(destPath);
}
return this;
}
if (typeof value === "function") {
// Invoke function; write out the result.
value = await value();
}
let packed = false;
if (value === null) {
// Treat null value as empty string; will create an empty file.
value = "";
packed = true;
} else if (!(value instanceof String) && isPacked(value)) {
// As of Node 22, fs.writeFile is incredibly slow for large String
// instances. Instead of treating a String instance as a Packed value, we
// want to consider it as a stringlike below. That will convert it to a
// primitive string before writing — which is orders of magnitude faster.
packed = true;
} else if (typeof value.pack === "function") {
// Pack the value for writing.
value = await value.pack();
packed = true;
} else if (isStringLike(value)) {
// Value has a meaningful `toString` method, use that.
value = String(value);
packed = true;
}
if (packed) {
// Single writeable value.
if (value instanceof ArrayBuffer) {
// Convert ArrayBuffer to Uint8Array, which Node.js can write directly.
value = new Uint8Array(value);
}
// Ensure this directory exists.
await fs.mkdir(this.dirname, { recursive: true });
// Write out the value as the contents of a file.
await fs.writeFile(destPath, value);
} else if (isPlainObject(value) && Object.keys(value).length === 0) {
// Special case: empty object means create an empty directory.
await fs.mkdir(destPath, { recursive: true });
} else if (Tree.isTreelike(value)) {
// Treat value as a subtree and write it out as a subdirectory.
const destTree = Reflect.construct(this.constructor, [destPath]);
// Create the directory here, even if the subtree is empty.
await fs.mkdir(destPath, { recursive: true });
// Write out the subtree.
await Tree.assign(destTree, value);
} else {
const typeName = value?.constructor?.name ?? "unknown";
throw new TypeError(
`Cannot write a value of type ${typeName} as ${stringKey}`
);
}
return this;
}
get url() {
return pathToFileURL(this.dirname);
}
}
/**
* Return true if the entry is a directory or is a symbolic link to a directory.
*/
async function isDirectory(entry, dirname) {
if (entry.isSymbolicLink()) {
const entryPath = path.resolve(dirname, entry.name);
try {
const realPath = await fs.realpath(entryPath);
entry = await fs.stat(realPath);
} catch (error) {
// The slash isn't crucial, so if link doesn't work that's okay
return false;
}
}
return entry.isDirectory();
}
/**
* Return true if the object is a string or object with a non-trival `toString`
* method.
*
* @param {any} obj
*/
function isStringLike(obj) {
if (typeof obj === "string") {
return true;
} else if (obj?.toString === undefined) {
return false;
} else if (obj.toString === getRealmObjectPrototype(obj)?.toString) {
// The stupid Object.prototype.toString implementation always returns
// "[object Object]", so if that's the only toString method the object has,
// we return false.
return false;
} else {
return true;
}
}
// Return the file information for the file/folder at the given path.
// If it does not exist, return undefined.
async function stat(filePath) {
try {
// Await the result here so that, if the file doesn't exist, the catch block
// below will catch the exception.
return await fs.stat(filePath);
} catch (/** @type {any} */ error) {
if (error.code === "ENOENT" /* File not found */) {
return undefined;
}
throw error;
}
}

View File

@@ -0,0 +1,46 @@
import { setParent } from "../utilities.js";
/**
* A tree defined by a function and an optional domain.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @implements {AsyncTree}
*/
export default class FunctionTree {
/**
* @param {function} fn the key->value function
* @param {Iterable<any>} [domain] optional domain of the function
*/
constructor(fn, domain = []) {
this.fn = fn;
this.domain = domain;
this.parent = null;
}
/**
* Return the application of the function to the given key.
*
* @param {any} key
*/
async get(key) {
const value =
this.fn.length <= 1
? // Function takes no arguments, one argument, or a variable number of
// arguments: invoke it.
await this.fn.call(this.parent, key)
: // Bind the key to the first parameter. Subsequent get calls will
// eventually bind all parameters until only one remains. At that point,
// the above condition will apply and the function will be invoked.
Reflect.construct(this.constructor, [this.fn.bind(this.parent, key)]);
setParent(value, this);
return value;
}
/**
* Enumerates the function's domain (if defined) as the tree's keys. If no domain
* was defined, this returns an empty iterator.
*/
async keys() {
return this.domain;
}
}

View File

@@ -0,0 +1,67 @@
import { Tree } from "../internal.js";
import * as trailingSlash from "../trailingSlash.js";
import { setParent } from "../utilities.js";
/**
* A tree backed by a JavaScript `Map` object.
*
* Note: By design, the standard `Map` class already complies with the
* `AsyncTree` interface. This class adds some additional tree behavior, such as
* constructing subtree instances and setting their `parent` property. While
* we'd like to construct this by subclassing `Map`, that class appears
* puzzingly and deliberately implemented to break subclasses.
*
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
* @implements {AsyncMutableTree}
*/
export default class MapTree {
/**
* @param {Iterable} [iterable]
*/
constructor(iterable = []) {
this.map = new Map(iterable);
this.parent = null;
}
async get(key) {
// Try key as is
let value = this.map.get(key);
if (value === undefined) {
// Try the other variation of the key
const alternateKey = trailingSlash.toggle(key);
value = this.map.get(alternateKey);
if (value === undefined) {
// Key doesn't exist
return undefined;
}
}
value = await value;
if (value === undefined) {
// Key exists but value is undefined
return undefined;
}
setParent(value, this);
return value;
}
/** @returns {boolean} */
isSubtree(value) {
return Tree.isAsyncTree(value);
}
async keys() {
const keys = [];
for (const [key, value] of this.map.entries()) {
keys.push(trailingSlash.toggle(key, this.isSubtree(value)));
}
return keys;
}
async set(key, value) {
this.map.set(key, value);
return this;
}
}

View File

@@ -0,0 +1,142 @@
import { Tree } from "../internal.js";
import * as symbols from "../symbols.js";
import * as trailingSlash from "../trailingSlash.js";
import { getRealmObjectPrototype, setParent } from "../utilities.js";
/**
* A tree defined by a plain object or array.
*
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
* @implements {AsyncMutableTree}
*/
export default class ObjectTree {
/**
* Create a tree wrapping a given plain object or array.
*
* @param {any} object The object/array to wrap.
*/
constructor(object) {
this.object = object;
this.parent = object[symbols.parent] ?? null;
}
/**
* Return the value for the given key.
*
* @param {any} key
*/
async get(key) {
if (key == null) {
// Reject nullish key.
throw new ReferenceError(
`${this.constructor.name}: Cannot get a null or undefined key.`
);
}
// Does the object have the key with or without a trailing slash?
const existingKey = findExistingKey(this.object, key);
if (existingKey === null) {
// Key doesn't exist
return undefined;
}
let value = await this.object[existingKey];
if (value === undefined) {
// Key exists but value is undefined
return undefined;
}
setParent(value, this);
if (typeof value === "function" && !Object.hasOwn(this.object, key)) {
// Value is an inherited method; bind it to the object.
value = value.bind(this.object);
}
return value;
}
/** @returns {boolean} */
isSubtree(value) {
return Tree.isAsyncTree(value);
}
/**
* Enumerate the object's keys.
*/
async keys() {
// Walk up the prototype chain to realm's Object.prototype.
let obj = this.object;
const objectPrototype = getRealmObjectPrototype(obj);
const result = new Set();
while (obj && obj !== objectPrototype) {
// Get the enumerable instance properties and the get/set properties.
const descriptors = Object.getOwnPropertyDescriptors(obj);
const propertyNames = Object.entries(descriptors)
.filter(
([name, descriptor]) =>
name !== "constructor" &&
(descriptor.enumerable ||
(descriptor.get !== undefined && descriptor.set !== undefined))
)
.map(([name, descriptor]) =>
trailingSlash.has(name)
? // Preserve existing slash
name
: // Add a slash if the value is a plain property and a subtree
trailingSlash.toggle(
name,
descriptor.value !== undefined &&
this.isSubtree(descriptor.value)
)
);
for (const name of propertyNames) {
result.add(name);
}
obj = Object.getPrototypeOf(obj);
}
return result;
}
/**
* Set the value for the given key. If the value is undefined, delete the key.
*
* @param {any} key
* @param {any} value
*/
async set(key, value) {
const existingKey = findExistingKey(this.object, key);
if (value === undefined) {
// Delete the key if it exists.
if (existingKey !== null) {
delete this.object[existingKey];
}
} else {
// If the key exists under a different form, delete the existing key.
if (existingKey !== null && existingKey !== key) {
delete this.object[existingKey];
}
// Set the value for the key.
this.object[key] = value;
}
return this;
}
}
function findExistingKey(object, key) {
// First try key as is
if (key in object) {
return key;
}
// Try alternate form
const alternateKey = trailingSlash.toggle(key);
if (alternateKey in object) {
return alternateKey;
}
return null;
}

View File

@@ -0,0 +1,34 @@
import { setParent } from "../utilities.js";
/**
* A tree of Set objects.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @implements {AsyncTree}
*/
export default class SetTree {
/**
* @param {Set} set
*/
constructor(set) {
this.values = Array.from(set);
this.parent = null;
}
async get(key) {
if (key == null) {
// Reject nullish key.
throw new ReferenceError(
`${this.constructor.name}: Cannot get a null or undefined key.`
);
}
const value = this.values[key];
setParent(value, this);
return value;
}
async keys() {
return this.values.keys();
}
}

View File

@@ -0,0 +1,123 @@
import * as trailingSlash from "../trailingSlash.js";
import { setParent } from "../utilities.js";
/**
* A tree of values obtained via HTTP/HTTPS calls. These values will be strings
* for HTTP responses with a MIME text type; otherwise they will be ArrayBuffer
* instances.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @implements {AsyncTree}
*/
export default class SiteTree {
/**
* @param {string} href
*/
constructor(href = window?.location.href) {
if (href?.startsWith(".") && window?.location !== undefined) {
// URL represents a relative path; concatenate with current location.
href = new URL(href, window.location.href).href;
}
// Add trailing slash if not present; URL should represent a directory.
href = trailingSlash.add(href);
this.href = href;
this.parent = null;
}
/** @returns {Promise<any>} */
async get(key) {
if (key == null) {
// Reject nullish key.
throw new ReferenceError(
`${this.constructor.name}: Cannot get a null or undefined key.`
);
}
// A key with a trailing slash and no extension is for a folder; return a
// subtree without making a network request.
if (trailingSlash.has(key) && !key.includes(".")) {
const href = new URL(key, this.href).href;
const value = Reflect.construct(this.constructor, [href]);
setParent(value, this);
return value;
}
// HACK: For now we don't allow lookup of Origami extension handlers.
if (key.endsWith(".handler")) {
return undefined;
}
const href = new URL(key, this.href).href;
// Fetch the data at the given route.
let response;
try {
response = await fetch(href);
} catch (error) {
return undefined;
}
return this.processResponse(response);
}
/**
* Returns an empty set of keys.
*
* For a variation of `SiteTree` that can return the keys for a site route,
* see [ExplorableSiteTree](ExplorableSiteTree.html).
*
* @returns {Promise<Iterable<string>>}
*/
async keys() {
return [];
}
// Return true if the given media type is a standard text type.
static mediaTypeIsText(mediaType) {
if (!mediaType) {
return false;
}
const regex = /^(?<type>[^/]+)\/(?<subtype>[^;]+)/;
const match = mediaType.match(regex);
if (!match) {
return false;
}
const { type, subtype } = match.groups;
if (type === "text") {
return true;
}
return (
subtype === "json" ||
subtype.endsWith("+json") ||
subtype.endsWith(".json") ||
subtype === "xml" ||
subtype.endsWith("+xml") ||
subtype.endsWith(".xml")
);
}
get path() {
return this.href;
}
processResponse(response) {
if (!response.ok) {
return undefined;
}
const mediaType = response.headers?.get("Content-Type");
if (SiteTree.mediaTypeIsText(mediaType)) {
return response.text();
} else {
const buffer = response.arrayBuffer();
setParent(buffer, this);
return buffer;
}
}
get url() {
return new URL(this.href);
}
}

View File

@@ -0,0 +1,174 @@
import * as trailingSlash from "../trailingSlash.js";
/**
* Return a tree of years, months, and days from a start date to an end date.
*
* Both the start and end date can be provided in "YYYY-MM-DD", "YYYY-MM", or
* "YYYY" format. If a start year is provided, but a month is not, then the
* first month of the year will be used; if a start month is provided, but a day
* is not, then the first day of that month will be used. Similar logic applies
* to the end date, using the last month of the year or the last day of the
* month.
*
* If a start date is omitted, today will be used, likewise for the end date.
*
* @typedef {string|undefined} CalendarOptionsDate
* @typedef {( year: string, month: string, day: string ) => any} CalendarOptionsFn
* @param {{ end?: CalendarOptionsDate, start?: CalendarOptionsDate, value: CalendarOptionsFn }} options
*/
export default function calendarTree(options) {
const start = dateParts(options.start);
const end = dateParts(options.end);
const valueFn = options.value;
// Fill in the missing parts of the start and end dates.
const today = new Date();
if (start.day === undefined) {
start.day = start.year ? 1 : today.getDate();
}
if (start.month === undefined) {
start.month = start.year ? 1 : today.getMonth() + 1;
}
if (start.year === undefined) {
start.year = today.getFullYear();
}
if (end.day === undefined) {
end.day = end.month
? daysInMonth(end.year, end.month)
: end.year
? 31 // Last day of December
: today.getDate();
}
if (end.month === undefined) {
end.month = end.year ? 12 : today.getMonth() + 1;
}
if (end.year === undefined) {
end.year = today.getFullYear();
}
return yearsTree(start, end, valueFn);
}
function dateParts(date) {
let year;
let month;
let day;
if (typeof date === "string") {
const parts = date.split("-");
year = parts[0] ? parseInt(parts[0]) : undefined;
month = parts[1] ? parseInt(parts[1]) : undefined;
day = parts[2] ? parseInt(parts[2]) : undefined;
}
return { year, month, day };
}
function daysForMonthTree(year, month, start, end, valueFn) {
return {
async get(day) {
day = parseInt(trailingSlash.remove(day));
return this.inRange(day)
? valueFn(year.toString(), twoDigits(month), twoDigits(day))
: undefined;
},
inRange(day) {
if (year === start.year && year === end.year) {
if (month === start.month && month === end.month) {
return day >= start.day && day <= end.day;
} else if (month === start.month) {
return day >= start.day;
} else if (month === end.month) {
return day <= end.day;
} else {
return true;
}
} else if (year === start.year) {
if (month === start.month) {
return day >= start.day;
} else {
return month > start.month;
}
} else if (year === end.year) {
if (month === end.month) {
return day <= end.day;
} else {
return month < end.month;
}
} else {
return true;
}
},
async keys() {
const days = Array.from(
{ length: daysInMonth(year, month) },
(_, i) => i + 1
);
return days
.filter((day) => this.inRange(day))
.map((day) => twoDigits(day));
},
};
}
function daysInMonth(year, month) {
return new Date(year, month, 0).getDate();
}
function monthsForYearTree(year, start, end, valueFn) {
return {
async get(month) {
month = parseInt(trailingSlash.remove(month));
return this.inRange(month)
? daysForMonthTree(year, month, start, end, valueFn)
: undefined;
},
inRange(month) {
if (year === start.year && year === end.year) {
return month >= start.month && month <= end.month;
} else if (year === start.year) {
return month >= start.month;
} else if (year === end.year) {
return month <= end.month;
} else {
return true;
}
},
async keys() {
const months = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];
return months
.filter((month) => this.inRange(month))
.map((month) => twoDigits(month));
},
};
}
function twoDigits(number) {
return number.toString().padStart(2, "0");
}
function yearsTree(start, end, valueFn) {
return {
async get(year) {
year = parseInt(trailingSlash.remove(year));
return this.inRange(year)
? monthsForYearTree(year, start, end, valueFn)
: undefined;
},
inRange(year) {
return year >= start.year && year <= end.year;
},
async keys() {
return Array.from(
{ length: end.year - start.year + 1 },
(_, i) => start.year + i
);
},
};
}

140
node_modules/@weborigami/async-tree/src/extension.js generated vendored Normal file
View File

@@ -0,0 +1,140 @@
import * as trailingSlash from "./trailingSlash.js";
import { isStringLike, toString } from "./utilities.js";
/**
* Replicate the logic of Node POSIX path.extname at
* https://github.com/nodejs/node/blob/main/lib/path.js so that we can use this
* in the browser.
*
* @param {string} path
* @returns {string}
*/
export function extname(path) {
if (typeof path !== "string") {
throw new TypeError(`Expected a string, got ${typeof path}`);
}
let startDot = -1;
let startPart = 0;
let end = -1;
let matchedSlash = true;
// Track the state of characters (if any) we see before our first dot and
// after any path separator we find
let preDotState = 0;
for (let i = path.length - 1; i >= 0; --i) {
const char = path[i];
if (char === "/") {
// If we reached a path separator that was not part of a set of path
// separators at the end of the string, stop now
if (!matchedSlash) {
startPart = i + 1;
break;
}
continue;
}
if (end === -1) {
// We saw the first non-path separator, mark this as the end of our
// extension
matchedSlash = false;
end = i + 1;
}
if (char === ".") {
// If this is our first dot, mark it as the start of our extension
if (startDot === -1) startDot = i;
else if (preDotState !== 1) preDotState = 1;
} else if (startDot !== -1) {
// We saw a non-dot and non-path separator before our dot, so we should
// have a good chance at having a non-empty extension
preDotState = -1;
}
}
if (
startDot === -1 ||
end === -1 ||
// We saw a non-dot character immediately before the dot
preDotState === 0 ||
// The (right-most) trimmed path component is exactly '..'
(preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)
) {
return "";
}
return path.slice(startDot, end);
}
/**
* See if the key ends with the given extension. If it does, return the base
* name without the extension; if it doesn't return null.
*
* If the extension is empty, the key must not have an extension to match.
*
* If the extension is a slash, then the key must end with a slash for the match
* to succeed. Otherwise, a trailing slash in the key is ignored for purposes of
* comparison to comply with the way Origami can unpack files. Example: the keys
* "data.json" and "data.json/" are treated equally.
*
* This uses a different, more general interpretation of "extension" to mean any
* suffix, rather than Node's interpretation in `extname`. In particular, this
* will match a multi-part extension like ".foo.bar" that contains more than one
* dot.
*/
export function match(key, ext) {
if (!isStringLike(key)) {
return null;
}
key = toString(key);
if (ext === "/") {
return trailingSlash.has(key) ? trailingSlash.remove(key) : null;
}
// Key matches if it ends with the same extension
const normalized = trailingSlash.remove(key);
if (normalized.endsWith(ext)) {
const removed =
ext.length > 0 ? normalized.slice(0, -ext.length) : normalized;
return trailingSlash.toggle(removed, trailingSlash.has(key));
}
// Didn't match
return null;
}
/**
* If the given key ends in the source extension (which will generally include a
* period), replace that extension with the result extension (which again should
* generally include a period). Otherwise, return the key as is.
*
* If the key ends in a trailing slash, that will be preserved in the result.
* Exception: if the source extension is empty, and the key doesn't have an
* extension, the result extension will be appended to the key without a slash.
*
* @param {string} key
* @param {string} sourceExtension
* @param {string} resultExtension
*/
export function replace(key, sourceExtension, resultExtension) {
if (!isStringLike(key)) {
return null;
}
key = toString(key);
if (!match(key, sourceExtension)) {
return key;
}
let replaced;
const normalizedKey = trailingSlash.remove(key);
if (sourceExtension === "") {
replaced = normalizedKey + resultExtension;
if (!normalizedKey.includes(".")) {
return replaced;
}
} else if (sourceExtension === "/") {
return trailingSlash.remove(key) + resultExtension;
} else {
replaced =
normalizedKey.slice(0, -sourceExtension.length) + resultExtension;
}
return trailingSlash.toggle(replaced, trailingSlash.has(key));
}

16
node_modules/@weborigami/async-tree/src/internal.js generated vendored Normal file
View File

@@ -0,0 +1,16 @@
//
// This library includes a number of modules with circular dependencies. This
// module exists to explicitly set the loading order for those modules. To
// enforce use of this loading order, other modules should only load the modules
// below via this module.
//
// About this pattern:
// https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de
//
// Note: to avoid having VS Code auto-sort the imports, keep lines between them.
export * as Tree from "./Tree.js";
export { default as ObjectTree } from "./drivers/ObjectTree.js";
export { default as DeepObjectTree } from "./drivers/DeepObjectTree.js";

View File

@@ -0,0 +1,4 @@
import { Treelike } from "../index.ts";
export function parse(json: string): any;
export function stringify(treelike: Treelike): Promise<string>;

23
node_modules/@weborigami/async-tree/src/jsonKeys.js generated vendored Normal file
View File

@@ -0,0 +1,23 @@
import { Tree } from "./internal.js";
/**
* The JSON Keys protocol lets a site expose the keys of a node in the site so
* that they can be read by SiteTree.
*
* This file format is a JSON array of key descriptors: a string like
* "index.html" for a specific resource available at the node, or a string with
* a trailing slash like "about/" for a subtree of that node.
*/
/**
* Given a tree node, return a JSON string that can be written to a .keys.json
* file.
*/
export async function stringify(treelike) {
const tree = Tree.from(treelike);
let keys = Array.from(await tree.keys());
// Skip the key `.keys.json` if present.
keys = keys.filter((key) => key !== ".keys.json");
const json = JSON.stringify(keys);
return json;
}

View File

@@ -0,0 +1,98 @@
import { ObjectTree, Tree } from "../internal.js";
/**
* Caches values from a source tree in a second cache tree. Cache source tree
* keys in memory.
*
* If no second tree is supplied, an in-memory value cache is used.
*
* An optional third filter tree can be supplied. If a filter tree is supplied,
* only values for keys that match the filter will be cached.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @typedef {import("@weborigami/types").AsyncMutableTree} AsyncMutableTree
* @typedef {import("../../index.ts").Treelike} Treelike
*
* @param {Treelike} sourceTreelike
* @param {AsyncMutableTree} [cacheTreelike]
* @param {Treelike} [filterTreelike]
* @returns {AsyncTree & { description: string }}
*/
export default function treeCache(
sourceTreelike,
cacheTreelike,
filterTreelike
) {
if (!sourceTreelike) {
const error = new TypeError(`cache: The source tree isn't defined.`);
/** @type {any} */ (error).position = 0;
throw error;
}
const source = Tree.from(sourceTreelike);
const filter = filterTreelike ? Tree.from(filterTreelike) : undefined;
/** @type {AsyncMutableTree} */
let cache;
if (cacheTreelike) {
// @ts-ignore
cache = Tree.from(cacheTreelike);
if (!Tree.isAsyncMutableTree(cache)) {
throw new Error("Cache tree must define a set() method.");
}
} else {
cache = new ObjectTree({});
}
let keys;
return {
description: "cache",
async get(key) {
// Check cache tree first.
let cacheValue = await cache.get(key);
if (cacheValue !== undefined && !Tree.isAsyncTree(cacheValue)) {
// Leaf node cache hit
return cacheValue;
}
// Cache miss or interior node cache hit.
let value = await source.get(key);
if (value !== undefined) {
// If a filter is defined, does the key match the filter?
const filterValue = filter ? await filter.get(key) : undefined;
const filterMatch = !filter || filterValue !== undefined;
if (filterMatch) {
if (Tree.isAsyncTree(value)) {
// Construct merged tree for a tree result.
if (cacheValue === undefined) {
// Construct new empty container in cache
await cache.set(key, {});
cacheValue = await cache.get(key);
if (!Tree.isAsyncTree(cacheValue)) {
// Coerce to tree and then save it back to the cache. This is
// necessary, e.g., if cache is an ObjectTree; we want the
// subtree to also be an ObjectTree, not a plain object.
cacheValue = Tree.from(cacheValue);
await cache.set(key, cacheValue);
}
}
value = treeCache(value, cacheValue, filterValue);
} else {
// Save in cache before returning.
await cache.set(key, value);
}
}
return value;
}
return undefined;
},
async keys() {
keys ??= await source.keys();
return keys;
},
};
}

View File

@@ -0,0 +1,124 @@
import * as trailingSlash from "../trailingSlash.js";
const treeToCaches = new WeakMap();
/**
* Given a key function, return a new key function and inverse key function that
* cache the results of the original.
*
* If `skipSubtrees` is true, the inverse key function will skip any source keys
* that are keys for subtrees, returning the source key unmodified.
*
* @typedef {import("../../index.ts").KeyFn} KeyFn
*
* @param {KeyFn} keyFn
* @param {boolean?} skipSubtrees
* @returns {{ key: KeyFn, inverseKey: KeyFn }}
*/
export default function cachedKeyFunctions(keyFn, skipSubtrees = false) {
return {
async inverseKey(resultKey, tree) {
const { resultKeyToSourceKey, sourceKeyToResultKey } =
getKeyMapsForTree(tree);
const cachedSourceKey = searchKeyMap(resultKeyToSourceKey, resultKey);
if (cachedSourceKey !== undefined) {
return cachedSourceKey;
}
// Iterate through the tree's keys, calculating source keys as we go,
// until we find a match. Cache all the intermediate results and the
// final match. This is O(n), but we stop as soon as we find a match,
// and subsequent calls will benefit from the intermediate results.
const resultKeyWithoutSlash = trailingSlash.remove(resultKey);
for (const sourceKey of await tree.keys()) {
// Skip any source keys we already know about.
if (sourceKeyToResultKey.has(sourceKey)) {
continue;
}
const computedResultKey = await computeAndCacheResultKey(
tree,
keyFn,
skipSubtrees,
sourceKey
);
if (
computedResultKey &&
trailingSlash.remove(computedResultKey) === resultKeyWithoutSlash
) {
// Match found, match trailing slash and return
return trailingSlash.toggle(sourceKey, trailingSlash.has(resultKey));
}
}
return undefined;
},
async key(sourceKey, tree) {
const { sourceKeyToResultKey } = getKeyMapsForTree(tree);
const cachedResultKey = searchKeyMap(sourceKeyToResultKey, sourceKey);
if (cachedResultKey !== undefined) {
return cachedResultKey;
}
const resultKey = await computeAndCacheResultKey(
tree,
keyFn,
skipSubtrees,
sourceKey
);
return resultKey;
},
};
}
async function computeAndCacheResultKey(tree, keyFn, skipSubtrees, sourceKey) {
const { resultKeyToSourceKey, sourceKeyToResultKey } =
getKeyMapsForTree(tree);
const resultKey =
skipSubtrees && trailingSlash.has(sourceKey)
? sourceKey
: await keyFn(sourceKey, tree);
sourceKeyToResultKey.set(sourceKey, resultKey);
resultKeyToSourceKey.set(resultKey, sourceKey);
return resultKey;
}
// Maintain key->inverseKey and inverseKey->key mappings for each tree. These
// store subtree keys in either direction with a trailing slash.
function getKeyMapsForTree(tree) {
let keyMaps = treeToCaches.get(tree);
if (!keyMaps) {
keyMaps = {
resultKeyToSourceKey: new Map(),
sourceKeyToResultKey: new Map(),
};
treeToCaches.set(tree, keyMaps);
}
return keyMaps;
}
// Search the given key map for the key. Ignore trailing slashes in the search,
// but preserve them in the result.
function searchKeyMap(keyMap, key) {
// Check key as is
let match;
if (keyMap.has(key)) {
match = keyMap.get(key);
} else if (!trailingSlash.has(key)) {
// Check key without trailing slash
const withSlash = trailingSlash.add(key);
if (keyMap.has(withSlash)) {
match = keyMap.get(withSlash);
}
}
return match
? trailingSlash.toggle(match, trailingSlash.has(key))
: undefined;
}

View File

@@ -0,0 +1,34 @@
import { toString } from "../utilities.js";
import deepValuesIterator from "./deepValuesIterator.js";
/**
* Concatenate the deep text values in a tree.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
*
* @this {AsyncTree|null}
* @param {import("../../index.ts").Treelike} treelike
*/
export default async function concatTreeValues(treelike) {
if (!treelike) {
const error = new TypeError(`concat: The tree isn't defined.`);
/** @type {any} */ (error).position = 0;
throw error;
}
const strings = [];
for await (const value of deepValuesIterator(treelike, { expand: true })) {
let string;
if (value === null) {
console.warn("Warning: Origami template encountered a null value");
string = "null";
} else if (value === undefined) {
console.warn("Warning: Origami template encountered an undefined value");
string = "undefined";
} else {
string = toString(value);
}
strings.push(string);
}
return strings.join("");
}

View File

@@ -0,0 +1,77 @@
import { Tree } from "../internal.js";
import * as trailingSlash from "../trailingSlash.js";
/**
* Return a tree that performs a deep merge of the given trees.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @param {import("../../index.ts").Treelike[]} sources
* @returns {AsyncTree & { description: string }}
*/
export default function deepMerge(...sources) {
let trees = sources.map((treelike) => Tree.from(treelike, { deep: true }));
let mergeParent;
return {
description: "deepMerge",
async get(key) {
const subtrees = [];
// Check trees for the indicated key in reverse order.
for (let index = trees.length - 1; index >= 0; index--) {
const tree = trees[index];
const value = await tree.get(key);
if (Tree.isAsyncTree(value)) {
if (value.parent === tree) {
// Merged tree acts as parent instead of the source tree.
value.parent = this;
}
subtrees.unshift(value);
} else if (value !== undefined) {
return value;
}
}
if (subtrees.length > 1) {
const merged = deepMerge(...subtrees);
merged.parent = this;
return merged;
} else if (subtrees.length === 1) {
return subtrees[0];
} else {
return undefined;
}
},
async keys() {
const keys = new Set();
// Collect keys in the order the trees were provided.
for (const tree of trees) {
for (const key of await tree.keys()) {
// Remove the alternate form of the key (if it exists)
const alternateKey = trailingSlash.toggle(key);
if (alternateKey !== key) {
keys.delete(alternateKey);
}
keys.add(key);
}
}
return keys;
},
get parent() {
return mergeParent;
},
set parent(parent) {
mergeParent = parent;
trees = sources.map((treelike) => {
const tree = Tree.isAsyncTree(treelike)
? Object.create(treelike)
: Tree.from(treelike);
tree.parent = parent;
return tree;
});
},
};
}

View File

@@ -0,0 +1,37 @@
import { Tree } from "../internal.js";
/**
* Reverse the order of keys at all levels of the tree.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @typedef {import("../../index.ts").Treelike} Treelike
*
* @param {Treelike} treelike
* @returns {AsyncTree}
*/
export default function deepReverse(treelike) {
if (!treelike) {
const error = new TypeError(
`deepReverse: The tree to reverse isn't defined.`
);
/** @type {any} */ (error).position = 0;
throw error;
}
const tree = Tree.from(treelike, { deep: true });
return {
async get(key) {
let value = await tree.get(key);
if (Tree.isAsyncTree(value)) {
value = deepReverse(value);
}
return value;
},
async keys() {
const keys = Array.from(await tree.keys());
keys.reverse();
return keys;
},
};
}

View File

@@ -0,0 +1,42 @@
import { Tree } from "../internal.js";
/**
* Returns a function that traverses a tree deeply and returns the values of the
* first `count` keys.
*
* This is similar to `deepValues`, but it is more efficient for large trees as
* stops after `count` values.
*
* @param {import("../../index.ts").Treelike} treelike
* @param {number} count
*/
export default async function deepTake(treelike, count) {
if (!treelike) {
const error = new TypeError(`deepTake: The tree isn't defined.`);
/** @type {any} */ (error).position = 0;
throw error;
}
const tree = await Tree.from(treelike, { deep: true });
const { values } = await traverse(tree, count);
return Tree.from(values, { deep: true });
}
async function traverse(tree, count) {
const values = [];
for (const key of await tree.keys()) {
if (count <= 0) {
break;
}
let value = await tree.get(key);
if (Tree.isAsyncTree(value)) {
const traversed = await traverse(value, count);
values.push(...traversed.values);
count = traversed.count;
} else {
values.push(value);
count--;
}
}
return { count, values };
}

View File

@@ -0,0 +1,19 @@
import deepValuesIterator from "./deepValuesIterator.js";
/**
* Return the in-order exterior values of a tree as a flat array.
*
* @param {import("../../index.ts").Treelike} treelike
* @param {{ expand?: boolean }} [options]
*/
export default async function deepValues(
treelike,
options = { expand: false }
) {
const iterator = deepValuesIterator(treelike, options);
const values = [];
for await (const value of iterator) {
values.push(value);
}
return values;
}

View File

@@ -0,0 +1,37 @@
import { Tree } from "../internal.js";
/**
* Return an iterator that yields all values in a tree, including nested trees.
*
* If the `expand` option is true, treelike values (but not functions) will be
* expanded into nested trees and their values will be yielded.
*
* @param {import("../../index.ts").Treelike} treelike
* @param {{ expand?: boolean }} [options]
* @returns {AsyncGenerator<any, void, undefined>}
*/
export default async function* deepValuesIterator(
treelike,
options = { expand: false }
) {
if (!treelike) {
const error = new TypeError(`deepValues: The tree isn't defined.`);
/** @type {any} */ (error).position = 0;
throw error;
}
const tree = Tree.from(treelike, { deep: true });
for (const key of await tree.keys()) {
let value = await tree.get(key);
// Recurse into child trees, but don't expand functions.
const recurse =
Tree.isAsyncTree(value) ||
(options.expand && typeof value !== "function" && Tree.isTreelike(value));
if (recurse) {
yield* deepValuesIterator(value, options);
} else {
yield value;
}
}
}

View File

@@ -0,0 +1,53 @@
import { ObjectTree, Tree } from "../internal.js";
/**
* Given a function that returns a grouping key for a value, returns a transform
* that applies that grouping function to a tree.
*
* @param {import("../../index.ts").Treelike} treelike
* @param {import("../../index.ts").ValueKeyFn} groupKeyFn
*/
export default async function group(treelike, groupKeyFn) {
if (!treelike) {
const error = new TypeError(`groupBy: The tree to group isn't defined.`);
/** @type {any} */ (error).position = 0;
throw error;
}
const tree = Tree.from(treelike);
const keys = Array.from(await tree.keys());
// Are all the keys integers?
const isArray = keys.every((key) => !Number.isNaN(parseInt(key)));
const result = {};
for (const key of await tree.keys()) {
const value = await tree.get(key);
// Get the groups for this value.
let groups = await groupKeyFn(value, key, tree);
if (!groups) {
continue;
}
if (!Tree.isTreelike(groups)) {
// A single value was returned
groups = [groups];
}
groups = Tree.from(groups);
// Add the value to each group.
for (const group of await Tree.values(groups)) {
if (isArray) {
result[group] ??= [];
result[group].push(value);
} else {
result[group] ??= {};
result[group][key] = value;
}
}
}
return new ObjectTree(result);
}

View File

@@ -0,0 +1,20 @@
import { Tree } from "../internal.js";
export default function invokeFunctions(treelike) {
const tree = Tree.from(treelike);
return {
async get(key) {
let value = await tree.get(key);
if (typeof value === "function") {
value = value();
} else if (Tree.isAsyncTree(value)) {
value = invokeFunctions(value);
}
return value;
},
async keys() {
return tree.keys();
},
};
}

View File

@@ -0,0 +1,48 @@
import * as extension from "../extension.js";
import * as trailingSlash from "../trailingSlash.js";
/**
* Given a source resultExtension and a result resultExtension, return a pair of key
* functions that map between them.
*
* The resulting `inverseKey` and `key` functions are compatible with those
* expected by map and other transforms.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @param {{ resultExtension?: string, sourceExtension: string }}
* options
*/
export default function keyFunctionsForExtensions({
resultExtension,
sourceExtension,
}) {
if (resultExtension === undefined) {
resultExtension = sourceExtension;
}
checkDeprecatedExtensionWithoutDot(resultExtension);
checkDeprecatedExtensionWithoutDot(sourceExtension);
return {
async inverseKey(resultKey, tree) {
// Remove trailing slash so that mapFn won't inadvertently unpack files.
const baseKey = trailingSlash.remove(resultKey);
const basename = extension.match(baseKey, resultExtension);
return basename ? `${basename}${sourceExtension}` : undefined;
},
async key(sourceKey, tree) {
return extension.match(sourceKey, sourceExtension)
? extension.replace(sourceKey, sourceExtension, resultExtension)
: undefined;
},
};
}
function checkDeprecatedExtensionWithoutDot(extension) {
if (extension && extension !== "/" && !extension.startsWith(".")) {
throw new RangeError(
`map: Warning: the extension "${extension}" should start with a period.`
);
}
}

View File

@@ -0,0 +1,129 @@
import { Tree } from "../internal.js";
import * as trailingSlash from "../trailingSlash.js";
/**
* Transform the keys and/or values of a tree.
*
* @typedef {import("../../index.ts").KeyFn} KeyFn
* @typedef {import("../../index.ts").TreeMapOptions} MapOptions
* @typedef {import("../../index.ts").ValueKeyFn} ValueKeyFn
*
* @param {import("../../index.ts").Treelike} treelike
* @param {MapOptions|ValueKeyFn} options
*/
export default function map(treelike, options = {}) {
let deep;
let description;
let inverseKeyFn;
let keyFn;
let needsSourceValue;
let valueFn;
if (!treelike) {
const error = new TypeError(`map: The tree to map isn't defined.`);
/** @type {any} */ (error).position = 0;
throw error;
}
if (typeof options === "function") {
// Take the single function argument as the valueFn
valueFn = options;
} else {
deep = options.deep;
description = options.description;
inverseKeyFn = options.inverseKey;
keyFn = options.key;
needsSourceValue = options.needsSourceValue;
valueFn = options.value;
}
deep ??= false;
description ??= "key/value map";
// @ts-ignore
inverseKeyFn ??= valueFn?.inverseKey;
// @ts-ignore
keyFn ??= valueFn?.key;
needsSourceValue ??= true;
if ((keyFn && !inverseKeyFn) || (!keyFn && inverseKeyFn)) {
throw new TypeError(
`map: You must specify both key and inverseKey functions, or neither.`
);
}
/**
* @param {import("@weborigami/types").AsyncTree} tree
*/
function mapFn(tree) {
// The transformed tree is actually an extension of the original tree's
// prototype chain. This allows the transformed tree to inherit any
// properties/methods. For example, the `parent` of the transformed tree is
// the original tree's parent.
const transformed = Object.create(tree);
transformed.description = description;
if (keyFn || valueFn) {
transformed.get = async (resultKey) => {
// Step 1: Map the result key to the source key.
const sourceKey = (await inverseKeyFn?.(resultKey, tree)) ?? resultKey;
if (sourceKey === undefined) {
// No source key means no value.
return undefined;
}
// Step 2: Get the source value.
let sourceValue;
if (needsSourceValue) {
// Normal case: get the value from the source tree.
sourceValue = await tree.get(sourceKey);
} else if (deep && trailingSlash.has(sourceKey)) {
// Only get the source value if it's expected to be a subtree.
sourceValue = tree;
}
// Step 3: Map the source value to the result value.
let resultValue;
if (needsSourceValue && sourceValue === undefined) {
// No source value means no result value.
resultValue = undefined;
} else if (deep && Tree.isAsyncTree(sourceValue)) {
// Map a subtree.
resultValue = mapFn(sourceValue);
} else if (valueFn) {
// Map a single value.
resultValue = await valueFn(sourceValue, sourceKey, tree);
} else {
// Return source value as is.
resultValue = sourceValue;
}
return resultValue;
};
}
if (keyFn) {
transformed.keys = async () => {
// Apply the keyFn to source keys for leaf values (not subtrees).
const sourceKeys = Array.from(await tree.keys());
const mapped = await Promise.all(
sourceKeys.map(async (sourceKey) =>
// Deep maps leave source keys for subtrees alone
deep && trailingSlash.has(sourceKey)
? sourceKey
: await keyFn(sourceKey, tree)
)
);
// Filter out any cases where the keyFn returned undefined.
const resultKeys = mapped.filter((key) => key !== undefined);
return resultKeys;
};
}
return transformed;
}
const tree = Tree.from(treelike, { deep });
return mapFn(tree);
}

View File

@@ -0,0 +1,65 @@
import { Tree } from "../internal.js";
import * as symbols from "../symbols.js";
import * as trailingSlash from "../trailingSlash.js";
/**
* Return a tree that performs a shallow merge of the given trees.
*
* Given a set of trees, the `get` method looks at each tree in turn. The first
* tree is asked for the value with the key. If an tree returns a defined value
* (i.e., not undefined), that value is returned. If the first tree returns
* undefined, the second tree will be asked, and so on. If none of the trees
* return a defined value, the `get` method returns undefined.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @param {import("../../index.ts").Treelike[]} sources
* @returns {AsyncTree & { description: string, trees: AsyncTree[]}}
*/
export default function merge(...sources) {
const trees = sources.map((treelike) => Tree.from(treelike));
return {
description: "merge",
async get(key) {
// Check trees for the indicated key in reverse order.
for (let index = trees.length - 1; index >= 0; index--) {
const tree = trees[index];
const value = await tree.get(key);
if (value !== undefined) {
// Merged tree acts as parent instead of the source tree.
if (Tree.isAsyncTree(value) && value.parent === tree) {
value.parent = this;
} else if (
typeof value === "object" &&
value?.[symbols.parent] === tree
) {
value[symbols.parent] = this;
}
return value;
}
}
return undefined;
},
async keys() {
const keys = new Set();
// Collect keys in the order the trees were provided.
for (const tree of trees) {
for (const key of await tree.keys()) {
// Remove the alternate form of the key (if it exists)
const alternateKey = trailingSlash.toggle(key);
if (alternateKey !== key) {
keys.delete(alternateKey);
}
keys.add(key);
}
}
return keys;
},
get trees() {
return trees;
},
};
}

View File

@@ -0,0 +1,88 @@
import { Tree } from "../internal.js";
import * as trailingSlash from "../trailingSlash.js";
/**
* A tree whose keys are strings interpreted as regular expressions.
*
* Requests to `get` a key are matched against the regular expressions, and the
* value for the first matching key is returned. The regular expresions are
* taken to match the entire key -- if they do not already start and end with
* `^` and `$` respectively, those are added.
*
* @type {import("../../index.ts").TreeTransform}
*/
export default async function regExpKeys(treelike) {
if (!treelike) {
const error = new TypeError(
`regExpKeys: The tree of regular expressions isn't defined.`
);
/** @type {any} */ (error).position = 0;
throw error;
}
const tree = Tree.from(treelike);
const map = new Map();
// We build the output tree first so that we can refer to it when setting
// `parent` on subtrees below.
let result = {
// @ts-ignore
description: "regExpKeys",
async get(key) {
if (key == null) {
// Reject nullish key.
throw new ReferenceError(
`${this.constructor.name}: Cannot get a null or undefined key.`
);
}
for (const [regExp, value] of map) {
if (regExp.test(key)) {
return value;
}
}
return undefined;
},
async keys() {
return map.keys();
},
};
// Turn the input tree's string keys into regular expressions, then map those
// to the corresponding values.
for (const key of await tree.keys()) {
if (typeof key !== "string") {
// Skip non-string keys.
continue;
}
// Get value.
let value = await tree.get(key);
let regExp;
if (trailingSlash.has(key) || Tree.isAsyncTree(value)) {
const baseKey = trailingSlash.remove(key);
regExp = new RegExp("^" + baseKey + "/?$");
// Subtree
value = regExpKeys(value);
if (!value.parent) {
value.parent = result;
}
} else {
// Construct regular expression.
let text = key;
if (!text.startsWith("^")) {
text = "^" + text;
}
if (!text.endsWith("$")) {
text = text + "$";
}
regExp = new RegExp(text);
}
map.set(regExp, value);
}
return result;
}

View File

@@ -0,0 +1,31 @@
import { Tree } from "../internal.js";
/**
* Reverse the order of the top-level keys in the tree.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @typedef {import("../../index.ts").Treelike} Treelike
*
* @param {Treelike} treelike
* @returns {AsyncTree}
*/
export default function reverse(treelike) {
if (!treelike) {
const error = new TypeError(`reverse: The tree to reverse isn't defined.`);
/** @type {any} */ (error).position = 0;
throw error;
}
const tree = Tree.from(treelike);
return {
async get(key) {
return tree.get(key);
},
async keys() {
const keys = Array.from(await tree.keys());
keys.reverse();
return keys;
},
};
}

View File

@@ -0,0 +1,71 @@
import { Tree } from "../internal.js";
/**
* A tree's "scope" is the collection of everything in that tree and all of its
* ancestors.
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @typedef {import("../../index.ts").Treelike} Treelike
*
* @param {Treelike} treelike
* @returns {AsyncTree & {trees: AsyncTree[]}}
*/
export default function scope(treelike) {
if (!treelike) {
const error = new TypeError(`scope: The tree isn't defined.`);
/** @type {any} */ (error).position = 0;
throw error;
}
const tree = Tree.from(treelike);
return {
// Starting with this tree, search up the parent hierarchy.
async get(key) {
/** @type {AsyncTree|null|undefined} */
let current = tree;
let value;
while (current) {
value = await current.get(key);
if (value !== undefined) {
break;
}
current = current.parent;
}
return value;
},
// Collect all keys for this tree and all parents
async keys() {
const keys = new Set();
/** @type {AsyncTree|null|undefined} */
let current = tree;
while (current) {
for (const key of await current.keys()) {
keys.add(key);
}
current = current.parent;
}
return keys;
},
// Collect all keys for this tree and all parents.
//
// This method exists for debugging purposes, as it's helpful to be able to
// quickly flatten and view the entire scope chain.
get trees() {
const result = [];
/** @type {AsyncTree|null|undefined} */
let current = tree;
while (current) {
result.push(current);
current = current.parent;
}
return result;
},
};
}

View File

@@ -0,0 +1,61 @@
import { Tree } from "../internal.js";
/**
* Return a new tree with the original's keys sorted. A comparison function can
* be provided; by default the keys will be sorted in [natural sort
* order](https://en.wikipedia.org/wiki/Natural_sort_order).
*
* @typedef {import("@weborigami/types").AsyncTree} AsyncTree
* @typedef {(key: any, tree: AsyncTree) => any} SortKeyFn
* @typedef {{ compare?: (a: any, b: any) => number, sortKey?: SortKeyFn }}
* SortOptions
*
* @param {import("../../index.ts").Treelike} treelike
* @param {SortOptions} [options]
*/
export default function sort(treelike, options) {
if (!treelike) {
const error = new TypeError(`sort: The tree to sort isn't defined.`);
/** @type {any} */ (error).position = 0;
throw error;
}
const sortKey = options?.sortKey;
let compare = options?.compare;
const tree = Tree.from(treelike);
const transformed = Object.create(tree);
transformed.keys = async () => {
const keys = Array.from(await tree.keys());
if (sortKey) {
// Invoke the async sortKey function to get sort keys.
// Create { key, sortKey } tuples.
const tuples = await Promise.all(
keys.map(async (key) => {
const sort = await sortKey(key, tree);
if (sort === undefined) {
throw new Error(
`sortKey function returned undefined for key ${key}`
);
}
return { key, sort };
})
);
// Wrap the comparison function so it applies to sort keys.
const defaultCompare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
const originalCompare = compare ?? defaultCompare;
// Sort by the sort key.
tuples.sort((a, b) => originalCompare(a.sort, b.sort));
// Map back to the original keys.
const sorted = tuples.map((pair) => pair.key);
return sorted;
} else {
// Use original keys as sort keys.
// If compare is undefined, this uses default sort order.
return keys.slice().sort(compare);
}
};
return transformed;
}

View File

@@ -0,0 +1,28 @@
import { Tree } from "../internal.js";
/**
* Returns a new tree with the number of keys limited to the indicated count.
*
* @param {import("../../index.ts").Treelike} treelike
* @param {number} count
*/
export default function take(treelike, count) {
if (!treelike) {
const error = new TypeError(`take: The tree to take from isn't defined.`);
/** @type {any} */ (error).position = 0;
throw error;
}
const tree = Tree.from(treelike);
return {
async keys() {
const keys = Array.from(await tree.keys());
return keys.slice(0, count);
},
async get(key) {
return tree.get(key);
},
};
}

2
node_modules/@weborigami/async-tree/src/symbols.js generated vendored Normal file
View File

@@ -0,0 +1,2 @@
export const deep = Symbol("deep");
export const parent = Symbol("parent");

View File

@@ -0,0 +1,54 @@
/**
* Add a trailing slash to a string key if the value is truthy. If the key
* is not a string, it will be returned as is.
*
* @param {any} key
*/
export function add(key) {
if (key == null) {
throw new ReferenceError("trailingSlash: key was undefined");
}
return typeof key === "string" && !key.endsWith("/") ? `${key}/` : key;
}
/**
* Return true if the indicated key is a string with a trailing slash,
* false otherwise.
*
* @param {any} key
*/
export function has(key) {
if (key == null) {
throw new ReferenceError("trailingSlash: key was undefined");
}
return typeof key === "string" && key.endsWith("/");
}
/**
* Remove a trailing slash from a string key.
*
* @param {any} key
*/
export function remove(key) {
if (key == null) {
throw new ReferenceError("trailingSlash: key was undefined");
}
return typeof key === "string" ? key.replace(/\/$/, "") : key;
}
/**
* If the key has a trailing slash, remove it; otherwise add it.
*
* @param {any} key
* @param {boolean} [force]
*/
export function toggle(key, force = undefined) {
if (key == null) {
throw new ReferenceError("trailingSlash: key was undefined");
}
if (typeof key !== "string") {
return key;
}
const addSlash = force ?? !has(key);
return addSlash ? add(key) : remove(key);
}

18
node_modules/@weborigami/async-tree/src/utilities.d.ts generated vendored Normal file
View File

@@ -0,0 +1,18 @@
import { AsyncTree } from "@weborigami/types";
import { Packed, PlainObject, StringLike } from "../index.ts";
export function box(value: any): any;
export function castArrayLike(keys: any[], values: any[]): any;
export function getRealmObjectPrototype(object: any): any;
export const hiddenFileNames: string[];
export function isPacked(obj: any): obj is Packed;
export function isPlainObject(obj: any): obj is PlainObject;
export function isStringLike(obj: any): obj is StringLike;
export function isUnpackable(obj): obj is { unpack: () => any };
export function keysFromPath(path: string): string[];
export const naturalOrder: (a: string, b: string) => number;
export function pathFromKeys(keys: string[]): string;
export function pipeline(start: any, ...functions: Function[]): Promise<any>;
export function setParent(child: any, parent: AsyncTree): void;
export function toPlainValue(object: any): Promise<any>;
export function toString(object: any): string;

374
node_modules/@weborigami/async-tree/src/utilities.js generated vendored Normal file
View File

@@ -0,0 +1,374 @@
import { Tree } from "./internal.js";
import * as symbols from "./symbols.js";
import * as trailingSlash from "./trailingSlash.js";
const textDecoder = new TextDecoder();
const TypedArray = Object.getPrototypeOf(Uint8Array);
/**
* Return the value as an object. If the value is already an object it will be
* returned as is. If the value is a primitive, it will be wrapped in an object:
* a string will be wrapped in a String object, a number will be wrapped in a
* Number object, and a boolean will be wrapped in a Boolean object.
*
* @param {any} value
*/
export function box(value) {
switch (typeof value) {
case "string":
return new String(value);
case "number":
return new Number(value);
case "boolean":
return new Boolean(value);
default:
return value;
}
}
/**
* Create an array or plain object from the given keys and values.
*
* If the given plain object has only sequential integer keys, return the
* values as an array. Otherwise, create a plain object with the keys and
* values.
*
* @param {any[]} keys
* @param {any[]} values
*/
export function castArrayLike(keys, values) {
let isArrayLike = false;
// Need at least one key to count as an array
if (keys.length > 0) {
// Assume it's an array
isArrayLike = true;
// Then check if all the keys are sequential integers
let expectedIndex = 0;
for (const key of keys) {
const index = Number(key);
if (key === "" || isNaN(index) || index !== expectedIndex) {
// Not array-like
isArrayLike = false;
break;
}
expectedIndex++;
}
}
return isArrayLike
? values
: Object.fromEntries(keys.map((key, i) => [key, values[i]]));
}
/**
* Return the Object prototype at the root of the object's prototype chain.
*
* This is used by functions like isPlainObject() to handle cases where the
* `Object` at the root prototype chain is in a different realm.
*
* @param {any} object
*/
export function getRealmObjectPrototype(object) {
if (Object.getPrototypeOf(object) === null) {
// The object has no prototype.
return null;
}
let proto = object;
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto);
}
return proto;
}
// Names of OS-generated files that should not be enumerated
export const hiddenFileNames = [".DS_Store"];
/**
* Return true if the object is in a packed form (or can be readily packed into
* a form) that can be given to fs.writeFile or response.write().
*
* @param {any} obj
* @returns {obj is import("../index.ts").Packed}
*/
export function isPacked(obj) {
return (
typeof obj === "string" ||
obj instanceof ArrayBuffer ||
obj instanceof ReadableStream ||
obj instanceof String ||
obj instanceof TypedArray
);
}
/**
* Return true if the object is a plain JavaScript object created by `{}`,
* `new Object()`, or `Object.create(null)`.
*
* This function also considers object-like things with no prototype (like a
* `Module`) as plain objects.
*
* @param {any} obj
* @returns {obj is import("../index.ts").PlainObject}
*/
export function isPlainObject(obj) {
// From https://stackoverflow.com/q/51722354/76472
if (typeof obj !== "object" || obj === null) {
return false;
}
// We treat object-like things with no prototype (like a Module) as plain
// objects.
if (Object.getPrototypeOf(obj) === null) {
return true;
}
// Do we inherit directly from Object in this realm?
return Object.getPrototypeOf(obj) === getRealmObjectPrototype(obj);
}
/**
* Return true if the value is a primitive JavaScript value.
*
* @param {any} value
*/
export function isPrimitive(value) {
// Check for null first, since typeof null === "object".
if (value === null) {
return true;
}
const type = typeof value;
return type !== "object" && type !== "function";
}
/**
* Return true if the object is a string or object with a non-trival `toString`
* method.
*
* @param {any} obj
* @returns {obj is import("../index.ts").StringLike}
*/
export function isStringLike(obj) {
if (typeof obj === "string") {
return true;
} else if (obj?.toString === undefined) {
return false;
} else if (obj.toString === getRealmObjectPrototype(obj)?.toString) {
// The stupid Object.prototype.toString implementation always returns
// "[object Object]", so if that's the only toString method the object has,
// we return false.
return false;
} else {
return true;
}
}
export function isUnpackable(obj) {
return (
isPacked(obj) && typeof (/** @type {any} */ (obj).unpack) === "function"
);
}
/**
* Given a path like "/foo/bar/baz", return an array of keys like ["foo/",
* "bar/", "baz"].
*
* Leading slashes are ignored. Consecutive slashes will be ignored. Trailing
* slashes are preserved.
*
* @param {string} pathname
*/
export function keysFromPath(pathname) {
// Split the path at each slash
let keys = pathname.split("/");
if (keys[0] === "") {
// The path begins with a slash; drop that part.
keys.shift();
}
if (keys.at(-1) === "") {
// The path ends with a slash; drop that part.
keys.pop();
}
// Drop any empty keys
keys = keys.filter((key) => key !== "");
// Add the trailing slash back to all keys but the last
for (let i = 0; i < keys.length - 1; i++) {
keys[i] += "/";
}
// Add trailing slash to last key if path ended with a slash
if (keys.length > 0 && trailingSlash.has(pathname)) {
keys[keys.length - 1] += "/";
}
return keys;
}
/**
* Compare two strings using [natural sort
* order](https://en.wikipedia.org/wiki/Natural_sort_order).
*/
export const naturalOrder = new Intl.Collator(undefined, {
numeric: true,
}).compare;
/**
* Return a slash-separated path for the given keys.
*
* This takes care to avoid adding consecutive slashes if they keys themselves
* already have trailing slashes.
*
* @param {string[]} keys
*/
export function pathFromKeys(keys) {
const normalized = keys.map((key) => trailingSlash.remove(key));
return normalized.join("/");
}
/**
* Apply a series of functions to a value, passing the result of each function
* to the next one.
*
* @param {any} start
* @param {...Function} fns
*/
export async function pipeline(start, ...fns) {
return fns.reduce(async (acc, fn) => fn(await acc), start);
}
/**
* If the child object doesn't have a parent yet, set it to the indicated
* parent. If the child is an AsyncTree, set the `parent` property. Otherwise,
* set the `symbols.parent` property.
*
* @param {*} child
* @param {*} parent
*/
export function setParent(child, parent) {
if (Tree.isAsyncTree(child)) {
// Value is a subtree; set its parent to this tree.
if (!child.parent) {
child.parent = parent;
}
} else if (Object.isExtensible(child) && !child[symbols.parent]) {
// Add parent reference as a symbol to avoid polluting the object. This
// reference will be used if the object is later used as a tree. We set
// `enumerable` to false even thought this makes no practical difference
// (symbols are never enumerated) because it can provide a hint in the
// debugger that the property is for internal use.
Object.defineProperty(child, symbols.parent, {
configurable: true,
enumerable: false,
value: parent,
writable: true,
});
}
}
/**
* Convert the given input to the plainest possible JavaScript value. This
* helper is intended for functions that want to accept an argument from the ori
* CLI, which could a string, a stream of data, or some other kind of JavaScript
* object.
*
* If the input is a function, it will be invoked and its result will be
* processed.
*
* If the input is a promise, it will be resolved and its result will be
* processed.
*
* If the input is treelike, it will be converted to a plain JavaScript object,
* recursively traversing the tree and converting all values to plain types.
*
* If the input is stringlike, its text will be returned.
*
* If the input is a ArrayBuffer or typed array, it will be interpreted as UTF-8
* text if it does not contain unprintable characters. If it does, it will be
* returned as a base64-encoded string.
*
* If the input has a custom class instance, its public properties will be
* returned as a plain object.
*
* @param {any} input
* @returns {Promise<any>}
*/
export async function toPlainValue(input) {
if (input instanceof Function) {
// Invoke function
input = input();
}
if (input instanceof Promise) {
// Resolve promise
input = await input;
}
if (isPrimitive(input) || input instanceof Date) {
return input;
} else if (Tree.isTreelike(input)) {
const mapped = await Tree.map(input, (value) => toPlainValue(value));
return Tree.plain(mapped);
} else if (isStringLike(input)) {
return toString(input);
} else if (input instanceof ArrayBuffer || input instanceof TypedArray) {
// Try to interpret the buffer as UTF-8 text, otherwise use base64.
const text = toString(input);
if (text !== null) {
return text;
} else {
return toBase64(input);
}
} else {
// Some other kind of class instance; return its public properties.
const plain = {};
for (const [key, value] of Object.entries(input)) {
plain[key] = await toPlainValue(value);
}
return plain;
}
}
function toBase64(object) {
if (typeof Buffer !== "undefined") {
// Node.js environment
return Buffer.from(object).toString("base64");
} else {
// Browser environment
let binary = "";
const bytes = new Uint8Array(object);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
}
/**
* Return a string form of the object, handling cases not generally handled by
* the standard JavaScript `toString()` method:
*
* 1. If the object is an ArrayBuffer or TypedArray, decode the array as UTF-8.
* 2. If the object is otherwise a plain JavaScript object with the useless
* default toString() method, return null instead of "[object Object]". In
* practice, it's generally more useful to have this method fail than to
* return a useless string.
* 3. If the object is a defined primitive value, return the result of
* String(object).
*
* Otherwise return null.
*
* @param {any} object
* @returns {string|null}
*/
export function toString(object) {
if (object instanceof ArrayBuffer || object instanceof TypedArray) {
// Treat the buffer as UTF-8 text.
const decoded = textDecoder.decode(object);
// If the result appears to contain non-printable characters, it's probably not a string.
// https://stackoverflow.com/a/1677660/76472
const hasNonPrintableCharacters = /[\x00-\x08\x0E-\x1F]/.test(decoded);
return hasNonPrintableCharacters ? null : decoded;
} else if (isStringLike(object) || (object !== null && isPrimitive(object))) {
return String(object);
} else {
return null;
}
}

386
node_modules/@weborigami/async-tree/test/Tree.test.js generated vendored Normal file
View File

@@ -0,0 +1,386 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import MapTree from "../src/drivers/MapTree.js";
import { DeepObjectTree, ObjectTree, Tree } from "../src/internal.js";
import * as symbols from "../src/symbols.js";
describe("Tree", () => {
test("assign applies one tree to another", async () => {
const target = new DeepObjectTree({
a: 1,
b: 2,
more: {
d: 3,
},
});
const source = new DeepObjectTree({
a: 4, // Overwrite existing value
b: undefined, // Delete
c: 5, // Add
more: {
// Should leave existing `more` keys alone.
e: 6, // Add
},
// Add new subtree
extra: {
f: 7,
},
});
// Apply changes.
const result = await Tree.assign(target, source);
assert.equal(result, target);
const plain = await Tree.plain(target);
assert.deepEqual(plain, {
a: 4,
c: 5,
more: {
d: 3,
e: 6,
},
extra: {
f: 7,
},
});
});
test("assign() can apply updates to an array", async () => {
const target = new ObjectTree(["a", "b", "c"]);
await Tree.assign(target, ["d", "e"]);
assert.deepEqual(await Tree.plain(target), ["d", "e", "c"]);
});
test("clear() removes all values", async () => {
const fixture = createFixture();
await Tree.clear(fixture);
assert.deepEqual(Array.from(await Tree.entries(fixture)), []);
});
test("entries() returns the [key, value] pairs", async () => {
const fixture = createFixture();
assert.deepEqual(Array.from(await Tree.entries(fixture)), [
["Alice.md", "Hello, **Alice**."],
["Bob.md", "Hello, **Bob**."],
["Carol.md", "Hello, **Carol**."],
]);
});
test("forEach() invokes a callback for each entry", async () => {
const fixture = createFixture();
const results = {};
await Tree.forEach(fixture, async (value, key) => {
results[key] = value;
});
assert.deepEqual(results, {
"Alice.md": "Hello, **Alice**.",
"Bob.md": "Hello, **Bob**.",
"Carol.md": "Hello, **Carol**.",
});
});
test("from() returns an async tree as is", async () => {
const tree1 = new ObjectTree({
a: "Hello, a.",
});
const tree2 = Tree.from(tree1);
assert.equal(tree2, tree1);
});
test("from() uses an object's unpack() method if defined", async () => {
const obj = new String();
/** @type {any} */ (obj).unpack = () => ({
a: "Hello, a.",
});
const tree = Tree.from(obj);
assert.deepEqual(await Tree.plain(tree), {
a: "Hello, a.",
});
});
test("from returns a deep object tree if deep option is true", async () => {
const obj = {
sub: {
a: 1,
},
};
const tree = Tree.from(obj, { deep: true });
assert(tree instanceof DeepObjectTree);
});
test("from returns a deep object tree if object has [deep] symbol set", async () => {
const obj = {
sub: {
a: 1,
},
};
Object.defineProperty(obj, symbols.deep, { value: true });
const tree = Tree.from(obj);
assert(tree instanceof DeepObjectTree);
});
test("from() creates a deferred tree if unpack() returns a promise", async () => {
const obj = new String();
/** @type {any} */ (obj).unpack = async () => ({
a: "Hello, a.",
});
const tree = Tree.from(obj);
assert.deepEqual(await Tree.plain(tree), {
a: "Hello, a.",
});
});
test("from() autoboxes primitive values", async () => {
const tree = Tree.from("Hello, world.");
const slice = await tree.get("slice");
const result = await slice(0, 5);
assert.equal(result, "Hello");
});
test("has returns true if the key exists", async () => {
const fixture = createFixture();
assert.equal(await Tree.has(fixture, "Alice.md"), true);
assert.equal(await Tree.has(fixture, "David.md"), false);
});
test("isAsyncTree returns true if the object is a tree", () => {
const missingGetAndKeys = {};
assert(!Tree.isAsyncTree(missingGetAndKeys));
const missingIterator = {
async get() {},
};
assert(!Tree.isAsyncTree(missingIterator));
const missingGet = {
async keys() {},
};
assert(!Tree.isAsyncTree(missingGet));
const hasGetAndKeys = {
async get() {},
async keys() {},
};
assert(Tree.isAsyncTree(hasGetAndKeys));
});
test("isAsyncMutableTree returns true if the object is a mutable tree", () => {
assert.equal(
Tree.isAsyncMutableTree({
get() {},
keys() {},
}),
false
);
assert.equal(Tree.isAsyncMutableTree(createFixture()), true);
});
test("isTreelike() returns true if the argument can be cast to an async tree", () => {
assert(!Tree.isTreelike(null));
assert(Tree.isTreelike({}));
assert(Tree.isTreelike([]));
assert(Tree.isTreelike(new Map()));
assert(Tree.isTreelike(new Set()));
});
test("map() maps values", async () => {
const tree = new DeepObjectTree({
a: "Alice",
more: {
b: "Bob",
},
});
const mapped = Tree.map(tree, {
deep: true,
value: (value) => value.toUpperCase(),
});
assert.deepEqual(await Tree.plain(mapped), {
a: "ALICE",
more: {
b: "BOB",
},
});
});
test("mapReduce() can map values and reduce them", async () => {
const tree = new DeepObjectTree({
a: 1,
b: 2,
more: {
c: 3,
},
d: 4,
});
const reduced = await Tree.mapReduce(
tree,
(value) => value,
async (values) => String.prototype.concat(...values)
);
assert.deepEqual(reduced, "1234");
});
test("paths returns an array of paths to the values in the tree", async () => {
const tree = new DeepObjectTree({
a: 1,
b: 2,
c: {
d: 3,
e: 4,
},
});
assert.deepEqual(await Tree.paths(tree), ["a", "b", "c/d", "c/e"]);
});
test("plain() produces a plain object version of a tree", async () => {
const tree = new ObjectTree({
a: 1,
// Slashes should be normalized
"sub1/": {
b: 2,
},
sub2: {
c: 3,
},
});
const plain = await Tree.plain(tree);
assert.deepEqual(plain, {
a: 1,
sub1: {
b: 2,
},
sub2: {
c: 3,
},
});
});
test("plain() produces an array for an array-like tree", async () => {
const original = ["a", "b", "c"];
const tree = new ObjectTree(original);
const plain = await Tree.plain(tree);
assert.deepEqual(plain, original);
});
test("plain() leaves an array-like tree as an object if keys aren't consecutive", async () => {
const original = {
0: "a",
1: "b",
// missing
3: "c",
};
const tree = new ObjectTree(original);
const plain = await Tree.plain(tree);
assert.deepEqual(plain, original);
});
test("plain() returns empty array or object for ObjectTree as necessary", async () => {
const tree = new ObjectTree({});
assert.deepEqual(await Tree.plain(tree), {});
const arrayTree = new ObjectTree([]);
assert.deepEqual(await Tree.plain(arrayTree), []);
});
test("plain() awaits async properties", async () => {
const object = {
get name() {
return Promise.resolve("Alice");
},
};
assert.deepEqual(await Tree.plain(object), { name: "Alice" });
});
test("plain() coerces TypedArray values to strings", async () => {
const tree = new ObjectTree({
a: new TextEncoder().encode("Hello, world."),
});
const plain = await Tree.plain(tree);
assert.equal(plain.a, "Hello, world.");
});
test("remove method removes a value", async () => {
const fixture = createFixture();
await Tree.remove(fixture, "Alice.md");
assert.deepEqual(Array.from(await Tree.entries(fixture)), [
["Bob.md", "Hello, **Bob**."],
["Carol.md", "Hello, **Carol**."],
]);
});
test("toFunction returns a function that invokes a tree's get() method", async () => {
const tree = new ObjectTree({
a: 1,
b: 2,
});
const fn = Tree.toFunction(tree);
assert.equal(await fn("a"), 1);
assert.equal(await fn("b"), 2);
});
test("traverse() a path of keys", async () => {
const tree = new ObjectTree({
a1: 1,
a2: {
b1: 2,
b2: {
c1: 3,
c2: 4,
},
},
});
assert.equal(await Tree.traverse(tree), tree);
assert.equal(await Tree.traverse(tree, "a1"), 1);
assert.equal(await Tree.traverse(tree, "a2", "b2", "c2"), 4);
assert.equal(
await Tree.traverse(tree, "a2", "doesntexist", "c2"),
undefined
);
});
test("traverse() a function with fixed number of arguments", async () => {
const tree = (a, b) => ({
c: "Result",
});
assert.equal(await Tree.traverse(tree, "a", "b", "c"), "Result");
});
test("traverse() from one tree into another", async () => {
const tree = new ObjectTree({
a: {
b: new MapTree([
["c", "Hello"],
["d", "Goodbye"],
]),
},
});
assert.equal(await Tree.traverse(tree, "a", "b", "c"), "Hello");
});
test("traversePath() traverses a slash-separated path", async () => {
const tree = new ObjectTree({
a: {
b: {
c: "Hello",
},
},
});
assert.equal(await Tree.traversePath(tree, "a/b/c"), "Hello");
});
test("values() returns the store's values", async () => {
const fixture = createFixture();
assert.deepEqual(Array.from(await Tree.values(fixture)), [
"Hello, **Alice**.",
"Hello, **Bob**.",
"Hello, **Carol**.",
]);
});
});
function createFixture() {
return new ObjectTree({
"Alice.md": "Hello, **Alice**.",
"Bob.md": "Hello, **Bob**.",
"Carol.md": "Hello, **Carol**.",
});
}

View File

@@ -0,0 +1,54 @@
/**
* A simple test runner for the browser to run the subset of the Node.s test
* runner used by the project.
*/
export default function assert(condition) {
if (!condition) {
throw new Error("Assertion failed");
}
}
assert.equal = (actual, expected) => {
if (Number.isNaN(actual) && Number.isNaN(expected)) {
return;
} else if (actual == expected) {
return;
} else {
throw new Error(`Expected ${expected} but got ${actual}`);
}
};
// This is a simplified deepEqual test that examines the conditions we care
// about. For reference, the actual Node assert.deepEqual is much more complex:
// see https://github.com/nodejs/node/blob/main/lib/internal/util/comparisons.js
assert.deepEqual = (actual, expected) => {
if (actual === expected) {
return;
} else if (
typeof actual === "object" &&
actual != null &&
typeof expected === "object" &&
expected != null &&
Object.keys(actual).length === Object.keys(expected).length
) {
for (const prop in actual) {
if (!expected.hasOwnProperty(prop)) {
break;
}
assert.deepEqual(actual[prop], expected[prop]);
}
return;
}
throw new Error(`Expected ${expected} but got ${actual}`);
};
assert.rejects = async (promise) => {
try {
await promise;
throw new Error("Expected promise to reject but it resolved");
} catch (error) {
return;
}
};

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<script type="importmap">
{
"imports": {
"node:assert": "./assert.js",
"node:test": "./testRunner.js"
}
}
</script>
<!-- Omit FileTree.test.js, which is Node.js only -->
<!-- Omit SiteTree.test.js, which requires mocks -->
<script type="module" src="../BrowserFileTree.test.js"></script>
<script type="module" src="../DeferredTree.test.js"></script>
<script type="module" src="../FunctionTree.test.js"></script>
<script type="module" src="../MapTree.test.js"></script>
<script type="module" src="../ObjectTree.test.js"></script>
<script type="module" src="../SetTree.test.js"></script>
<script type="module" src="../Tree.test.js"></script>
<script type="module" src="../operations/cache.test.js"></script>
<script type="module" src="../operations/merge.test.js"></script>
<script type="module" src="../operations/deepMerge.test.js"></script>
<script type="module" src="../transforms/cachedKeyMaps.test.js"></script>
<script
type="module"
src="../transforms/keyFunctionsForExtensions.test.js"
></script>
<script type="module" src="../transforms/mapFn.test.js"></script>
<script type="module" src="../utilities.test.js"></script>
</head>
<body></body>
</html>

View File

@@ -0,0 +1,51 @@
/**
* A simple test runner for the browser to run the subset of the Node.s test
* runner used by the project.
*/
let promises = {};
let currentSuite;
const markers = {
success: "✅",
skipped: "ー",
fail: "❌",
};
export async function describe(name, fn) {
promises[name] = [];
currentSuite = name;
await fn();
const results = await Promise.all(promises[name]);
const someFailed = results.some((result) => result.result === "fail");
const header = `${someFailed ? markers.fail : markers.success} ${name}`;
console[someFailed ? "group" : "groupCollapsed"](header);
for (const result of results) {
const marker = markers[result.result];
const name = result.name;
const message = result.result === "fail" ? `: ${result.message}` : "";
const skipped = result.result === "skipped" ? " [skipped]" : "";
console.log(`${marker} ${name}${message}${skipped}`);
}
console.groupEnd();
}
// Node test() calls can call an async function, but the test() function isn't
// declared async. We implicitly wrap the test call with a Promise and add it to
// the list of promises for the current suite.
export async function test(name, fn) {
promises[currentSuite].push(runTest(name, fn));
}
test.skip = (name, fn) => {
promises[currentSuite].push(Promise.resolve({ result: "skipped", name }));
};
async function runTest(name, fn) {
try {
await fn();
return { result: "success", name };
} catch (/** @type {any} */ error) {
return { result: "fail", name, message: error.message };
}
}

View File

@@ -0,0 +1,153 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import BrowserFileTree from "../../src/drivers/BrowserFileTree.js";
import { Tree } from "../../src/internal.js";
// Skip these tests if we're not in a browser.
const isBrowser = typeof window !== "undefined";
if (isBrowser) {
describe("BrowserFileTree", async () => {
test("can get the keys of the tree", async () => {
const fixture = await createFixture();
assert.deepEqual(Array.from(await fixture.keys()), [
"Alice.md",
"Bob.md",
"Carol.md",
"subfolder/",
]);
});
test("can get the value for a key", async () => {
const fixture = await createFixture();
const buffer = await fixture.get("Alice.md");
assert.equal(text(buffer), "Hello, **Alice**.");
});
test("getting an unsupported key returns undefined", async () => {
const fixture = await createFixture();
assert.equal(await fixture.get("xyz"), undefined);
});
test("getting empty key returns undefined", async () => {
const fixture = await createFixture();
assert.equal(await fixture.get(""), undefined);
});
test("getting a null/undefined key throws an exception", async () => {
const fixture = await createFixture();
await assert.rejects(async () => {
await fixture.get(null);
});
await assert.rejects(async () => {
await fixture.get(undefined);
});
});
test("sets parent on subtrees", async () => {
const fixture = await createFixture();
const subfolder = await fixture.get("subfolder");
assert.equal(subfolder.parent, fixture);
});
test("can retrieve values with optional trailing slash", async () => {
const fixture = await createFixture();
assert(await fixture.get("Alice.md"));
assert(await fixture.get("Alice.md/"));
assert(await fixture.get("subfolder"));
assert(await fixture.get("subfolder/"));
});
test("can set a value", async () => {
const fixture = await createFixture();
// Update existing key.
await fixture.set("Alice.md", "Goodbye, **Alice**.");
// New key.
await fixture.set("David.md", "Hello, **David**.");
// Delete key.
await fixture.set("Bob.md", undefined);
// Delete non-existent key.
await fixture.set("xyz", undefined);
assert.deepEqual(await strings(fixture), {
"Alice.md": "Goodbye, **Alice**.",
"Carol.md": "Hello, **Carol**.",
"David.md": "Hello, **David**.",
subfolder: {},
});
});
test("can create a subfolder via set", async () => {
const fixture = await createFixture();
const tree = {
async get(key) {
const name = key.replace(/\.md$/, "");
return `Hello, **${name}**.`;
},
async keys() {
return ["Ellen.md"];
},
};
await fixture.set("more", tree);
assert.deepEqual(await strings(fixture), {
"Alice.md": "Hello, **Alice**.",
"Bob.md": "Hello, **Bob**.",
"Carol.md": "Hello, **Carol**.",
more: {
"Ellen.md": "Hello, **Ellen**.",
},
subfolder: {},
});
});
});
}
async function createFile(directory, name, contents) {
const file = await directory.getFileHandle(name, { create: true });
const writable = await file.createWritable();
await writable.write(contents);
await writable.close();
}
let count = 0;
async function createFixture() {
const root = await navigator.storage.getDirectory();
const directory = await root.getDirectoryHandle("async-tree", {
create: true,
});
// Create a new subdirectory for each test.
const subdirectoryName = `test${count++}`;
// Delete any pre-existing subdirectory with that name.
try {
await directory.removeEntry(subdirectoryName, { recursive: true });
} catch (e) {
// Ignore errors.
}
const subdirectory = await directory.getDirectoryHandle(subdirectoryName, {
create: true,
});
await createFile(subdirectory, "Alice.md", "Hello, **Alice**.");
await createFile(subdirectory, "Bob.md", "Hello, **Bob**.");
await createFile(subdirectory, "Carol.md", "Hello, **Carol**.");
await subdirectory.getDirectoryHandle("subfolder", {
create: true,
});
return new BrowserFileTree(subdirectory);
}
async function strings(tree) {
return Tree.plain(Tree.map(tree, (value) => text(value)));
}
function text(arrayBuffer) {
return new TextDecoder().decode(arrayBuffer);
}

View File

@@ -0,0 +1,17 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import DeepMapTree from "../../src/drivers/DeepMapTree.js";
import { Tree } from "../../src/internal.js";
describe("DeepMapTree", () => {
test("returns a DeepMapTree for value that's a Map", async () => {
const tree = new DeepMapTree([
["a", 1],
["map", new Map([["b", 2]])],
]);
const map = await tree.get("map");
assert.equal(map instanceof DeepMapTree, true);
assert.deepEqual(await Tree.plain(map), { b: 2 });
assert.equal(map.parent, tree);
});
});

View File

@@ -0,0 +1,35 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { DeepObjectTree, Tree } from "../../src/internal.js";
describe("DeepObjectTree", () => {
test("returns an ObjectTree for value that's a plain sub-object or sub-array", async () => {
const tree = createFixture();
const object = await tree.get("object");
assert.equal(object instanceof DeepObjectTree, true);
assert.deepEqual(await Tree.plain(object), { b: 2 });
assert.equal(object.parent, tree);
const array = await tree.get("array");
assert.equal(array instanceof DeepObjectTree, true);
assert.deepEqual(await Tree.plain(array), [3]);
assert.equal(array.parent, tree);
});
test("adds trailing slashes to keys for subtrees including plain objects or arrays", async () => {
const tree = createFixture();
const keys = Array.from(await tree.keys());
assert.deepEqual(keys, ["a", "object/", "array/"]);
});
});
function createFixture() {
return new DeepObjectTree({
a: 1,
object: {
b: 2,
},
array: [3],
});
}

View File

@@ -0,0 +1,22 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import DeferredTree from "../../src/drivers/DeferredTree.js";
import { ObjectTree, Tree } from "../../src/internal.js";
describe("DeferredTree", () => {
test("lazy-loads a treelike object", async () => {
const tree = new DeferredTree(async () => ({ a: 1, b: 2, c: 3 }));
assert.deepEqual(await Tree.plain(tree), { a: 1, b: 2, c: 3 });
});
test("sets parent on subtrees", async () => {
const object = {
a: 1,
};
const parent = new ObjectTree({});
const fixture = new DeferredTree(() => object);
fixture.parent = parent;
const tree = await fixture.tree();
assert.equal(tree.parent, parent);
});
});

View File

@@ -0,0 +1,116 @@
import assert from "node:assert";
import { beforeEach, describe, mock, test } from "node:test";
import ExplorableSiteTree from "../../src/drivers/ExplorableSiteTree.js";
import { Tree } from "../../src/internal.js";
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
const mockHost = "https://mock";
const mockResponses = {
"/.keys.json": {
data: JSON.stringify(["about/", "index.html"]),
},
"/about": {
redirected: true,
status: 301,
url: "https://mock/about/",
},
"/about/.keys.json": {
data: JSON.stringify(["Alice.html", "Bob.html", "Carol.html"]),
},
"/about/Alice.html": {
data: "Hello, Alice!",
},
"/about/Bob.html": {
data: "Hello, Bob!",
},
"/about/Carol.html": {
data: "Hello, Carol!",
},
"/index.html": {
data: "Home page",
},
};
describe("ExplorableSiteTree", () => {
beforeEach(() => {
mock.method(global, "fetch", mockFetch);
});
test("can get the keys of a tree", async () => {
const fixture = new ExplorableSiteTree(mockHost);
const keys = await fixture.keys();
assert.deepEqual(Array.from(keys), ["about/", "index.html"]);
});
test("can get a plain value for a key", async () => {
const fixture = new ExplorableSiteTree(mockHost);
const arrayBuffer = await fixture.get("index.html");
const text = textDecoder.decode(arrayBuffer);
assert.equal(text, "Home page");
});
test("getting an unsupported key returns undefined", async () => {
const fixture = new ExplorableSiteTree(mockHost);
assert.equal(await fixture.get("xyz"), undefined);
});
test("getting a null/undefined key throws an exception", async () => {
const fixture = new ExplorableSiteTree(mockHost);
await assert.rejects(async () => {
await fixture.get(null);
});
await assert.rejects(async () => {
await fixture.get(undefined);
});
});
test("can return a new tree for a key that redirects", async () => {
const fixture = new ExplorableSiteTree(mockHost);
const about = await fixture.get("about");
assert(about instanceof ExplorableSiteTree);
assert.equal(about.href, "https://mock/about/");
});
test("can convert a site to a plain object", async () => {
const fixture = new ExplorableSiteTree(mockHost);
// Convert buffers to strings.
const strings = Tree.map(fixture, {
deep: true,
value: (value) => textDecoder.decode(value),
});
assert.deepEqual(await Tree.plain(strings), {
about: {
"Alice.html": "Hello, Alice!",
"Bob.html": "Hello, Bob!",
"Carol.html": "Hello, Carol!",
},
"index.html": "Home page",
});
});
});
async function mockFetch(href) {
if (!href.startsWith(mockHost)) {
return { status: 404 };
}
const path = href.slice(mockHost.length);
const mockedResponse = mockResponses[path];
if (mockedResponse) {
return Object.assign(
{
arrayBuffer: () => textEncoder.encode(mockedResponse.data).buffer,
ok: true,
status: 200,
text: () => mockedResponse.data,
},
mockedResponse
);
}
return {
ok: false,
status: 404,
};
}

View File

@@ -0,0 +1,192 @@
import assert from "node:assert";
import * as fs from "node:fs/promises";
import path from "node:path";
import { describe, test } from "node:test";
import { fileURLToPath } from "node:url";
import FileTree from "../../src/drivers/FileTree.js";
import { ObjectTree, Tree } from "../../src/internal.js";
const dirname = path.dirname(fileURLToPath(import.meta.url));
const tempDirectory = path.join(dirname, "fixtures/temp");
const textDecoder = new TextDecoder();
describe("FileTree", async () => {
test("can get the keys of the tree", async () => {
const fixture = createFixture("fixtures/markdown");
assert.deepEqual(Array.from(await fixture.keys()), [
"Alice.md",
"Bob.md",
"Carol.md",
"subfolder/",
]);
});
test("can get the value for a key", async () => {
const fixture = createFixture("fixtures/markdown");
const buffer = await fixture.get("Alice.md");
const text = textDecoder.decode(buffer);
assert.equal(text, "Hello, **Alice**.");
});
test("getting an unsupported key returns undefined", async () => {
const fixture = createFixture("fixtures/markdown");
assert.equal(await fixture.get("xyz"), undefined);
});
test("getting empty key returns undefined", async () => {
const fixture = createFixture("fixtures/markdown");
assert.equal(await fixture.get(""), undefined);
});
test("getting a null/undefined key throws an exception", async () => {
const fixture = createFixture("fixtures/markdown");
await assert.rejects(async () => {
await fixture.get(null);
});
await assert.rejects(async () => {
await fixture.get(undefined);
});
});
test("can retrieve values with optional trailing slash", async () => {
const fixture = createFixture("fixtures/markdown");
assert(await fixture.get("Alice.md"));
assert(await fixture.get("Alice.md/"));
assert(await fixture.get("subfolder"));
assert(await fixture.get("subfolder/"));
});
test("sets parent on subtrees", async () => {
const fixture = createFixture("fixtures");
const markdown = await fixture.get("markdown");
assert.equal(markdown.parent, fixture);
});
test("can write out a file via set()", async () => {
await createTempDirectory();
// Write out a file.
const fileName = "file1";
const fileText = "This is the first file.";
const tempFiles = new FileTree(tempDirectory);
await tempFiles.set(fileName, fileText);
// Read it back in.
const filePath = path.join(tempDirectory, fileName);
const actualText = String(await fs.readFile(filePath));
assert.equal(fileText, actualText);
await removeTempDirectory();
});
test("create subfolder via set() with empty object value", async () => {
await createTempDirectory();
// Write out new, empty folder called "empty".
const tempFiles = new FileTree(tempDirectory);
await tempFiles.set("empty", {});
// Verify folder exists and has no contents.
const folderPath = path.join(tempDirectory, "empty");
const stats = await fs.stat(folderPath);
assert(stats.isDirectory());
const files = await fs.readdir(folderPath);
assert.deepEqual(files, []);
await removeTempDirectory();
});
test("create subfolder via set() with empty tree value", async () => {
await createTempDirectory();
// Write out new, empty folder called "empty".
const tempFiles = new FileTree(tempDirectory);
await tempFiles.set("empty", new ObjectTree({}));
// Verify folder exists and has no contents.
const folderPath = path.join(tempDirectory, "empty");
const stats = await fs.stat(folderPath);
assert(stats.isDirectory());
const files = await fs.readdir(folderPath);
assert.deepEqual(files, []);
await removeTempDirectory();
});
test("can write out subfolder via set()", async () => {
await createTempDirectory();
// Create a tiny set of "files".
const obj = {
file1: "This is the first file.",
subfolder: {
file2: "This is the second file.",
},
};
// Write out files as a new folder called "folder".
const tempFiles = new FileTree(tempDirectory);
await tempFiles.set("folder", obj);
// Read them back in.
const actualFiles = await tempFiles.get("folder");
const strings = Tree.map(actualFiles, {
deep: true,
value: (buffer) => textDecoder.decode(buffer),
});
const plain = await Tree.plain(strings);
assert.deepEqual(plain, obj);
await removeTempDirectory();
});
test("can delete a file via set()", async () => {
await createTempDirectory();
const tempFile = path.join(tempDirectory, "file");
await fs.writeFile(tempFile, "");
const tempFiles = new FileTree(tempDirectory);
await tempFiles.set("file", undefined);
let stats;
try {
stats = await fs.stat(tempFile);
} catch (/** @type {any} */ error) {
if (error.code !== "ENOENT") {
throw error;
}
}
assert(stats === undefined);
await removeTempDirectory();
});
test("can delete a folder via set()", async () => {
await createTempDirectory();
const folder = path.join(tempDirectory, "folder");
await fs.mkdir(folder);
const tempFiles = new FileTree(tempDirectory);
await tempFiles.set("folder", undefined);
let stats;
try {
stats = await fs.stat(folder);
} catch (/** @type {any} */ error) {
if (error.code !== "ENOENT") {
throw error;
}
}
assert(stats === undefined);
await removeTempDirectory();
});
});
function createFixture(fixturePath) {
return new FileTree(path.join(dirname, fixturePath));
}
async function createTempDirectory() {
await fs.mkdir(tempDirectory, { recursive: true });
}
async function removeTempDirectory() {
await fs.rm(tempDirectory, { recursive: true });
}

View File

@@ -0,0 +1,46 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import FunctionTree from "../../src/drivers/FunctionTree.js";
describe("FunctionTree", async () => {
test("can get the keys of the tree", async () => {
const fixture = createFixture();
assert.deepEqual(Array.from(await fixture.keys()), [
"Alice.md",
"Bob.md",
"Carol.md",
]);
});
test("can get the value for a key", async () => {
const fixture = createFixture();
const alice = await fixture.get("Alice.md");
assert.equal(alice, "Hello, **Alice**.");
});
test("getting a value from function with multiple arguments curries the function", async () => {
const fixture = new FunctionTree((a, b, c) => a + b + c);
const fnA = await fixture.get(1);
const fnAB = await fnA.get(2);
const result = await fnAB.get(3);
assert.equal(result, 6);
});
test("getting an unsupported key returns undefined", async () => {
const fixture = createFixture();
assert.equal(await fixture.get("xyz"), undefined);
});
});
function createFixture() {
return new FunctionTree(
(key) => {
if (key?.endsWith?.(".md")) {
const name = key.slice(0, -3);
return `Hello, **${name}**.`;
}
return undefined;
},
["Alice.md", "Bob.md", "Carol.md"]
);
}

View File

@@ -0,0 +1,59 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import MapTree from "../../src/drivers/MapTree.js";
import * as symbols from "../../src/symbols.js";
describe("MapTree", () => {
test("can get the keys of the tree", async () => {
const fixture = createFixture();
assert.deepEqual(Array.from(await fixture.keys()), ["a", "b", "c"]);
});
test("can get the value for a key", async () => {
const fixture = createFixture();
const a = await fixture.get("a");
assert.equal(a, 1);
});
test("sets parent on subtrees", async () => {
const map = new Map([["more", new Map([["a", 1]])]]);
const fixture = new MapTree(map);
const more = await fixture.get("more");
assert.equal(more[symbols.parent], fixture);
});
test("adds trailing slashes to keys for subtrees", async () => {
const tree = new MapTree([
["a", 1],
["subtree", new MapTree([["b", 2]])],
]);
const keys = Array.from(await tree.keys());
assert.deepEqual(keys, ["a", "subtree/"]);
});
test("can retrieve values with optional trailing slash", async () => {
const subtree = new MapTree([["b", 2]]);
const tree = new MapTree([
["a", 1],
["subtree", subtree],
]);
assert.equal(await tree.get("a"), 1);
assert.equal(await tree.get("a/"), 1);
assert.equal(await tree.get("subtree"), subtree);
assert.equal(await tree.get("subtree/"), subtree);
});
test("getting an unsupported key returns undefined", async () => {
const fixture = createFixture();
assert.equal(await fixture.get("d"), undefined);
});
});
function createFixture() {
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
]);
return new MapTree(map);
}

View File

@@ -0,0 +1,156 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { ObjectTree, Tree } from "../../src/internal.js";
import * as symbols from "../../src/symbols.js";
describe("ObjectTree", () => {
test("can get the keys of the tree", async () => {
const fixture = createFixture();
assert.deepEqual(Array.from(await fixture.keys()), [
"Alice.md",
"Bob.md",
"Carol.md",
]);
});
test("can get the value for a key", async () => {
const fixture = createFixture();
const alice = await fixture.get("Alice.md");
assert.equal(alice, "Hello, **Alice**.");
});
test("getting an unsupported key returns undefined", async () => {
const fixture = createFixture();
assert.equal(await fixture.get("xyz"), undefined);
});
test("getting a null/undefined key throws an exception", async () => {
const fixture = createFixture();
await assert.rejects(async () => {
await fixture.get(null);
});
await assert.rejects(async () => {
await fixture.get(undefined);
});
});
test("can set a value", async () => {
const tree = new ObjectTree({
a: 1,
b: 2,
c: 3,
});
// Update existing key
await tree.set("a", 4);
// Delete key
await tree.set("b", undefined);
// Overwrite key with trailing slash
await tree.set("c/", {});
// New key
await tree.set("d", 5);
assert.deepEqual(await Tree.entries(tree), [
["a", 4],
["c/", {}],
["d", 5],
]);
});
test("can wrap a class instance", async () => {
class Foo {
constructor() {
this.a = 1;
}
get prop() {
return this._prop;
}
set prop(prop) {
this._prop = prop;
}
}
class Bar extends Foo {
method() {}
}
const bar = new Bar();
/** @type {any} */ (bar).extra = "Hello";
const fixture = new ObjectTree(bar);
assert.deepEqual(await Tree.entries(fixture), [
["a", 1],
["extra", "Hello"],
["prop", undefined],
]);
assert.equal(await fixture.get("a"), 1);
await fixture.set("prop", "Goodbye");
assert.equal(bar.prop, "Goodbye");
assert.equal(await fixture.get("prop"), "Goodbye");
});
test("sets parent symbol on subobjects", async () => {
const fixture = new ObjectTree({
sub: {},
});
const sub = await fixture.get("sub");
assert.equal(sub[symbols.parent], fixture);
});
test("sets parent on subtrees", async () => {
const fixture = new ObjectTree({
a: 1,
more: new ObjectTree({
b: 2,
}),
});
const more = await fixture.get("more");
assert.equal(more.parent, fixture);
});
test("adds trailing slashes to keys for subtrees", async () => {
const tree = new ObjectTree({
a1: 1,
a2: new ObjectTree({
b1: 2,
}),
a3: 3,
a4: new ObjectTree({
b2: 4,
}),
});
const keys = Array.from(await tree.keys());
assert.deepEqual(keys, ["a1", "a2/", "a3", "a4/"]);
});
test("can retrieve values with optional trailing slash", async () => {
const subtree = {
async get(key) {},
async keys() {},
};
const tree = new ObjectTree({
a: 1,
subtree,
});
assert.equal(await tree.get("a"), 1);
assert.equal(await tree.get("a/"), 1);
assert.equal(await tree.get("subtree"), subtree);
assert.equal(await tree.get("subtree/"), subtree);
});
test("method on an object is bound to the object", async () => {
const n = new Number(123);
const tree = new ObjectTree(n);
const method = await tree.get("toString");
assert.equal(method(), "123");
});
});
function createFixture() {
return new ObjectTree({
"Alice.md": "Hello, **Alice**.",
"Bob.md": "Hello, **Bob**.",
"Carol.md": "Hello, **Carol**.",
});
}

View File

@@ -0,0 +1,44 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import SetTree from "../../src/drivers/SetTree.js";
import { ObjectTree } from "../../src/internal.js";
describe("SetTree", () => {
test("can get the keys of the tree", async () => {
const set = new Set(["a", "b", "c"]);
const fixture = new SetTree(set);
assert.deepEqual(Array.from(await fixture.keys()), [0, 1, 2]);
});
test("can get the value for a key", async () => {
const set = new Set(["a", "b", "c"]);
const fixture = new SetTree(set);
const a = await fixture.get(0);
assert.equal(a, "a");
});
test("getting an unsupported key returns undefined", async () => {
const set = new Set(["a", "b", "c"]);
const fixture = new SetTree(set);
assert.equal(await fixture.get(3), undefined);
});
test("getting a null/undefined key throws an exception", async () => {
const set = new Set(["a", "b", "c"]);
const fixture = new SetTree(set);
await assert.rejects(async () => {
await fixture.get(null);
});
await assert.rejects(async () => {
await fixture.get(undefined);
});
});
test("sets parent on subtrees", async () => {
const set = new Set();
set.add(new ObjectTree({}));
const fixture = new SetTree(set);
const subtree = await fixture.get(0);
assert.equal(subtree.parent, fixture);
});
});

View File

@@ -0,0 +1,92 @@
import assert from "node:assert";
import { beforeEach, describe, mock, test } from "node:test";
import SiteTree from "../../src/drivers/SiteTree.js";
const textDecoder = new TextDecoder();
const textEncoder = new TextEncoder();
const mockHost = "https://mock";
const mockResponses = {
"/about": {
redirected: true,
status: 301,
url: "https://mock/about/",
},
"/about/Alice.html": {
data: "Hello, Alice!",
},
"/about/Bob.html": {
data: "Hello, Bob!",
},
"/about/Carol.html": {
data: "Hello, Carol!",
},
"/index.html": {
data: "Home page",
},
};
describe("SiteTree", () => {
beforeEach(() => {
mock.method(global, "fetch", mockFetch);
});
test("returns an empty array as the keys of a tree", async () => {
const fixture = new SiteTree(mockHost);
const keys = await fixture.keys();
assert.deepEqual(Array.from(keys), []);
});
test("can get a plain value for a key", async () => {
const fixture = new SiteTree(mockHost);
const arrayBuffer = await fixture.get("index.html");
const text = textDecoder.decode(arrayBuffer);
assert.equal(text, "Home page");
});
test("immediately return a new tree for a key with a trailing slash", async () => {
const fixture = new SiteTree(mockHost);
const about = await fixture.get("about/");
assert(about instanceof SiteTree);
assert.equal(about.href, "https://mock/about/");
});
test("getting an unsupported key returns undefined", async () => {
const fixture = new SiteTree(mockHost);
assert.equal(await fixture.get("xyz"), undefined);
});
test("getting a null/undefined key throws an exception", async () => {
const fixture = new SiteTree(mockHost);
await assert.rejects(async () => {
await fixture.get(null);
});
await assert.rejects(async () => {
await fixture.get(undefined);
});
});
});
async function mockFetch(href) {
if (!href.startsWith(mockHost)) {
return { status: 404 };
}
const path = href.slice(mockHost.length);
const mockedResponse = mockResponses[path];
if (mockedResponse) {
return Object.assign(
{
arrayBuffer: () => textEncoder.encode(mockedResponse.data).buffer,
ok: true,
status: 200,
text: () => mockedResponse.data,
},
mockedResponse
);
}
return {
ok: false,
status: 404,
};
}

View File

@@ -0,0 +1,121 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import calendar from "../../src/drivers/calendarTree.js";
import { toPlainValue } from "../../src/utilities.js";
describe("calendarTree", () => {
test("without a start or end, returns a tree for today", async () => {
const tree = calendar({
value: (year, month, day) => `${year}-${month}-${day}`,
});
const plain = await toPlainValue(tree);
const today = new Date();
const year = today.getFullYear();
const month = (today.getMonth() + 1).toString().padStart(2, "0");
const day = today.getDate().toString().padStart(2, "0");
assert.deepEqual(plain, {
[year]: {
[month]: {
[day]: `${year}-${month}-${day}`,
},
},
});
});
test("returns a tree for a month range", async () => {
const tree = calendar({
start: "2025-01",
end: "2025-02",
value: (year, month, day) => `${year}-${month}-${day}`,
});
const plain = await toPlainValue(tree);
assert.deepEqual(plain, {
2025: {
"01": {
"01": "2025-01-01",
"02": "2025-01-02",
"03": "2025-01-03",
"04": "2025-01-04",
"05": "2025-01-05",
"06": "2025-01-06",
"07": "2025-01-07",
"08": "2025-01-08",
"09": "2025-01-09",
10: "2025-01-10",
11: "2025-01-11",
12: "2025-01-12",
13: "2025-01-13",
14: "2025-01-14",
15: "2025-01-15",
16: "2025-01-16",
17: "2025-01-17",
18: "2025-01-18",
19: "2025-01-19",
20: "2025-01-20",
21: "2025-01-21",
22: "2025-01-22",
23: "2025-01-23",
24: "2025-01-24",
25: "2025-01-25",
26: "2025-01-26",
27: "2025-01-27",
28: "2025-01-28",
29: "2025-01-29",
30: "2025-01-30",
31: "2025-01-31",
},
"02": {
"01": "2025-02-01",
"02": "2025-02-02",
"03": "2025-02-03",
"04": "2025-02-04",
"05": "2025-02-05",
"06": "2025-02-06",
"07": "2025-02-07",
"08": "2025-02-08",
"09": "2025-02-09",
10: "2025-02-10",
11: "2025-02-11",
12: "2025-02-12",
13: "2025-02-13",
14: "2025-02-14",
15: "2025-02-15",
16: "2025-02-16",
17: "2025-02-17",
18: "2025-02-18",
19: "2025-02-19",
20: "2025-02-20",
21: "2025-02-21",
22: "2025-02-22",
23: "2025-02-23",
24: "2025-02-24",
25: "2025-02-25",
26: "2025-02-26",
27: "2025-02-27",
28: "2025-02-28",
},
},
});
});
test("returns a tree for a day range", async () => {
const tree = calendar({
start: "2025-02-27",
end: "2025-03-02",
value: (year, month, day) => `${year}-${month}-${day}`,
});
const plain = await toPlainValue(tree);
assert.deepEqual(plain, {
2025: {
"02": {
27: "2025-02-27",
28: "2025-02-28",
},
"03": {
"01": "2025-03-01",
"02": "2025-03-02",
},
},
});
});
});

View File

@@ -0,0 +1 @@
Hello, **Alice**.

View File

@@ -0,0 +1 @@
Hello, **Bob**.

View File

@@ -0,0 +1 @@
Hello, **Carol**.

View File

@@ -0,0 +1 @@
This file exists to force the creation of the parent folder.

View File

@@ -0,0 +1,41 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { extname, match, replace } from "../src/extension.js";
describe("extension", () => {
test("extname", () => {
assert.equal(extname(".\\"), "");
assert.equal(extname("..\\"), ".\\");
assert.equal(extname("file.ext\\"), ".ext\\");
assert.equal(extname("file.ext\\\\"), ".ext\\\\");
assert.equal(extname("file\\"), "");
assert.equal(extname("file\\\\"), "");
assert.equal(extname("file.\\"), ".\\");
assert.equal(extname("file.\\\\"), ".\\\\");
});
test("match", () => {
assert.equal(match("file.md", ".md"), "file");
assert.equal(match("file.md", ".txt"), null);
assert.equal(match("file.md/", ".md"), "file/");
assert.equal(match("file", ""), "file");
assert.equal(match("file", "/"), null);
assert.equal(match("file/", "/"), "file");
});
test("match can handle multi-part extensions", () => {
assert.equal(match("foo.ori.html", ".ori.html"), "foo");
assert.equal(match("foo.ori.html", ".html"), "foo.ori");
assert.equal(match("foo.ori.html", ".txt"), null);
assert.equal(match("foo.ori.html/", ".ori.html"), "foo/");
});
test("replace", () => {
assert.equal(replace("file.md", ".md", ".html"), "file.html");
assert.equal(replace("file.md", ".txt", ".html"), "file.md");
assert.equal(replace("file.md/", ".md", ".html"), "file.html/");
assert.equal(replace("folder/", "", ".html"), "folder.html");
assert.equal(replace("folder", "/", ".html"), "folder");
assert.equal(replace("folder/", "/", ".html"), "folder.html");
});
});

View File

@@ -0,0 +1,15 @@
import { DeepObjectTree } from "@weborigami/async-tree";
import assert from "node:assert";
import { describe, test } from "node:test";
import * as jsonKeys from "../src/jsonKeys.js";
describe("jsonKeys", () => {
test("stringifies JSON Keys", async () => {
const tree = new DeepObjectTree({
about: {},
"index.html": "Home",
});
const json = await jsonKeys.stringify(tree);
assert.strictEqual(json, '["about/","index.html"]');
});
});

View File

@@ -0,0 +1,63 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { DeepObjectTree, ObjectTree, Tree } from "../../src/internal.js";
import cache from "../../src/operations/cache.js";
describe("cache", () => {
test("caches reads of values from one tree into another", async () => {
const objectCache = new ObjectTree({});
const fixture = cache(
new DeepObjectTree({
a: 1,
b: 2,
c: 3,
more: {
d: 4,
},
}),
objectCache
);
const keys = [...(await fixture.keys())];
assert.deepEqual(keys, ["a", "b", "c", "more/"]);
assert.equal(await objectCache.get("a"), undefined);
assert.equal(await fixture.get("a"), 1);
assert.equal(await objectCache.get("a"), 1); // Now in cache
assert.equal(await objectCache.get("b"), undefined);
assert.equal(await fixture.get("b"), 2);
assert.equal(await objectCache.get("b"), 2);
assert.equal(await objectCache.get("more"), undefined);
const more = await fixture.get("more");
assert.deepEqual([...(await more.keys())], ["d"]);
assert.equal(await more.get("d"), 4);
const moreCache = await objectCache.get("more");
assert.equal(await moreCache.get("d"), 4);
});
test("if a cache filter is supplied, it only caches values whose keys match the filter", async () => {
const objectCache = new ObjectTree({});
const fixture = cache(
Tree.from({
"a.txt": "a",
"b.txt": "b",
}),
objectCache,
Tree.from({
"a.txt": true,
})
);
// Access some values to populate the cache.
assert.equal(await fixture.get("a.txt"), "a");
assert.equal(await fixture.get("b.txt"), "b");
// The a.txt value should be cached because it matches the filter.
assert.equal(await objectCache.get("a.txt"), "a");
// The b.txt value should not be cached because it does not match the filter.
assert.equal(await objectCache.get("b.txt"), undefined);
});
});

View File

@@ -0,0 +1,90 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { DeepObjectTree, ObjectTree } from "../../src/internal.js";
import cachedKeyFunctions from "../../src/operations/cachedKeyFunctions.js";
describe("cachedKeyFunctions", () => {
test("maps keys with caching", async () => {
const tree = new ObjectTree({
a: "letter a",
b: "letter b",
});
let callCount = 0;
const addUnderscore = async (sourceKey, tree) => {
callCount++;
return `_${sourceKey}`;
};
const { inverseKey, key } = cachedKeyFunctions(addUnderscore);
assert.equal(await inverseKey("_a", tree), "a"); // Cache miss
assert.equal(callCount, 1);
assert.equal(await inverseKey("_a", tree), "a");
assert.equal(callCount, 1);
assert.equal(await inverseKey("_b", tree), "b"); // Cache miss
assert.equal(callCount, 2);
assert.equal(await key("a", tree), "_a");
assert.equal(await key("a", tree), "_a");
assert.equal(await key("b", tree), "_b");
assert.equal(callCount, 2);
// `c` isn't in tree, so we should get undefined.
assert.equal(await inverseKey("_c", tree), undefined);
// But key mapping is still possible.
assert.equal(await key("c", tree), "_c");
// And now we have a cache hit.
assert.equal(await inverseKey("_c", tree), "c");
assert.equal(callCount, 3);
});
test("maps keys with caching and deep option", async () => {
const tree = new DeepObjectTree({
a: "letter a",
b: {
c: "letter c",
},
});
let callCount = 0;
const addUnderscore = async (sourceKey, tree) => {
callCount++;
return `_${sourceKey}`;
};
const { inverseKey, key } = cachedKeyFunctions(addUnderscore, true);
assert.equal(await inverseKey("_a", tree), "a"); // Cache miss
assert.equal(await inverseKey("_a", tree), "a");
assert.equal(callCount, 1);
// Subtree key left alone
assert.equal(await inverseKey("_b", tree), undefined);
assert.equal(await inverseKey("b", tree), "b");
assert.equal(await inverseKey("b/", tree), "b/");
assert.equal(callCount, 1);
assert.equal(await key("a", tree), "_a");
assert.equal(await key("a", tree), "_a");
assert.equal(callCount, 1);
assert.equal(await key("b/", tree), "b/");
assert.equal(await key("b", tree), "b");
assert.equal(callCount, 1);
});
test("preserves trailing slashes", async () => {
const tree = new ObjectTree({
a: "letter a",
});
const addUnderscore = async (sourceKey) => `_${sourceKey}`;
const { inverseKey, key } = cachedKeyFunctions(addUnderscore);
assert.equal(await key("a/", tree), "_a/");
assert.equal(await key("a", tree), "_a");
assert.equal(await inverseKey("_a/", tree), "a/");
assert.equal(await inverseKey("_a", tree), "a");
});
});

View File

@@ -0,0 +1,34 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import FunctionTree from "../../src/drivers/FunctionTree.js";
import { Tree } from "../../src/internal.js";
import concat from "../../src/operations/concat.js";
describe("concat", () => {
test("concatenates deep tree values", async () => {
const tree = Tree.from({
a: "A",
b: "B",
c: "C",
more: {
d: "D",
e: "E",
},
});
const result = await concat.call(null, tree);
assert.equal(result, "ABCDE");
});
test("concatenates deep tree-like values", async () => {
const letters = ["a", "b", "c"];
const specimens = new FunctionTree(
(letter) => ({
lowercase: letter,
uppercase: letter.toUpperCase(),
}),
letters
);
const result = await concat.call(null, specimens);
assert.equal(result, "aAbBcC");
});
});

View File

@@ -0,0 +1,42 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { DeepObjectTree, Tree } from "../../src/internal.js";
import mergeDeep from "../../src/operations/deepMerge.js";
describe("mergeDeep", () => {
test("can merge deep", async () => {
const fixture = mergeDeep(
new DeepObjectTree({
a: {
b: 0, // Will be obscured by `b` below
c: {
d: 2,
},
},
}),
new DeepObjectTree({
a: {
b: 1,
c: {
e: 3,
},
f: 4,
},
})
);
assert.deepEqual(await Tree.plain(fixture), {
a: {
b: 1,
c: {
d: 2,
e: 3,
},
f: 4,
},
});
// Parent of a subvalue is the merged tree
const a = await fixture.get("a");
assert.equal(a.parent, fixture);
});
});

View File

@@ -0,0 +1,24 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { Tree } from "../../src/internal.js";
import deepReverse from "../../src/operations/deepReverse.js";
describe("deepReverse", () => {
test("reverses keys at all levels of a tree", async () => {
const tree = {
a: 1,
b: {
c: 2,
d: 3,
},
};
const reversed = deepReverse.call(null, tree);
assert.deepEqual(await Tree.plain(reversed), {
b: {
d: 3,
c: 2,
},
a: 1,
});
});
});

View File

@@ -0,0 +1,22 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { Tree } from "../../src/internal.js";
import deepTake from "../../src/operations/deepTake.js";
describe("deepTake", () => {
test("traverses deeply and returns a limited number of items", async () => {
const tree = {
a: 1,
b: {
c: 2,
d: {
e: 3,
},
f: 4,
},
g: 5,
};
const result = await deepTake(tree, 4);
assert.deepEqual(await Tree.plain(result), [1, 2, 3, 4]);
});
});

View File

@@ -0,0 +1,28 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import deepValues from "../../src/operations/deepValues.js";
describe("deepValues", () => {
test("returns in-order array of a tree's values", async () => {
const tree = {
a: 1,
b: {
c: 2,
d: {
e: 3,
},
},
f: {
g: 4,
},
};
const values = await deepValues(tree);
assert.deepEqual(values, [1, 2, 3, 4]);
});
test("returns in-order array of values in nested arrays", async () => {
const tree = [1, [2, 3], 4];
const values = await deepValues(tree);
assert.deepEqual(values, [1, 2, 3, 4]);
});
});

View File

@@ -0,0 +1,23 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { ObjectTree } from "../../src/internal.js";
import deepValuesIterator from "../../src/operations/deepValuesIterator.js";
describe("deepValuesIterator", () => {
test("returns an iterator of a tree's deep values", async () => {
const tree = new ObjectTree({
a: 1,
b: 2,
more: {
c: 3,
d: 4,
},
});
const values = [];
// The tree will be shallow, but we'll ask to expand the values.
for await (const value of deepValuesIterator(tree, { expand: true })) {
values.push(value);
}
assert.deepEqual(values, [1, 2, 3, 4]);
});
});

View File

@@ -0,0 +1,54 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { Tree } from "../../src/internal.js";
import group from "../../src/operations/group.js";
describe("group transform", () => {
test("groups an array using a group key function", async () => {
const fonts = [
{ name: "Aboreto", tags: ["Sans Serif"] },
{ name: "Albert Sans", tags: ["Geometric", "Sans Serif"] },
{ name: "Alegreya", tags: ["Serif"] },
{ name: "Work Sans", tags: ["Grotesque", "Sans Serif"] },
];
const tree = Tree.from(fonts);
const grouped = await group(tree, (value, key, tree) => value.tags);
assert.deepEqual(await Tree.plain(grouped), {
Geometric: [{ name: "Albert Sans", tags: ["Geometric", "Sans Serif"] }],
Grotesque: [{ name: "Work Sans", tags: ["Grotesque", "Sans Serif"] }],
"Sans Serif": [
{ name: "Aboreto", tags: ["Sans Serif"] },
{ name: "Albert Sans", tags: ["Geometric", "Sans Serif"] },
{ name: "Work Sans", tags: ["Grotesque", "Sans Serif"] },
],
Serif: [{ name: "Alegreya", tags: ["Serif"] }],
});
});
test("groups an object using a group key function", async () => {
const fonts = {
Aboreto: { tags: ["Sans Serif"] },
"Albert Sans": { tags: ["Geometric", "Sans Serif"] },
Alegreya: { tags: ["Serif"] },
"Work Sans": { tags: ["Grotesque", "Sans Serif"] },
};
const tree = Tree.from(fonts);
const grouped = await group(tree, (value, key, tree) => value.tags);
assert.deepEqual(await Tree.plain(grouped), {
Geometric: {
"Albert Sans": { tags: ["Geometric", "Sans Serif"] },
},
Grotesque: {
"Work Sans": { tags: ["Grotesque", "Sans Serif"] },
},
"Sans Serif": {
Aboreto: { tags: ["Sans Serif"] },
"Albert Sans": { tags: ["Geometric", "Sans Serif"] },
"Work Sans": { tags: ["Grotesque", "Sans Serif"] },
},
Serif: {
Alegreya: { tags: ["Serif"] },
},
});
});
});

View File

@@ -0,0 +1,17 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { Tree } from "../../src/internal.js";
import invokeFunctions from "../../src/operations/invokeFunctions.js";
describe("invokeFunctions", () => {
test("invokes function values, leaves other values as is", async () => {
const fixture = invokeFunctions({
a: 1,
b: () => 2,
});
assert.deepEqual(await Tree.plain(fixture), {
a: 1,
b: 2,
});
});
});

View File

@@ -0,0 +1,82 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { ObjectTree, Tree } from "../../src/internal.js";
import keyFunctionsForExtensions from "../../src/operations/keyFunctionsForExtensions.js";
import map from "../../src/operations/map.js";
describe("keyMapsForExtensions", () => {
test("returns key functions that pass a matching key through", async () => {
const { inverseKey, key } = keyFunctionsForExtensions({
sourceExtension: ".txt",
});
assert.equal(await inverseKey("file.txt"), "file.txt");
assert.equal(await inverseKey("file.txt/"), "file.txt");
assert.equal(await key("file.txt"), "file.txt");
assert.equal(await key("file.txt/"), "file.txt/");
assert.equal(await inverseKey("file.foo"), undefined);
assert.equal(await key("file.foo"), undefined);
});
test("returns key functions that can map extensions", async () => {
const { inverseKey, key } = keyFunctionsForExtensions({
resultExtension: ".json",
sourceExtension: ".md",
});
assert.equal(await inverseKey("file.json"), "file.md");
assert.equal(await inverseKey("file.json/"), "file.md");
assert.equal(await key("file.md"), "file.json");
assert.equal(await key("file.md/"), "file.json/");
assert.equal(await inverseKey("file.foo"), undefined);
assert.equal(await key("file.foo"), undefined);
});
test("key functions can handle a slash as an explicit extension", async () => {
const { inverseKey, key } = keyFunctionsForExtensions({
resultExtension: ".html",
sourceExtension: "/",
});
assert.equal(await inverseKey("file.html"), "file/");
assert.equal(await inverseKey("file.html/"), "file/");
assert.equal(await key("file"), undefined);
assert.equal(await key("file/"), "file.html");
});
test("works with map to handle keys that end in a given resultExtension", async () => {
const files = new ObjectTree({
"file1.txt": "will be mapped",
file2: "won't be mapped",
"file3.foo": "won't be mapped",
});
const { inverseKey, key } = keyFunctionsForExtensions({
sourceExtension: ".txt",
});
const fixture = map(files, {
inverseKey,
key,
value: (sourceValue, sourceKey, tree) => sourceValue.toUpperCase(),
});
assert.deepEqual(await Tree.plain(fixture), {
"file1.txt": "WILL BE MAPPED",
});
});
test("works with map to change a key's resultExtension", async () => {
const files = new ObjectTree({
"file1.txt": "will be mapped",
file2: "won't be mapped",
"file3.foo": "won't be mapped",
});
const { inverseKey, key } = keyFunctionsForExtensions({
resultExtension: ".upper",
sourceExtension: ".txt",
});
const fixture = map(files, {
inverseKey,
key,
value: (sourceValue, sourceKey, tree) => sourceValue.toUpperCase(),
});
assert.deepEqual(await Tree.plain(fixture), {
"file1.upper": "WILL BE MAPPED",
});
});
});

View File

@@ -0,0 +1,204 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import FunctionTree from "../../src/drivers/FunctionTree.js";
import { DeepObjectTree, ObjectTree, Tree } from "../../src/internal.js";
import map from "../../src/operations/map.js";
import * as trailingSlash from "../../src/trailingSlash.js";
describe("map", () => {
test("returns identity graph if no key or value function is supplied", async () => {
const tree = {
a: "letter a",
b: "letter b",
};
const mapped = map(tree, {});
assert.deepEqual(await Tree.plain(mapped), {
a: "letter a",
b: "letter b",
});
});
test("maps values", async () => {
const tree = new ObjectTree({
a: "letter a",
b: "letter b",
c: undefined, // Won't be mapped
});
const mapped = map(tree, {
value: (sourceValue, sourceKey, innerTree) => {
assert(sourceKey === "a" || sourceKey === "b");
assert.equal(innerTree, tree);
return sourceValue.toUpperCase();
},
});
assert.deepEqual(await Tree.plain(mapped), {
a: "LETTER A",
b: "LETTER B",
c: undefined,
});
});
test("interprets a single function argument as the value function", async () => {
const tree = {
a: "letter a",
b: "letter b",
};
const uppercaseValues = map(tree, (sourceValue, sourceKey, tree) => {
assert(sourceKey === "a" || sourceKey === "b");
return sourceValue.toUpperCase();
});
assert.deepEqual(await Tree.plain(uppercaseValues), {
a: "LETTER A",
b: "LETTER B",
});
});
test("maps keys using key and inverseKey", async () => {
const tree = {
a: "letter a",
b: "letter b",
};
const underscoreKeys = map(tree, {
key: addUnderscore,
inverseKey: removeUnderscore,
});
assert.deepEqual(await Tree.plain(underscoreKeys), {
_a: "letter a",
_b: "letter b",
});
});
test("maps keys and values", async () => {
const tree = {
a: "letter a",
b: "letter b",
};
const underscoreKeysUppercaseValues = map(tree, {
key: addUnderscore,
inverseKey: removeUnderscore,
value: async (sourceValue, sourceKey, tree) => sourceValue.toUpperCase(),
});
assert.deepEqual(await Tree.plain(underscoreKeysUppercaseValues), {
_a: "LETTER A",
_b: "LETTER B",
});
});
test("a shallow map is applied to async subtrees too", async () => {
const tree = {
a: "letter a",
more: {
b: "letter b",
},
};
const underscoreKeys = map(tree, {
key: async (sourceKey, tree) => `_${sourceKey}`,
inverseKey: async (resultKey, tree) => resultKey.slice(1),
value: async (sourceValue, sourceKey, tree) => sourceKey,
});
assert.deepEqual(await Tree.plain(underscoreKeys), {
_a: "a",
_more: "more",
});
});
test("value can provide a default key and inverse key functions", async () => {
const uppercase = (s) => s.toUpperCase();
uppercase.key = addUnderscore;
uppercase.inverseKey = removeUnderscore;
const tree = {
a: "letter a",
b: "letter b",
};
const mapped = map(tree, uppercase);
assert.deepEqual(await Tree.plain(mapped), {
_a: "LETTER A",
_b: "LETTER B",
});
});
test("deep maps values", async () => {
const tree = new DeepObjectTree({
a: "letter a",
more: {
b: "letter b",
},
});
const uppercaseValues = map(tree, {
deep: true,
value: (sourceValue, sourceKey, tree) => sourceValue.toUpperCase(),
});
assert.deepEqual(await Tree.plain(uppercaseValues), {
a: "LETTER A",
more: {
b: "LETTER B",
},
});
});
test("deep maps leaf keys", async () => {
const tree = new DeepObjectTree({
a: "letter a",
more: {
b: "letter b",
},
});
const underscoreKeys = map(tree, {
deep: true,
key: addUnderscore,
inverseKey: removeUnderscore,
});
assert.deepEqual(await Tree.plain(underscoreKeys), {
_a: "letter a",
more: {
_b: "letter b",
},
});
});
test("deep maps leaf keys and values", async () => {
const tree = new DeepObjectTree({
a: "letter a",
more: {
b: "letter b",
},
});
const underscoreKeysUppercaseValues = map(tree, {
deep: true,
key: addUnderscore,
inverseKey: removeUnderscore,
value: async (sourceValue, sourceKey, tree) => sourceValue.toUpperCase(),
});
assert.deepEqual(await Tree.plain(underscoreKeysUppercaseValues), {
_a: "LETTER A",
more: {
_b: "LETTER B",
},
});
});
test("needsSourceValue can be set to false in cases where the value isn't necessary", async () => {
let flag = false;
const tree = new FunctionTree(() => {
flag = true;
}, ["a", "b", "c"]);
const mapped = map(tree, {
needsSourceValue: false,
value: () => "X",
});
assert.deepEqual(await Tree.plain(mapped), {
a: "X",
b: "X",
c: "X",
});
assert(!flag);
});
});
function addUnderscore(key) {
return `_${key}`;
}
function removeUnderscore(key) {
return trailingSlash.has(key) ? key : key.slice(1);
}

View File

@@ -0,0 +1,64 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { DeepObjectTree, ObjectTree, Tree } from "../../src/internal.js";
import merge from "../../src/operations/merge.js";
import * as symbols from "../../src/symbols.js";
describe("merge", () => {
test("performs a shallow merge", async () => {
const fixture = merge(
{
a: 1,
// Will be obscured by `b` that follows
b: {
c: 2,
},
},
{
b: {
d: 3,
},
e: {
f: 4,
},
}
);
assert.deepEqual(await Tree.plain(fixture), {
a: 1,
b: {
d: 3,
},
e: {
f: 4,
},
});
// Merge is shallow, and last tree wins, so `b/c` doesn't exist
const c = await Tree.traverse(fixture, "b", "c");
assert.equal(c, undefined);
// Parent of a subvalue is the merged tree
const b = await fixture.get("b");
assert.equal(b[symbols.parent], fixture);
});
test("subtree can overwrite a leaf node", async () => {
const fixture = merge(
new ObjectTree({
a: 1,
}),
new DeepObjectTree({
a: {
b: 2,
},
})
);
assert.deepEqual([...(await fixture.keys())], ["a/"]);
assert.deepEqual(await Tree.plain(fixture), {
a: {
b: 2,
},
});
});
});

View File

@@ -0,0 +1,25 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { DeepObjectTree, Tree } from "../../src/internal.js";
import regExpKeys from "../../src/operations/regExpKeys.js";
describe("regExpKeys", () => {
test("matches keys using regular expressions", async () => {
const fixture = await regExpKeys(
new DeepObjectTree({
a: true,
"b.*": true,
c: {
d: true,
"e*": true,
},
})
);
assert(await Tree.traverse(fixture, "a"));
assert(!(await Tree.traverse(fixture, "alice")));
assert(await Tree.traverse(fixture, "bob"));
assert(await Tree.traverse(fixture, "brenda"));
assert(await Tree.traverse(fixture, "c", "d"));
assert(await Tree.traverse(fixture, "c", "eee"));
});
});

View File

@@ -0,0 +1,23 @@
import assert from "node:assert";
import { describe, test } from "node:test";
import { Tree } from "../../src/internal.js";
import reverse from "../../src/operations/reverse.js";
describe("reverse", () => {
test("reverses a tree's top-level keys", async () => {
const tree = {
a: "A",
b: "B",
c: "C",
};
const reversed = reverse.call(null, tree);
// @ts-ignore
assert.deepEqual(Array.from(await reversed.keys()), ["c", "b", "a"]);
// @ts-ignore
assert.deepEqual(await Tree.plain(reversed), {
c: "C",
b: "B",
a: "A",
});
});
});

Some files were not shown because too many files have changed in this diff Show More