diff --git a/backend/assets.mapped.layout.json b/backend/assets.mapped.layout.json
new file mode 100644
index 0000000..db507ad
--- /dev/null
+++ b/backend/assets.mapped.layout.json
@@ -0,0 +1 @@
+[ {"filename":"console.aframe.component","position":"0 0 0","rotation":"0 0 0"}, {"filename":"hello_world.json","position":"0 0 0","rotation":"0 0 0"}, {"filename":"hello_world.txt","position":"1 1 -1.2","rotation":"0 -50 0"}, {"filename":"index.html","position":"0 0 0","rotation":"0 0 0"}, {"filename":"index.html.png","position":"0 0 0","rotation":"0 0 0"}, {"filename":"sloan_test.txt","position":"1.4 1.5 -1","rotation":"0 -80 0"}, {"filename":"SpaSca_JSONCanvas_export.canvas","position":"0 0 0","rotation":"0 0 0"} ]
diff --git a/backend/assets.names b/backend/assets.names
new file mode 100644
index 0000000..57d2131
--- /dev/null
+++ b/backend/assets.names
@@ -0,0 +1,20 @@
+kenney_supermarket_assets/bottle-return.glb
+kenney_supermarket_assets/cash-register.glb
+kenney_supermarket_assets/character-employee.glb
+kenney_supermarket_assets/column.glb
+kenney_supermarket_assets/display-bread.glb
+kenney_supermarket_assets/display-fruit.glb
+kenney_supermarket_assets/fence-door-rotate.glb
+kenney_supermarket_assets/fence.glb
+kenney_supermarket_assets/floor.glb
+kenney_supermarket_assets/freezer.glb
+kenney_supermarket_assets/freezers-standing.glb
+kenney_supermarket_assets/shelf-bags.glb
+kenney_supermarket_assets/shelf-boxes.glb
+kenney_supermarket_assets/shelf-end.glb
+kenney_supermarket_assets/shopping-basket.glb
+kenney_supermarket_assets/shopping-cart.glb
+kenney_supermarket_assets/wall-corner.glb
+kenney_supermarket_assets/wall-door-rotate.glb
+kenney_supermarket_assets/wall.glb
+kenney_supermarket_assets/wall-window.glb
diff --git a/backend/custom-mime-example.xml b/backend/custom-mime-example.xml
new file mode 100644
index 0000000..9c95aef
--- /dev/null
+++ b/backend/custom-mime-example.xml
@@ -0,0 +1,31 @@
+from /usr/share/mime/packages/
+ once modified should run update-mime-database
+ or probably sufficient thus better, in user mode
+ ~/.local/share/mime/ then namely update-mime-database ~/.local/share/mime/
+ https://unix.stackexchange.com/a/564888/486198
+
+could be done for
+ .images.json
+ .aframe.component
+ .extractedpdf.json
+ .exported_author.json
+ .layout.json
+ .layout.json
+ .packeddirectory.json
+ .jxrstyles.json
+ .aframe.entity
+ .canvas
+ .2dmap.png
+ .webannotation.json
+
+ .pmwiki
+ .glb
+
+
+
+
+ jxr Model
+
+
+
+
diff --git a/backend/extract_as_json.js b/backend/extract_as_json.js
new file mode 100644
index 0000000..229fb64
--- /dev/null
+++ b/backend/extract_as_json.js
@@ -0,0 +1,16 @@
+const PDFExtract = require('pdf.js-extract').PDFExtract;
+const pdfExtract = new PDFExtract();
+const options = {};
+
+const fs = require('fs');
+
+var args = process.argv.slice(2)
+//console.log(args)
+if (args.length < 1) return console.log('missing input pdf filename');
+
+const filename = args[0]
+const output = filename+'_text.json'
+pdfExtract.extract(filename, options, (err, data) => {
+ if (err) return console.log(err);
+ fs.writeFileSync(output, JSON.stringify(data) )
+});
diff --git a/backend/mapping b/backend/mapping
new file mode 100644
index 0000000..2a03816
--- /dev/null
+++ b/backend/mapping
@@ -0,0 +1,28 @@
+7 7 2 # 7 width, 7 high, 2 layers
+layer 0
+19 19 19 19 18 19 19
+19 09 09 09 09 09 20
+20 09 09 09 09 09 19
+19 09 09 09 09 09 19
+19 09 09 09 09 09 20
+20 09 09 09 09 09 19
+20 09 09 09 09 09 19
+19 19 20 19 19 20 19
+layer 0.1
+00 00 00 00 00 00 00
+00 10 10 10 10 10 00
+00 00 00 00 00 00 00
+00 00 00 00 00 00 00
+00 13 13 00 13 13 00
+00 00 00 00 00 00 00
+00 00 00 00 00 00 00
+00 10 10 10 10 10 00
+00 00 00 00 00 00 00
+rotations layer 0
+00 00 00 00 00 00 00
+00 00 00 00 00 00 00
+00 00 00 00 00 00 00
+00 00 00 00 00 00 00
+00 00 00 00 00 00 00
+00 00 00 00 00 00 00
+00 00 00 00 00 00 00
diff --git a/backend/package-lock.json b/backend/package-lock.json
new file mode 100644
index 0000000..142345f
--- /dev/null
+++ b/backend/package-lock.json
@@ -0,0 +1,3045 @@
+{
+ "name": "fot_sloan_companion",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "@elasticemail/elasticemail-client": "^4.0.23",
+ "express": "^4.21.1",
+ "ip": "^2.0.1",
+ "node-html-to-image": "^5.0.0"
+ }
+ },
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
+ "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/cli": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.25.9.tgz",
+ "integrity": "sha512-I+02IfrTiSanpxJBlZQYb18qCxB6c2Ih371cVpfgIrPQrjAYkf45XxomTJOG8JBWX5GY35/+TmhCMdJ4ZPkL8Q==",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "commander": "^6.2.0",
+ "convert-source-map": "^2.0.0",
+ "fs-readdir-recursive": "^1.1.0",
+ "glob": "^7.2.0",
+ "make-dir": "^2.1.0",
+ "slash": "^2.0.0"
+ },
+ "bin": {
+ "babel": "bin/babel.js",
+ "babel-external-helpers": "bin/babel-external-helpers.js"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "optionalDependencies": {
+ "@nicolo-ribaudo/chokidar-2": "2.1.8-no-fsevents.3",
+ "chokidar": "^3.6.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz",
+ "integrity": "sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.0.tgz",
+ "integrity": "sha512-qETICbZSLe7uXv9VE8T/RWOdIE5qqyTucOt4zLYMafj2MRO271VGgLd4RACJMeBO37UPWhXiKMBk7YlJ0fOzQA==",
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz",
+ "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==",
+ "peer": true,
+ "dependencies": {
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.26.0",
+ "@babel/generator": "^7.26.0",
+ "@babel/helper-compilation-targets": "^7.25.9",
+ "@babel/helper-module-transforms": "^7.26.0",
+ "@babel/helpers": "^7.26.0",
+ "@babel/parser": "^7.26.0",
+ "@babel/template": "^7.25.9",
+ "@babel/traverse": "^7.25.9",
+ "@babel/types": "^7.26.0",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "peer": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@babel/core/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "peer": true
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.0.tgz",
+ "integrity": "sha512-/AIkAmInnWwgEAJGQr9vY0c66Mj6kjkE2ZPB1PurTRaRAh3U+J45sAQMjQDJdh4WbR3l0x5xkimXBKyBXXAu2w==",
+ "peer": true,
+ "dependencies": {
+ "@babel/parser": "^7.26.0",
+ "@babel/types": "^7.26.0",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz",
+ "integrity": "sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ==",
+ "peer": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.25.9",
+ "@babel/helper-validator-option": "^7.25.9",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "peer": true,
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz",
+ "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==",
+ "peer": true,
+ "dependencies": {
+ "@babel/traverse": "^7.25.9",
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz",
+ "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==",
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9",
+ "@babel/traverse": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+ "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+ "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz",
+ "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==",
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz",
+ "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==",
+ "peer": true,
+ "dependencies": {
+ "@babel/template": "^7.25.9",
+ "@babel/types": "^7.26.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.26.1",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.1.tgz",
+ "integrity": "sha512-reoQYNiAJreZNsJzyrDNzFQ+IQ5JFiIzAHJg9bn94S3l+4++J7RsIhNMoB+lgP/9tpmiAQqspv+xfdxTSzREOw==",
+ "peer": true,
+ "dependencies": {
+ "@babel/types": "^7.26.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz",
+ "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.25.9",
+ "@babel/parser": "^7.25.9",
+ "@babel/types": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz",
+ "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==",
+ "peer": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.25.9",
+ "@babel/generator": "^7.25.9",
+ "@babel/parser": "^7.25.9",
+ "@babel/template": "^7.25.9",
+ "@babel/types": "^7.25.9",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "peer": true,
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@babel/traverse/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "peer": true
+ },
+ "node_modules/@babel/types": {
+ "version": "7.26.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
+ "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
+ "peer": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@elasticemail/elasticemail-client": {
+ "version": "4.0.23",
+ "resolved": "https://registry.npmjs.org/@elasticemail/elasticemail-client/-/elasticemail-client-4.0.23.tgz",
+ "integrity": "sha512-qKfJQFhipEZrttsq4e+bYcnCAS1VeHSWboXABnTdvMOjLIC98s0rPcfO4d4ZZdHEIAwQA/K+tD/sWU1UZuDCHQ==",
+ "dependencies": {
+ "@babel/cli": "^7.0.0",
+ "superagent": "^5.3.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
+ "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
+ "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
+ "peer": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.25",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
+ "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nicolo-ribaudo/chokidar-2": {
+ "version": "2.1.8-no-fsevents.3",
+ "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz",
+ "integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==",
+ "optional": true
+ },
+ "node_modules/@puppeteer/browsers": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.1.tgz",
+ "integrity": "sha512-uK7o3hHkK+naEobMSJ+2ySYyXtQkBxIH8Gn4MK9ciePjNV+Pf+PgY/W7iPzn2MTjl3stcYB5AlcTmPYw7AXDwA==",
+ "dependencies": {
+ "debug": "^4.3.6",
+ "extract-zip": "^2.0.1",
+ "progress": "^2.0.3",
+ "proxy-agent": "^6.4.0",
+ "semver": "^7.6.3",
+ "tar-fs": "^3.0.6",
+ "unbzip2-stream": "^1.4.3",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "browsers": "lib/cjs/main-cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/@tootallnate/quickjs-emscripten": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
+ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="
+ },
+ "node_modules/@types/node": {
+ "version": "22.7.6",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz",
+ "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==",
+ "optional": true,
+ "dependencies": {
+ "undici-types": "~6.19.2"
+ }
+ },
+ "node_modules/@types/yauzl": {
+ "version": "2.10.3",
+ "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+ "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+ "optional": true,
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz",
+ "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/agent-base/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/agent-base/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "optional": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ },
+ "node_modules/array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
+ },
+ "node_modules/ast-types": {
+ "version": "0.13.4",
+ "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz",
+ "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==",
+ "dependencies": {
+ "tslib": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
+ "node_modules/b4a": {
+ "version": "1.6.7",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
+ "integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg=="
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "node_modules/bare-events": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.0.tgz",
+ "integrity": "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==",
+ "optional": true
+ },
+ "node_modules/bare-fs": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.3.5.tgz",
+ "integrity": "sha512-SlE9eTxifPDJrT6YgemQ1WGFleevzwY+XAP1Xqgl56HtcrisC2CHCZ2tq6dBpcH2TnNxwUEUGhweo+lrQtYuiw==",
+ "optional": true,
+ "dependencies": {
+ "bare-events": "^2.0.0",
+ "bare-path": "^2.0.0",
+ "bare-stream": "^2.0.0"
+ }
+ },
+ "node_modules/bare-os": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.4.4.tgz",
+ "integrity": "sha512-z3UiI2yi1mK0sXeRdc4O1Kk8aOa/e+FNWZcTiPB/dfTWyLypuE99LibgRaQki914Jq//yAWylcAt+mknKdixRQ==",
+ "optional": true
+ },
+ "node_modules/bare-path": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.3.tgz",
+ "integrity": "sha512-lh/eITfU8hrj9Ru5quUp0Io1kJWIk1bTjzo7JH1P5dWmQ2EL4hFUlfI8FonAhSlgIfhn63p84CDY/x+PisgcXA==",
+ "optional": true,
+ "dependencies": {
+ "bare-os": "^2.1.0"
+ }
+ },
+ "node_modules/bare-stream": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.3.1.tgz",
+ "integrity": "sha512-Vm8kAeOcfzHPTH8sq0tHBnUqYrkXdroaBVVylqFT4cF5wnMfKEIxxy2jIGu2zKVNl9P8MAP9XBWwXJ9N2+jfEw==",
+ "optional": true,
+ "dependencies": {
+ "streamx": "^2.20.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/basic-ftp": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.5.tgz",
+ "integrity": "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg==",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "optional": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/body-parser": {
+ "version": "1.20.3",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "on-finished": "2.4.1",
+ "qs": "6.13.0",
+ "raw-body": "2.5.2",
+ "type-is": "~1.6.18",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "optional": true,
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.24.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
+ "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "peer": true,
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001669",
+ "electron-to-chromium": "^1.5.41",
+ "node-releases": "^2.0.18",
+ "update-browserslist-db": "^1.1.1"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/bytes": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001675",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001675.tgz",
+ "integrity": "sha512-/wV1bQwPrkLiQMjaJF5yUMVM/VdRPOCU8QZ+PmG6uW6DvYSrNY1bpwHI/3mOcUosLaJCzYDi5o91IQB51ft6cg==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "peer": true
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "optional": true,
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chromium-bidi": {
+ "version": "0.6.5",
+ "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.5.tgz",
+ "integrity": "sha512-RuLrmzYrxSb0s9SgpB+QN5jJucPduZQ/9SIe76MDxYJuecPW5mxMdacJ1f4EtgiV+R0p3sCkznTMvH0MPGFqjA==",
+ "dependencies": {
+ "mitt": "3.0.1",
+ "urlpattern-polyfill": "10.0.0",
+ "zod": "3.23.8"
+ },
+ "peerDependencies": {
+ "devtools-protocol": "*"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
+ "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/component-emitter": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
+ "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ },
+ "node_modules/content-disposition": {
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
+ "dependencies": {
+ "safe-buffer": "5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/content-type": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="
+ },
+ "node_modules/cookie": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
+ },
+ "node_modules/cookiejar": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
+ "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw=="
+ },
+ "node_modules/cosmiconfig": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
+ "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
+ "dependencies": {
+ "env-paths": "^2.2.1",
+ "import-fresh": "^3.3.0",
+ "js-yaml": "^4.1.0",
+ "parse-json": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/d-fischer"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.9.5"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/data-uri-to-buffer": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz",
+ "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/degenerator": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz",
+ "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==",
+ "dependencies": {
+ "ast-types": "^0.13.4",
+ "escodegen": "^2.1.0",
+ "esprima": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/destroy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
+ "engines": {
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/devtools-protocol": {
+ "version": "0.0.1330662",
+ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1330662.tgz",
+ "integrity": "sha512-pzh6YQ8zZfz3iKlCvgzVCu22NdpZ8hNmwU6WnQjNVquh0A9iVosPtNLWDwaWVGyrntQlltPFztTMK5Cg6lfCuw=="
+ },
+ "node_modules/ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.49",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.49.tgz",
+ "integrity": "sha512-ZXfs1Of8fDb6z7WEYZjXpgIRF6MEu8JdeGA0A40aZq6OQbS+eJpnnV49epZRna2DU/YsEjSQuGtQPPtvt6J65A==",
+ "peer": true
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/end-of-stream": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
+ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
+ "dependencies": {
+ "once": "^1.4.0"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "dependencies": {
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
+ },
+ "node_modules/escodegen": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz",
+ "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==",
+ "dependencies": {
+ "esprima": "^4.0.1",
+ "estraverse": "^5.2.0",
+ "esutils": "^2.0.2"
+ },
+ "bin": {
+ "escodegen": "bin/escodegen.js",
+ "esgenerate": "bin/esgenerate.js"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "optionalDependencies": {
+ "source-map": "~0.6.1"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/express": {
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
+ "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.20.3",
+ "content-disposition": "0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "0.7.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.3.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.10",
+ "proxy-addr": "~2.0.7",
+ "qs": "6.13.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "0.19.0",
+ "serve-static": "1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.10.0"
+ }
+ },
+ "node_modules/extract-zip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+ "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+ "dependencies": {
+ "debug": "^4.1.1",
+ "get-stream": "^5.1.0",
+ "yauzl": "^2.10.0"
+ },
+ "bin": {
+ "extract-zip": "cli.js"
+ },
+ "engines": {
+ "node": ">= 10.17.0"
+ },
+ "optionalDependencies": {
+ "@types/yauzl": "^2.9.1"
+ }
+ },
+ "node_modules/extract-zip/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/extract-zip/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="
+ },
+ "node_modules/fast-safe-stringify": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
+ },
+ "node_modules/fd-slicer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+ "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+ "dependencies": {
+ "pend": "~1.2.0"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "optional": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/finalhandler": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "2.0.1",
+ "unpipe": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.2.tgz",
+ "integrity": "sha512-sJe+TQb2vIaIyO783qN6BlMYWMw3WBOHA1Ay2qxsnjuafEOQFJ2JakedOQirT6D5XPRxDvS7AHYyem9fTpb4LQ==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/formidable": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/formidable/-/formidable-1.2.6.tgz",
+ "integrity": "sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==",
+ "deprecated": "Please upgrade to latest, formidable@v2 or formidable@v3! Check these notes: https://bit.ly/2ZEqIau",
+ "funding": {
+ "url": "https://ko-fi.com/tunnckoCore/commissions"
+ }
+ },
+ "node_modules/forwarded": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
+ "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=14.14"
+ }
+ },
+ "node_modules/fs-readdir-recursive": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz",
+ "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA=="
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "peer": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-stream": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+ "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+ "dependencies": {
+ "pump": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-uri": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.3.tgz",
+ "integrity": "sha512-BzUrJBS9EcUb4cFol8r4W3v1cPsSyajLSthNkz5BxbpDcHN5tIrM10E2eNvfnvBn3DaT3DUgx0OpsBKkaOpanw==",
+ "dependencies": {
+ "basic-ftp": "^5.0.2",
+ "data-uri-to-buffer": "^6.0.2",
+ "debug": "^4.3.4",
+ "fs-extra": "^11.2.0"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/get-uri/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/get-uri/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "optional": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/globals": {
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "peer": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+ },
+ "node_modules/handlebars": {
+ "version": "4.7.8",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
+ "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
+ "dependencies": {
+ "minimist": "^1.2.5",
+ "neo-async": "^2.6.2",
+ "source-map": "^0.6.1",
+ "wordwrap": "^1.0.0"
+ },
+ "bin": {
+ "handlebars": "bin/handlebars"
+ },
+ "engines": {
+ "node": ">=0.4.7"
+ },
+ "optionalDependencies": {
+ "uglify-js": "^3.1.4"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+ "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/http-errors": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+ "dependencies": {
+ "depd": "2.0.0",
+ "inherits": "2.0.4",
+ "setprototypeof": "1.2.0",
+ "statuses": "2.0.1",
+ "toidentifier": "1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/http-proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz",
+ "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==",
+ "dependencies": {
+ "agent-base": "^7.0.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/https-proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
+ "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/ip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
+ "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ=="
+ },
+ "node_modules/ip-address": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
+ "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
+ "dependencies": {
+ "jsbn": "1.1.0",
+ "sprintf-js": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "optional": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "optional": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "optional": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsbn": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
+ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
+ },
+ "node_modules/jsesc": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
+ "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
+ "peer": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "peer": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
+ },
+ "node_modules/lru-cache": {
+ "version": "7.18.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
+ "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
+ "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==",
+ "dependencies": {
+ "pify": "^4.0.1",
+ "semver": "^5.6.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "5.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+ "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
+ "bin": {
+ "semver": "bin/semver"
+ }
+ },
+ "node_modules/media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/merge-descriptors": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="
+ },
+ "node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
+ },
+ "node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/neo-async": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
+ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
+ },
+ "node_modules/netmask": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz",
+ "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/node-html-to-image": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/node-html-to-image/-/node-html-to-image-5.0.0.tgz",
+ "integrity": "sha512-mRnggiH3PLJbBaiUMc6Assw5EKwy4uaXVIBnfd/MiqWAOJN3A6ktDwpuKhpIVaXNPeojPCD//yDGGJT+GF2CPQ==",
+ "dependencies": {
+ "handlebars": "4.7.8",
+ "puppeteer": "23.2.2",
+ "puppeteer-cluster": "0.24.0"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
+ "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
+ "peer": true
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
+ "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/on-finished": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+ "dependencies": {
+ "ee-first": "1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/pac-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-BFi3vZnO9X5Qt6NRz7ZOaPja3ic0PhlsmCRYLOpN11+mWBCR6XJDqW5RF3j8jm4WGGQZtBA+bTfxYzeKW73eHg==",
+ "dependencies": {
+ "@tootallnate/quickjs-emscripten": "^0.23.0",
+ "agent-base": "^7.0.2",
+ "debug": "^4.3.4",
+ "get-uri": "^6.0.1",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.5",
+ "pac-resolver": "^7.0.1",
+ "socks-proxy-agent": "^8.0.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/pac-proxy-agent/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pac-proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/pac-resolver": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz",
+ "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==",
+ "dependencies": {
+ "degenerator": "^5.0.0",
+ "netmask": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-to-regexp": {
+ "version": "0.1.10",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
+ "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "optional": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
+ "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/progress": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/proxy-addr": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
+ "dependencies": {
+ "forwarded": "0.2.0",
+ "ipaddr.js": "1.9.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/proxy-agent": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz",
+ "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==",
+ "dependencies": {
+ "agent-base": "^7.0.2",
+ "debug": "^4.3.4",
+ "http-proxy-agent": "^7.0.1",
+ "https-proxy-agent": "^7.0.3",
+ "lru-cache": "^7.14.1",
+ "pac-proxy-agent": "^7.0.1",
+ "proxy-from-env": "^1.1.0",
+ "socks-proxy-agent": "^8.0.2"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/proxy-agent/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
+ "node_modules/pump": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
+ "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
+ "dependencies": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "node_modules/puppeteer": {
+ "version": "23.2.2",
+ "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.2.2.tgz",
+ "integrity": "sha512-3RX5vEhlOE/9M7ZXxoJVvI7pbOF3fzFK4etwbdii7w+r58q+krK5l2FXWlA1ETnNs7ilM/KLFc0n6K8VUi99dA==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "@puppeteer/browsers": "2.3.1",
+ "chromium-bidi": "0.6.5",
+ "cosmiconfig": "^9.0.0",
+ "devtools-protocol": "0.0.1330662",
+ "puppeteer-core": "23.2.2",
+ "typed-query-selector": "^2.12.0"
+ },
+ "bin": {
+ "puppeteer": "lib/cjs/puppeteer/node/cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/puppeteer-cluster": {
+ "version": "0.24.0",
+ "resolved": "https://registry.npmjs.org/puppeteer-cluster/-/puppeteer-cluster-0.24.0.tgz",
+ "integrity": "sha512-zHPoNsrwkFLKFtgJJv2aC13EbMASQsE048uZd7CyikEXcl+sc1Nf6PMFb9kMoZI7/zMYxvuP658o2mw079ZZyQ==",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "peerDependencies": {
+ "puppeteer": ">=22.0.0"
+ }
+ },
+ "node_modules/puppeteer-cluster/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/puppeteer-cluster/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/puppeteer-core": {
+ "version": "23.2.2",
+ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.2.2.tgz",
+ "integrity": "sha512-MK2Kbdmro+nX9/pfGKm8TiU5G3CuS6BbcNfkcz42GWnHp7KYsJHrP6lPDepiyvwjti5Bt/4a8U3w+DoFpIcBHQ==",
+ "dependencies": {
+ "@puppeteer/browsers": "2.3.1",
+ "chromium-bidi": "0.6.5",
+ "debug": "^4.3.6",
+ "devtools-protocol": "0.0.1330662",
+ "typed-query-selector": "^2.12.0",
+ "ws": "^8.18.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/puppeteer-core/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/puppeteer-core/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/qs": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
+ "dependencies": {
+ "side-channel": "^1.0.6"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/queue-tick": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz",
+ "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="
+ },
+ "node_modules/range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/raw-body": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
+ "dependencies": {
+ "bytes": "3.1.2",
+ "http-errors": "2.0.0",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "optional": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "node_modules/semver": {
+ "version": "7.6.3",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
+ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/send": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
+ "dependencies": {
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "2.0.0",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/send/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/serve-static": {
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
+ "dependencies": {
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.19.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setprototypeof": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
+ },
+ "node_modules/side-channel": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+ "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/slash": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz",
+ "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
+ "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
+ "dependencies": {
+ "ip-address": "^9.0.5",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "8.0.4",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz",
+ "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==",
+ "dependencies": {
+ "agent-base": "^7.1.1",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/socks-proxy-agent/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/socks-proxy-agent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
+ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
+ },
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/streamx": {
+ "version": "2.20.1",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.20.1.tgz",
+ "integrity": "sha512-uTa0mU6WUC65iUvzKH4X9hEdvSW7rbPxPtwfWiLMSj3qTdQbAiUboZTxauKfpFuGIGa1C2BYijZ7wgdUXICJhA==",
+ "dependencies": {
+ "fast-fifo": "^1.3.2",
+ "queue-tick": "^1.0.1",
+ "text-decoder": "^1.1.0"
+ },
+ "optionalDependencies": {
+ "bare-events": "^2.2.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/superagent": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.3.1.tgz",
+ "integrity": "sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==",
+ "deprecated": "Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net",
+ "dependencies": {
+ "component-emitter": "^1.3.0",
+ "cookiejar": "^2.1.2",
+ "debug": "^4.1.1",
+ "fast-safe-stringify": "^2.0.7",
+ "form-data": "^3.0.0",
+ "formidable": "^1.2.2",
+ "methods": "^1.1.2",
+ "mime": "^2.4.6",
+ "qs": "^6.9.4",
+ "readable-stream": "^3.6.0",
+ "semver": "^7.3.2"
+ },
+ "engines": {
+ "node": ">= 7.0.0"
+ }
+ },
+ "node_modules/superagent/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/superagent/node_modules/mime": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
+ "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/superagent/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/tar-fs": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.6.tgz",
+ "integrity": "sha512-iokBDQQkUyeXhgPYaZxmczGPhnhXZ0CmrqI+MOb/WFGS9DW5wnfrLgtjUJBvz50vQ3qfRwJ62QVoCFu8mPVu5w==",
+ "dependencies": {
+ "pump": "^3.0.0",
+ "tar-stream": "^3.1.5"
+ },
+ "optionalDependencies": {
+ "bare-fs": "^2.1.1",
+ "bare-path": "^2.1.0"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
+ "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/text-decoder": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.1.tgz",
+ "integrity": "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ=="
+ },
+ "node_modules/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "optional": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/toidentifier": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
+ "engines": {
+ "node": ">=0.6"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz",
+ "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA=="
+ },
+ "node_modules/type-is": {
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
+ "dependencies": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/typed-query-selector": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz",
+ "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg=="
+ },
+ "node_modules/uglify-js": {
+ "version": "3.19.3",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
+ "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
+ "optional": true,
+ "bin": {
+ "uglifyjs": "bin/uglifyjs"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/unbzip2-stream": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz",
+ "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==",
+ "dependencies": {
+ "buffer": "^5.2.1",
+ "through": "^2.3.8"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "6.19.8",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+ "optional": true
+ },
+ "node_modules/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
+ "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "peer": true,
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.0"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/urlpattern-polyfill": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz",
+ "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg=="
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
+ },
+ "node_modules/utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
+ "node_modules/vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/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==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/wrap-ansi/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=="
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "peer": true
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yauzl": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+ "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "fd-slicer": "~1.1.0"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.23.8",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
+ "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+}
diff --git a/backend/package.json b/backend/package.json
new file mode 100644
index 0000000..2699e61
--- /dev/null
+++ b/backend/package.json
@@ -0,0 +1,6 @@
+{
+ "dependencies": {
+ "express": "^4.21.1",
+ "pdf.js-extract": ""
+ }
+}
diff --git a/backend/packing_directory_script b/backend/packing_directory_script
new file mode 100755
index 0000000..567d0cc
--- /dev/null
+++ b/backend/packing_directory_script
@@ -0,0 +1,14 @@
+#!/bin/bash
+
+cd public
+echo -n '[' > ../pack.json; for X in *;
+do
+ echo -n "{\"filename\":\"$X\", \"content\":\"$(base64 -w0 $X)\"" >> ../pack.json;
+ # curl --insecure -I https://192.168.0.129:3000/$X | grep "Content-Type" | sed 's/;.*//' | sed 's/\(.*\): \(.*\)/,"\1":"\2"/' #>> ../pack.json;
+ echo -n ",\"contenttype\":\"" >> ../pack.json;
+ curl --insecure -I -s -o /dev/null -w '%header{Content-Type}' https://192.168.0.129:3000/$X | sed 's/;.*//' >> ../pack.json;
+ echo -n "\"}," >> ../pack.json;
+done;
+
+sed -i "s/.$//" ../pack.json;
+echo -n "]" >> ../pack.json
diff --git a/backend/pdf_xml.js b/backend/pdf_xml.js
new file mode 100644
index 0000000..e69de29
diff --git a/backend/pmwiki_reader.lua b/backend/pmwiki_reader.lua
new file mode 100644
index 0000000..36c45d8
--- /dev/null
+++ b/backend/pmwiki_reader.lua
@@ -0,0 +1,194 @@
+-- Pandoc reader for PMWiki format: https://www.pmwiki.org/wiki/PmWiki/MarkupMasterIndex
+-- Using LPeg: https://www.inf.puc-rio.br/~roberto/lpeg/
+-- Inspired by https://pandoc.org/custom-readers.html
+local P, S, R, Cf, Cc, Ct, V, Cs, Cg, Cb, B, C, Cmt =
+ lpeg.P, lpeg.S, lpeg.R, lpeg.Cf, lpeg.Cc, lpeg.Ct, lpeg.V,
+ lpeg.Cs, lpeg.Cg, lpeg.Cb, lpeg.B, lpeg.C, lpeg.Cmt
+
+local whitespacechar = S(" \t\r\n")
+local specialchar = S("/*~[]\\{}|")
+local wordchar = (1 - (whitespacechar + specialchar))
+local spacechar = S(" \t")
+local newline = P"\r"^-1 * P"\n"
+local blankline = spacechar^0 * newline
+local endline = newline * #-blankline
+local endequals = spacechar^0 * P"="^0 * spacechar^0 * newline
+local cellsep = spacechar^0 * P"|"
+local apostrophe = string.char(39)
+local doubleApo = P(apostrophe) * P(apostrophe)
+local fenced = '```\n%s\n```\n'
+local cellsep = spacechar^0 * P"||"
+
+local function trim(s)
+ return (s:gsub("^%s*(.-)%s*$", "%1"))
+end
+
+local function ListItem(lev, ch)
+ local start
+ if ch == nil then
+ start = S"*#"
+ else
+ start = P(ch)
+ end
+ local subitem = function(c)
+ if lev < 6 then
+ return ListItem(lev + 1, c)
+ else
+ return (1 - 1) -- fails
+ end
+ end
+ local parser = spacechar^0
+ * start^lev
+ * #(- start)
+ * spacechar^0
+ * Ct((V"Inline" - (newline * spacechar^0 * S"*#"))^0)
+ * newline
+ * (Ct(subitem("*")^1) / pandoc.BulletList
+ +
+ Ct(subitem("#")^1) / pandoc.OrderedList
+ +
+ Cc(nil))
+ / function (ils, sublist)
+ return { pandoc.Plain(ils), sublist }
+ end
+ return parser
+end
+
+-- Grammar
+G = P{ "Doc",
+ Doc = Ct(V"Block"^0)
+ / pandoc.Pandoc ;
+ Block = blankline^0
+ * ( V"IndentedBlock"
+ + V"Header"
+ + V"HorizontalRule"
+ + V"CodeBlock"
+ + V"List"
+ + V"Table"
+ + V"Para"
+ ) ;
+ IndentedBlock = C((spacechar^1
+ * (1 - newline)^1
+ * newline)^1
+ )
+ / function(text)
+ block = pandoc.RawBlock('markdown', fenced:format(text))
+ return block
+ end;
+ CodeBlock = P"[@"
+ * blankline
+ * C((1 - (newline * P"@]"))^0)
+ * newline
+ * P"@]"
+ / function(text)
+ block = pandoc.RawBlock('markdown', fenced:format(text))
+ return block
+ end;
+ List = V"BulletList"
+ + V"OrderedList" ;
+ BulletList = Ct(ListItem(1,'*')^1)
+ / pandoc.BulletList ;
+ OrderedList = Ct(ListItem(1,'#')^1)
+ / pandoc.OrderedList ;
+ Table = V"TableProperties"
+ * (V"TableHeader" + Cc{})
+ * Ct(V"TableRow"^1)
+ / function(headrow, bodyrows)
+ local numcolumns = #(bodyrows[1])
+ local aligns = {}
+ local widths = {}
+ for i = 1,numcolumns do
+ aligns[i] = pandoc.AlignDefault
+ widths[i] = 0
+ end
+ return pandoc.utils.from_simple_table(
+ pandoc.SimpleTable({}, aligns, widths, headrow, bodyrows))
+ end ;
+ TableProperties = cellsep
+ * spacechar^0
+ * P("border=")
+ * (1 - newline)^1
+ * newline;
+ TableHeader = Ct(V"HeaderCell"^1)
+ * cellsep^-1
+ * spacechar^0
+ * newline ;
+ TableRow = Ct(V"BodyCell"^1)
+ * cellsep^-1
+ * spacechar^0
+ * newline ;
+ HeaderCell = cellsep
+ * P"!"^-1
+ * spacechar^0
+ * Ct((V"Inline" - (newline + cellsep))^0)
+ / function(ils) return { pandoc.Plain(ils) } end ;
+ BodyCell = cellsep
+ * spacechar^0
+ * Ct((V"Inline" - (newline + cellsep))^0)
+ / function(ils) return { pandoc.Plain(ils) } end ;
+ Para = Ct(V"Inline"^1)
+ * newline
+ / pandoc.Para ;
+ HorizontalRule = spacechar^0
+ * P"----"
+ * spacechar^0
+ * newline
+ / pandoc.HorizontalRule;
+ Header = (P("!")^1 / string.len)
+ * spacechar^0
+ * Ct((V"Inline" - endequals)^1)
+ * endequals
+ / pandoc.Header;
+ Inline = V"Link"
+ + V"Url"
+ + V"Code"
+ + V"Bold"
+ + V"Emph"
+ + V"Strikeout"
+ + V"Str"
+ + V"Space"
+ + V"Special";
+ Link = P"[["
+ * C((1 - (P"]]" + P"|"))^0)
+ * (P"|" * Ct((V"Inline" - P"]]")^1))^-1
+ * P"]]"
+ / function(url, desc)
+ local txt = desc or {pandoc.Str(url)}
+ return pandoc.Link(txt, url)
+ end;
+ Url = C(
+ P"http"
+ * P"s"^-1
+ * P"://"
+ * (1 - whitespacechar)^1
+ )
+ / function(url)
+ return pandoc.Link(url, url)
+ end;
+ Code = P'@@'
+ * C((1 - P'@@')^0)
+ * P'@@'
+ / trim / pandoc.Code;
+ Emph = P"''"
+ * C(((wordchar + whitespacechar) - P"''")^1)
+ * P"''"
+ / pandoc.Emph;
+ Bold = P"'''"
+ * C(((wordchar + whitespacechar) - P"'''")^1)
+ * P"'''"
+ / pandoc.Strong;
+ Strikeout = P"{-"
+ * C(((wordchar + whitespacechar) - P"-}")^1)
+ * P"-}"
+ / pandoc.Strikeout;
+ Str = wordchar^1
+ / pandoc.Str;
+ Special = specialchar
+ / pandoc.Str;
+ Space = spacechar^1
+ / pandoc.Space ;
+}
+
+function Reader(input, reader_options)
+ return lpeg.match(G, tostring(input))
+end
diff --git a/backend/stt/whisper_setup.txt b/backend/stt/whisper_setup.txt
new file mode 100644
index 0000000..26d117a
--- /dev/null
+++ b/backend/stt/whisper_setup.txt
@@ -0,0 +1,2 @@
+ffmpeg -i public/poweruser_audiofile.ogg -ar 16000 -y terst.wav
+LD_LIBRARY_PATH=/usr/app/whisper.cpp/build/bin/ /usr/app/whisper.cpp/build/bin/whisper-cli -f terst.wav -m /usr/app/whisper.cpp/models/ggml-base.en.bin
diff --git a/backend/withdefault.jxrstyles.json b/backend/withdefault.jxrstyles.json
new file mode 100644
index 0000000..b5abac9
--- /dev/null
+++ b/backend/withdefault.jxrstyles.json
@@ -0,0 +1,26 @@
+{
+ "default" : [
+ {"selector":"#start_file_sloan_testtxt_end_file_hello_worldtxt", "attribute":"line", "value": "color:blue"},
+ {"selector":"a-sky", "attribute":"color", "value": "lightblue"},
+ {"selector":".notes", "attribute":"color", "value": "purple"},
+ {"selector":".notes", "attribute":"outline-color", "value": "darkblue"},
+ {"selector":"a-troika-text a-plane", "attribute":"color", "value": "white"},
+ {"selector":"a-troika-text a-triangle", "attribute":"color", "value": "gray"}
+ ],
+ "light" : [
+ {"selector":"#start_file_sloan_testtxt_end_file_hello_worldtxt", "attribute":"line", "value": "color:blue"},
+ {"selector":"a-sky", "attribute":"color", "value": "gray"},
+ {"selector":".notes", "attribute":"color", "value": "black"},
+ {"selector":".notes", "attribute":"outline-color", "value": "white"},
+ {"selector":"a-troika-text a-plane", "attribute":"color", "value": "red"},
+ {"selector":"a-troika-text a-triangle", "attribute":"color", "value": "darkred"}
+ ],
+ "print" : [
+ {"selector":"#start_file_sloan_testtxt_end_file_hello_worldtxt", "attribute":"line", "value": "color:brown"},
+ {"selector":"a-sky", "attribute":"color", "value": "#EEE"},
+ {"selector":".notes", "attribute":"color", "value": "black"},
+ {"selector":".notes", "attribute":"outline-color", "value": "white"},
+ {"selector":"a-troika-text a-plane", "attribute":"color", "value": "lightyellow"},
+ {"selector":"a-troika-text a-triangle", "attribute":"color", "value": "orange"}
+ ]
+}
diff --git a/data/console.aframe.component b/data/console.aframe.component
new file mode 100644
index 0000000..4b2e663
--- /dev/null
+++ b/data/console.aframe.component
@@ -0,0 +1 @@
+console.log('test')
diff --git a/data/default.layout.json b/data/default.layout.json
new file mode 100644
index 0000000..fe51488
--- /dev/null
+++ b/data/default.layout.json
@@ -0,0 +1 @@
+[]
diff --git a/data/demo_q1.json b/data/demo_q1.json
index 1c53e78..c1cde71 100644
--- a/data/demo_q1.json
+++ b/data/demo_q1.json
@@ -7,23 +7,25 @@
"content":[
{"name":"Curated demos for Q1", "old_description":"use the left wrist to show commands, look behind and press nextDemo()",
"alt":"You have a sphere on your left wrist, touch it to view code snippets",
-"description":"You have a sphere on your left wrist.\n You can tap it to reveal snippets of code with your pointing finger.\n Touching these on the left side allows you to:\n - Move them with your right hand\n - Execute with your left hand\n\nTo go to the next step in the demo, look behind you and tap 'jxr demoNext()' (edited)",
+"description":"You have a sphere on your left wrist.\n You can tap it to reveal snippets of code with your pointing finger.\n Touching these on the left side allows you to:\n - Move them with your right hand\n - Execute with your left hand\n\nTo go to the next step in the demo, look behind you and tap 'jxr nextDemo()'",
"screenshot":"demoqueueq1.png",
+"code":["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L79","https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L1912-L1922", "https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L785-L799"] ,
"usernames":["demoqueueq1"] },
- {"name":"Physical Table in VR (alignment)", "video":"https://youtu.be/A_vH3wRVX_4?t=3336", "description":"move the yellow table from center and release it by your desk height", "screenshot":"tabletest.png", "usernames":["tabletest"]},
- {"name":"Tap wrist as shortcut", "description":"left wrist to show hide/code snippets","usernames":["q1_step_wrist"] },
- {"name":"Shortcut binding", "description":"drag and drop onto wrist to make new command\ntry with nextDemo() then tap it to move on", "usernames":["q1_step_shortcutset"] },
- {"name":"Highlight Text", "video":"https://youtu.be/A_vH3wRVX_4?t=5446", "description": "Puck a line from a PDF to change its coloor.\nThe result becomes available in 2D for yourself and others.\n\nUse the highlighters to freely draw over the document, under its text.\n\nSee https://companion.benetou.fr/highlights_example.html", "screenshot":"q1_step_highlights.png", "usernames":["q1_step_highlights"] },
- {"name":"References cards", "video":"https://youtu.be/A_vH3wRVX_4?t=6541", "description": "Load a bibliography and manipulate reference as cards.\nSee https://companion.benetou.fr/references_manual_v04.json", "screenshot":"q1_step_refcards.png", "usernames":["q1_step_refcards"] },
- {"name":"Manuscript stick to closest panels", "description":"Use the right wrist to show commands, show panels,\npick then release the manuscript from its center to drop it on the closest panel.", "usernames":["q1_step_snappanels"] },
- {"name":"Unfolding Cube", "screenshot":"demo_cube_screenshot.jpg", "description":"Unfold and fold the cube, scale it to room scale\nthen back to the size of your hand to mive.", "screenshot":"refoncubetester.png", "usernames":["refoncubetester"] },
- {"name":"Screenshot in VR", "description": "Document your process by taking screenshots\nthat become instantly available on the Web for yourself\nand to collaborators.\nSee https://companion.benetou.fr/audio_notes_example.html", "usernames":["q1_step_screenshot"] },
- {"name":"Audio recording ", "screenshot":"poweruser_screenshot_1739174489566.jpg", "description": "Document the screenshots by talking over them.\nThey also become available to share.\n\nTranscriptions are used to make the documentation searchable.\nSee https://companion.benetou.fr/audio_notes_example.html", "usernames":["q1_step_audio"] },
+ {"name":"Physical Table in VR (alignment)", "video":"https://youtu.be/A_vH3wRVX_4?t=3336", "description":"move the yellow table from center and release it by your desk height", "screenshot":"tabletest.png", "usernames":["tabletest"], "code":["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L675-L684"]},
+ {"name":"Tap wrist as shortcut", "description":"left wrist to show hide/code snippets","usernames":["q1_step_wrist"], "code":["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/jxr-core.js#L586-L652", "https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/jxr-core.js#L674-L729"] },
+ {"name":"Shortcut binding", "description":"drag and drop onto wrist to make new command\ntry with nextDemo() then tap it to move on", "usernames":["q1_step_shortcutset"], "code":["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/jxr-core.js#L13", "https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/jxr-core.js#L182-L185"] },
+ {"name":"Highlight Text", "video":"https://youtu.be/A_vH3wRVX_4?t=5446", "description": "Puck a line from a PDF to change its coloor.\nThe result becomes available in 2D for yourself and others.\n\nUse the highlighters to freely draw over the document, under its text.\n\nSee https://companion.benetou.fr/highlights_example.html", "screenshot":"q1_step_highlights.png", "usernames":["q1_step_highlights"], "code":["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L825-L842", "https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L966-L1027"] },
+ {"name":"References cards", "video":"https://youtu.be/A_vH3wRVX_4?t=6541", "description": "Load a bibliography and manipulate reference as cards.\nSee https://companion.benetou.fr/references_manual_v04.json", "screenshot":"q1_step_refcards.png", "usernames":["q1_step_refcards"], "code":["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L806-L818","https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/filters/json_ref_manual.js"] },
+ {"name":"Manuscript stick to closest panels", "description":"Use the right wrist to show commands, show panels,\npick then release the manuscript from its center to drop it on the closest panel.", "usernames":["q1_step_snappanels"], "code":["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L1668-L1693"] },
+ {"name":"Unfolding Cube", "screenshot":"demo_cube_screenshot.jpg", "description":"Unfold and fold the cube, scale it to room scale\nthen back to the size of your hand to mive.", "screenshot":"refoncubetester.png", "usernames":["refoncubetester"], "code":["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L690-L712", "https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L1707-L1878"] },
+ {"name":"Screenshot in VR", "description": "Document your process by taking screenshots\nthat become instantly available on the Web for yourself\nand to collaborators.\nSee https://companion.benetou.fr/audio_notes_example.html", "usernames":["q1_step_screenshot"], "code":["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L851-L877", "https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L1601-L1618"] },
+ {"name":"Audio recording ", "screenshot":"poweruser_screenshot_1739174489566.jpg", "description": "Document the screenshots by talking over them.\nThey also become available to share.\n\nTranscriptions are used to make the documentation searchable.\nSee https://companion.benetou.fr/audio_notes_example.html", "usernames":["q1_step_audio"], "code": ["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L844-L849", "https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L1451-L1583", "https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/filters/srt_to_json.js", "https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/backend/converters/ogg_tts.js"] },
{"name":"Visual Background ", "description":"(non-functional) for grey, room (3D model) and ornaments (animations in background), potential for ambient info as image or semantically integrated widgets",
- "usernames":[ "backgroundexploration", "backgroundexplorationlowopacity", "backgroundexplorationlowwhitestatic", "backgroundexplorationlowwhite", "backgroundexplorationlowwhitegrids" ]
+ "usernames":[ "backgroundexploration", "backgroundexplorationlowopacity", "backgroundexplorationlowwhitestatic", "backgroundexplorationlowwhite", "backgroundexplorationlowwhitegrids" ],
+ "code": ["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L78", "https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L938-L948", "https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L2277-L2301"]
},
- {"name":"Customization via URL set", "description":"Modify the URL to customize the experience and share that with others, e.g. https://companion.benetou.fr/index.html?set_IDmanuscript_color=lightyellow", "usernames":["q1_step_urlcustom"] },
- {"name":"Upload Document via desktop", "description":"Using a desktop or laptop, drag and drop an image in the top right corner, see the result live in XR.", "usernames":["q1_step_showfile"] },
+ {"name":"Customization via URL set", "description":"Modify the URL to customize the experience and share that with others, e.g. https://companion.benetou.fr/index.html?set_IDmanuscript_color=lightyellow", "usernames":["q1_step_urlcustom"], "code": ["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/filters/modifications_via_url.js"] },
+ {"name":"Upload Document via desktop", "description":"Using a desktop or laptop, drag and drop an image in the top right corner, see the result live in XR.", "usernames":["q1_step_showfile"], "code": ["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L88-L138"] },
{"name":"End of currated demos for Q1", "description":"Thank you for testing, please feel free to share idea on how to open this work more.", "usernames":["demoqueueq1end"] },
{"unassigned-usernames":[
"poweruser",
diff --git a/data/demo_q2.json b/data/demo_q2.json
new file mode 100644
index 0000000..f53c622
--- /dev/null
+++ b/data/demo_q2.json
@@ -0,0 +1,52 @@
+{
+ "configuration": {
+ "description":"update the URL sequentially because they are hosted on the same domain",
+ "clarification":"usernames are used as identifiers and thus must be unique, even if leading to no behavior change. Example would q1step4 then q1step5.",
+ "prefixurl":"https://companion.benetou.fr/index.html?username="
+ },
+ "content":[
+ {"name":"automated hand testing", "testingurl": ["https://companion.benetou.fr/index.html?username=q2_step_refcards_filtering&emulatexr=true"], "description":"left wrist to show hide/code snippets","usernames":["q1_step_wrist"], "code":["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/jxr-core.js#L586-L652", "https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/jxr-core.js#L674-L729"] },
+ {"name":"refcards filtering", "testingurl": ["https://companion.benetou.fr/index.html?username=q2_step_refcards_filtering&emulatexr=true"], "description":"left wrist to show hide/code snippets","usernames":["q1_step_wrist"], "code":["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/jxr-core.js#L586-L652", "https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/jxr-core.js#L674-L729"] },
+ {"name":"volumetric frames", "video":"https://youtu.be/A_vH3wRVX_4?t=3336", "description":"move the yellow table from center and release it by your desk height", "screenshot":"tabletest.png", "usernames":["tabletest"], "testingurl": ["https://companion.benetou.fr/index.html?username=q2_step_volumetric_frames&emulatexr=true"], "code":["https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L675-L684"]},
+{"name": "icon_tags","usernames":[ "icon_tags"]},
+{"name": "q2_annotated_bibliography","usernames":[ "q2_annotated_bibliography"]},
+{"name": "q2_annotated_bibliography_week2","usernames":[ "q2_annotated_bibliography_week2"]},
+{"name": "q2_drop_for_graph","usernames":[ "q2_drop_for_graph"]},
+{"name": "q2_immersive_console","usernames":[ "q2_immersive_console"]},
+{"name": "q2_json_collaborations","usernames":[ "q2_json_collaborations"]},
+{"name": "q2_lense","usernames":[ "q2_lense"]},
+{"name": "q2_os_keyboard","usernames":[ "q2_os_keyboard"]},
+{"name": "q2_pasting","usernames":[ "q2_pasting"]},
+{"name": "q2_picker","usernames":[ "q2_picker"]},
+{"name": "q2_remote_ntfy_keyboard","usernames":[ "q2_remote_ntfy_keyboard"]},
+{"name": "q2_ring_keyboard","usernames":[ "q2_ring_keyboard"]},
+{"name": "q2_step_contextuallayouts","usernames":[ "q2_step_contextuallayouts"]},
+{"name": "q2_step_end","usernames":[ "q2_step_end"]},
+{"name": "q2_step_highlight","usernames":[ "q2_step_highlight"]},
+{"name": "q2_step_jsonedit","usernames":[ "q2_step_jsonedit"]},
+{"name": "q2_step_layout_animationtests","usernames":[ "q2_step_layout_animationtests"]},
+{"name": "q2_step_refcards_filtering","usernames":[ "q2_step_refcards_filtering"]},
+{"name": "q2_step_start","usernames":[ "q2_step_start"]},
+{"name": "q2_step_volumetric_frames","usernames":[ "q2_step_volumetric_frames"]},
+{"name": "q2_visualmetaexport","usernames":[ "q2_visualmetaexport"]},
+{"name": "q2_visualmetaexport_map","usernames":[ "q2_visualmetaexport_map"]},
+{"name": "q2_visualmetaexport_map_via_wordpress","usernames":[ "q2_visualmetaexport_map_via_wordpress"]},
+{"name": "q2_wrist_rotations","usernames":[ "q2_wrist_rotations"]},
+{"name": "ring_discovery","usernames":[ "ring_discovery"]},
+{"name": "ring_discovery_with_keyboard","usernames":[ "ring_discovery_with_keyboard"]},
+{"name": "ring_highlights","usernames":[ "ring_highlights"]},
+{"name": "temple_test","usernames":[ "temple_test"]},
+{"name": "q2_most_recent_file","usernames":[ "q2_most_recent_file"]},
+{"name": "q2_onrelease_lookat","usernames":[ "q2_onrelease_lookat"]}
+ ],
+
+ "withemulation":{
+ "keyword":"emulatexr=true",
+ "content":[
+ "q2_lense",
+ "q2_step_refcards_filtering",
+ "q2_step_volumetric_frames",
+ "q2_step_layout_animationtests"
+ ]
+ }
+}
diff --git a/data/demos_editor_example.html b/data/demos_editor_example.html
index 7b40961..e454200 100644
--- a/data/demos_editor_example.html
+++ b/data/demos_editor_example.html
@@ -4,6 +4,7 @@
+
save
@@ -34,7 +35,8 @@ function save(){
if ( uu ) newContent.content.push( uu )
let filename = "demos_saved_"+Date.now()+".json"
saveJSONToWebDAV( filename, newContent )
- let url = webdavURL+subdirWebDAV+filename
+ let url = "https://companion.benetou.fr/demos_example.html?filename="+filename
+ // let url = webdavURL+subdirWebDAV+filename
window.open(url, '_blank')
}
@@ -102,7 +104,8 @@ ideas :
-code links, e.g. "code":["https://git.benetou.fr/utopiah/text-code-xr-engine/src/branch/master/index.html#L129", "https://git.benetou.fr/utopiah/text-code-xr-engine/src/branch/master/index.html#L1375-L1408"]
+ remove items (to split into N demos)
+ update code link to proper repository, i.e. https://git.benetou.fr/utopiah/spasca-fot-sloan-q1
better integration with viewer (e.g. side by side view)
in XR mode
listing of past saved demo queues
@@ -117,6 +120,7 @@ done :
linked from viewer
re-order
test output https://companion.benetou.fr/demos_saved_1742539103750.json
+ open directly in viewer, e.g. https://companion.benetou.fr/demos_example.html?filename=demos_saved_1742882525475.json
diff --git a/data/demos_example.html b/data/demos_example.html
index 66adeb6..bfb1c8c 100644
--- a/data/demos_example.html
+++ b/data/demos_example.html
@@ -4,6 +4,7 @@
+
@@ -17,19 +18,31 @@ const subdirWebDAV = "/fotsave/fot_sloan_companion_public/"
var webdavClient = window.WebDAV.createClient(webdavURL)
const hmdURL = "https://hmd.link/?https://companion.benetou.fr"
+const urlParams = new URLSearchParams(window.location.search);
+let filename = urlParams.get('filename');
+if (!filename) filename = 'demo_q1.json'
+
+// should update on query parameter
+ // should then use that in demo editor page
+
// const eventSourceConverted = new EventSource( `https://ntfy.benetou.fr/convertedwebdav/sse` )
// to use for live updates
async function getContent(){
let rootEl = document.getElementById("content")
+ /*
const contents = await webdavClient.getDirectoryContents(subdirWebDAV);
// consider instead search https://github.com/perry-mitchell/webdav-client#search
- contents.filter(f => f.basename.endsWith('demo_q1.json'))
+ contents.filter(f => f.basename.endsWith( filename ))
.map(a => {
fetch(a.basename).then( r => r.json() ).then( r => {
+ */
+ // removed direct WebDAV as it seems to create CORS issues (which... wasn't the case until now?)
+ fetch(filename).then( r => r.json() ).then( r => {
r.content.filter( c => c.name ).map( c => {
let h2 = document.createElement("h2")
h2.innerText = c.name
+ h2.id = c.name.replaceAll(' ', '_') // is added after the page has loaded... should position back
rootEl.appendChild(h2)
if ( c.screenshot ){
let img = document.createElement("img")
@@ -77,12 +90,43 @@ async function getContent(){
ul.appendChild(li)
})
}
+ if (c.testingurl) {
+ let h3 = document.createElement("h4")
+ h3.innerHTML = "testing URL:"
+ rootEl.appendChild(h3)
+ let ul = document.createElement("ul")
+ rootEl.appendChild(ul)
+ c.testingurl.map( h => {
+ let li = document.createElement("li")
+ let link = document.createElement("a")
+ link.href = h
+ link.innerText = h.replace(/.*\//,'')
+ li.appendChild(link)
+ ul.appendChild(li)
+ })
+ }
+ if (c.code) {
+ let h3 = document.createElement("h4")
+ h3.innerHTML = "code:"
+ rootEl.appendChild(h3)
+ let ul = document.createElement("ul")
+ rootEl.appendChild(ul)
+ c.code.map( h => {
+ let li = document.createElement("li")
+ let link = document.createElement("a")
+ link.href = h
+ link.innerText = h.replace(/.*\//,'')
+ li.appendChild(link)
+ ul.appendChild(li)
+ })
+ }
let hr = document.createElement("hr")
rootEl.appendChild(hr)
})
+ if (location.hash) document.getElementById(location.hash.replace('#','')).scrollIntoView()
})
- })
+ //})
}
getContent()
@@ -102,8 +146,9 @@ ideas :
integrate better live messages (via ?allowNtfyFeedbackHUD=true , e.g. https://companion.benetou.fr/index.htm?allowNtfyFeedbackHUD=true )
JSON editing, either as-is or via PmWiki (including raw text within JSON) or with CodeMirror as editor (just text area is plain text should be enough
feedback intertwined per demo (based on screenshot/audio recording demos)
- richer text rendering
+ richer text rendering (beyond just clickable link)
couple live messages with inView(targetSelector)
+ showcase sequentialFiltersInteractionOnReleased
@@ -117,6 +162,7 @@ done :
alternative descriptions https://futuretextlab.info/1st-quarter/ (more verbose but static)
mobile view
linked to editor : https://companion.benetou.fr/demos_editor_example.html
+ edited list https://companion.benetou.fr/demos_example.html?filename=demos_saved_1742834331999.json
diff --git a/data/demos_feedback_example.html b/data/demos_feedback_example.html
new file mode 100644
index 0000000..d918b14
--- /dev/null
+++ b/data/demos_feedback_example.html
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+Feedback on XR for academic authoring demo
+
+Please feel the form freely. You can add comment per demo, all optionals, and at the end global feedback and your email. Every word is appreciated. Write as much or as little as you like. Press send at the bottom to save.
+
+
+
+
+
+
+
+
+
+
+ideas :
+
+
+
+save on local storage (if so, provide a clear all fields button)
+ accept audio feedback
+ ntfy on feedback received
+
+
+
+done :
+
+
+
+responsive-ish design
+ save, including which JSON source file was used (now that experiment list and order can be modified)
+ principle
+
+
+
+
diff --git a/data/demos_feedback_viewer_example.html b/data/demos_feedback_viewer_example.html
new file mode 100644
index 0000000..70b5a87
--- /dev/null
+++ b/data/demos_feedback_viewer_example.html
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+Feedback on XR for academic authoring demo
+
+
+
+
+
+
+
+
+
+
+ideas :
+
+
+
+link back to go anchors on orignial demo viewer page
+
+
+
+done :
+
+
+
+link to proper example, e.g. https://companion.benetou.fr/demos_feedback_viewer_example.html?filename=demos_feedback_1742934066973.json
+ display feedback
+
+
+
+
diff --git a/data/fabien_corneish_zen.keymap b/data/fabien_corneish_zen.keymap
new file mode 100644
index 0000000..ff5f4e5
--- /dev/null
+++ b/data/fabien_corneish_zen.keymap
@@ -0,0 +1,75 @@
+#include
+#include
+#include
+
+/ {
+ chosen {
+ zmk,matrix_transform = &default_transform;
+ //zmk,matrix_transform = &five_column_transform;
+ };
+};
+
+/ {
+
+
+ behaviors {
+ hm: homerow_mods {
+ compatible = "zmk,behavior-hold-tap";
+ label = "HOMEROW_MODS";
+ #binding-cells = <2>;
+ tapping-term-ms = <150>;
+ quick-tap-ms = <0>;
+ flavor = "tap-preferred";
+ bindings = <&kp>, <&kp>;
+ };
+ };
+
+ keymap {
+ compatible = "zmk,keymap";
+
+ default_layer {
+ label = "QWERTY";
+// -----------------------------------------------------------------------------------------
+// | TAB | Q | W | E | R | T | | Y | U | I | O | P | BKSP |
+// | CTRL | A | S | D | F | G | | H | J | K | L | ; | ' |
+// | SHFT | Z | X | C | V | B | | N | M | , | . | / | ESC |
+// | GUI | LWR | SPC | | ENT | RSE | ALT |
+ bindings = <
+ &kp TAB &kp Q &kp W &kp E &kp R &kp T &kp Y &kp U &kp I &kp O &kp P &kp BSPC
+ &kp LCTRL &kp A &kp S &kp D &kp F &kp G &kp H &kp J &kp K &kp L &kp SEMI &kp SQT
+ //&kp LCTRL &kp A &kp S &kp D &kp F &kp G &kp H &kp J &kp K &kp L &kp SEMI &kp SQT
+ &kp LSHFT &kp Z &kp X &kp C &kp V &kp B &kp N &kp M &kp COMMA &kp DOT &kp FSLH &kp ESC
+ &kp LGUI &mo 1 &kp SPACE &kp RET &mo 2 &kp RALT
+ >;
+ };
+ lower_layer {
+ label = "NUMBER";
+// -----------------------------------------------------------------------------------------
+// | TAB | 1 | 2 | 3 | 4 | 5 | | 6 | 7 | 8 | 9 | 0 | BKSP |
+// | | BT1 | BT2 | BT3 | BT4 | BT5 | | LFT | DWN | UP | RGT | | |
+// | SHFT | F11 | F12 | | | | | | PAGE_DOWN | PAGE_UP | | | BTCLR |
+// | GUI | | SPC | | ENT | | ALT |
+ bindings = <
+ &kp TAB &kp N1 &kp N2 &kp N3 &kp N4 &kp N5 &kp N6 &kp N7 &kp N8 &kp N9 &kp N0 &kp BSPC
+ &trans &bt BT_SEL 0 &bt BT_SEL 1 &bt BT_SEL 2 &bt BT_SEL 3 &bt BT_SEL 4 &kp LEFT &kp DOWN &kp UP &kp RIGHT &trans &trans
+ &kp LSHFT &kp F11 &kp F12 &trans &trans &trans &trans &kp PAGE_DOWN &kp PAGE_UP &trans &trans &bt BT_CLR
+ &kp LGUI &trans &kp SPACE &kp RET &trans &kp RALT
+ >;
+ };
+
+ raise_layer {
+ label = "SYMBOL";
+// -----------------------------------------------------------------------------------------
+// | TAB | ! | @ | # | $ | % | | ^ | & | * | ( | ) | BKSP |
+// | CTRL | | | | | | | - | = | [ | ] | \ | ` |
+// | SHFT | pp | vUP | vDO | | | | _ | + | { | } | "|" | ~ |
+// | GUI | | SPC | | ENT | | ALT |
+ bindings = <
+ &kp TAB &kp EXCL &kp AT &kp HASH &kp DLLR &kp PRCNT &kp CARET &kp AMPS &kp KP_MULTIPLY &kp LPAR &kp RPAR &kp BSPC
+ &kp LCTRL &trans &trans &trans &trans &trans &kp MINUS &kp EQUAL &kp LBKT &kp RBKT &kp BSLH &kp GRAVE
+ &kp LSHFT &kp C_PLAY_PAUSE &kp K_VOLUME_UP &kp K_VOLUME_DOWN &trans &trans &kp UNDER &kp PLUS &kp LBRC &kp RBRC &kp PIPE &kp TILDE
+ &kp LGUI &trans &kp SPACE &kp RET &trans &kp RALT
+ >;
+ };
+ };
+};
diff --git a/data/filters/csv.js b/data/filters/csv.js
new file mode 100644
index 0000000..f85c2de
--- /dev/null
+++ b/data/filters/csv.js
@@ -0,0 +1,31 @@
+function filterCSV( contentFilename ){
+
+ if ( contentFilename.endsWith(".csv") ) {
+ console.log('it is a CSV file', contentFilename)
+
+ // mereology option
+ let openingOptions = filesWithMetadata[contentFilename].openingOptions
+ // can be used via e.g. showFile("https://fabien.benetou.fr/?action=source",{ mereology:"whole"})
+
+ fetch( contentFilename )
+ .then( r => r.text() ).then( r => {
+ // console.log('CSV', r)
+
+ console.log( "mereology", openingOptions.mereology )
+ switch( openingOptions.mereology ) {
+ case "whole":
+ addNewNote(r, "0 1.4 -.8")
+ break;
+ default:
+ r.split('\n').map( (c,i) => addNewNote(c, "0 "+(1+i/10)+" -.8") )
+ }
+
+ AFRAME.scenes[0].emit('csvloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("csvloaded", e => console.log(e))
+ })
+ }
+ applyNextFilter( contentFilename )
+}
+
+sequentialFilters.push( filterCSV )
diff --git a/data/filters/docx_packed.xml.js b/data/filters/docx_packed.xml.js
new file mode 100644
index 0000000..4a4e7ef
--- /dev/null
+++ b/data/filters/docx_packed.xml.js
@@ -0,0 +1,30 @@
+function filterDocx( contentFilename ){
+ let file = filesWithMetadata[contentFilename]
+ if (!file) return
+
+ let contentType = file.contentType
+
+ if ( contentType.includes("xml") && contentFilename.includes("_content.docx/") && contentFilename.endsWith(".docx")) {
+ console.log('it is a docx packed file', contentFilename)
+ fetch( contentFilename )
+ .then( r => r.blob(r) )
+ .then( file => zip.loadAsync(file) )
+ .then( f => {
+ // console.log(f.files)
+
+ f.files["word/document.xml"].async("string")
+ .then( str => new window.DOMParser().parseFromString(str, "text/xml"))
+ .then(data => {
+ // console.log('docx via packed', data)
+
+ el = addNewNote( data.childNodes[0].textContent, "-.5 1.2 -0.5" )
+ AFRAME.scenes[0].emit('docxxmlloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("visualmetajsonloaded", e => console.log(e))
+ })
+ });
+ }
+ applyNextFilter( contentFilename )
+}
+
+sequentialFilters.push( filterDocx )
diff --git a/data/filters/docx_unpacked.xml.js b/data/filters/docx_unpacked.xml.js
new file mode 100644
index 0000000..cebb60c
--- /dev/null
+++ b/data/filters/docx_unpacked.xml.js
@@ -0,0 +1,24 @@
+function filterDocxJSON( contentFilename ){
+ let file = filesWithMetadata[contentFilename]
+ if (!file) return
+
+ let contentType = file.contentType
+
+ if ( contentType.includes("xml") && contentFilename.includes("_content.docx/") && contentFilename.endsWith(".xml")) {
+ console.log('it is a docx file', contentFilename)
+ fetch( contentFilename )
+ .then(response => response.text())
+ .then(str => new window.DOMParser().parseFromString(str, "text/xml"))
+ .then(data => {
+ // console.log(data)
+
+ el = addNewNote( data.childNodes[0].textContent, "0 1.2 -0.7" )
+ AFRAME.scenes[0].emit('docxxmlloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("visualmetajsonloaded", e => console.log(e))
+ });
+ }
+ applyNextFilter( contentFilename )
+}
+
+sequentialFilters.push( filterDocxJSON )
diff --git a/data/filters/content_filter_examples.js b/data/filters/image_and_glb_gltf.js
similarity index 100%
rename from data/filters/content_filter_examples.js
rename to data/filters/image_and_glb_gltf.js
diff --git a/data/filters/json_ref_manual.js b/data/filters/json_ref_manual.js
index fd3dd62..d225cd4 100644
--- a/data/filters/json_ref_manual.js
+++ b/data/filters/json_ref_manual.js
@@ -4,7 +4,7 @@ function filterJSONRef( contentFilename ){
let contentType = file.contentType
- if ( contentType.includes("json") && contentFilename.endsWith(".json")) {
+ if ( contentType.includes("json") && contentFilename.startsWith("references_manual_") && contentFilename.endsWith(".json")) {
console.log('it is a manual reference JSON file', contentFilename)
fetch( contentFilename ).then( r => r.json() ).then( json => {
let ref = json["data-objects"]
@@ -29,28 +29,54 @@ function filterJSONRef( contentFilename ){
fullEl.setAttribute("rotation", "45 0 0")
fullEl.setAttribute("scale", ".01 .01 .01")
fullEl.classList.add("reference-entry-card")
+ fullEl.data = r
let backgroundEl = document.createElement("a-box")
backgroundEl.setAttribute("scale", "10 5 .1")
backgroundEl.setAttribute("position", "4.5 0 -.1")
fullEl.appendChild( backgroundEl )
+ if (r["annote"]) {
+ let annoteEl = addNewNoteAsPostItNote( r["annote"], "-.3 "+(1+i/20)+" -.5", ".02 .02 .02" )
+ annoteEl.classList.add("reference-entry-annotate")
+ }
+ if (r["note"]) {
+ let annoteEl = addNewNoteAsPostItNote( r["note"], "-.2 "+(1+i/20)+" -.5", ".02 .02 .02" )
+ annoteEl.classList.add("reference-entry-note")
+ }
// if ACM and OA might be available via https://dl.acm.org/doi/pdf/DOI
// could then try to pass as PDF reader argument
// cf pageAsTextViaXML() and related in index.html
// note that it'd still need to fetch then upload via WebDAV
let pdf = r["bibtex-data"]["source-pdf"]
- let acmoa = r["bibtex-data"]["free-acm-access"]
- if (pdf && acmoa && pdf.startsWith("https://dl.acm.org")) {
+ let acmoa = r["bibtex-data"]["free-acm-access"] // warning, this is a string, not a boolean
+ // fallback due to file structure change between v4 and v11
+ if (!pdf) pdf = r["source-pdf"]
+ if (!acmoa) acmoa = r["free-acm-access"] // warning, this is a string, not a boolean
+
+ if (pdf && acmoa == "true" && pdf.startsWith("https://dl.acm.org")) {
+
// could then try to fetch content then upload via WebDAV
// should skip if already available
let pdfEl = document.createElement("a-box")
//pdfEl.setAttribute("scale", ".1 .1 .1")
- pdfEl.setAttribute("position", "-.9 0 0")
+ pdfEl.setAttribute("position", "-1.2 0 0") // trying to move away in order to be executable
fullEl.appendChild( pdfEl )
- let truncated_filename = "3209542.3209570" // hardcoded example
- // should instead try to fetch .xml on saved/pdfxml/ and if 200 then change color
+ let truncated_filename = pdf.replace(/.*pdf\//,'').replace(/.*\//,'')
+ // here by convention it would be in "/saved/pdfxml/"
+ let url = "/saved/pdfxml/"+truncated_filename+".xml"
+ fetch(url).then( response => {
+ if (response.ok) {
+ pdfEl.setAttribute( "color", "purple" )
+ // could have a showFile() on pinched here
+ let openCommandEl = addNewNote( "jxr showFile('"+url+"')", "-.6 "+(1+i/20)+" -.5", '.05 .05 .05' )
+ openCommandEl.classList.add("reference-entry-showfile")
+ }
+ })
+
+ /*
if (pdf.endsWith( truncated_filename )) {
pdfEl.setAttribute("color", "green" )
+ pdfEl.setAttribute("color", "purple" )
//pdfEl.setAttribute("value", "jxr console.log('"+truncated_filename+"')" )
// what should become the target then? the cube?
// problematic because it becomes movable
@@ -58,14 +84,13 @@ function filterJSONRef( contentFilename ){
// then should add JXR open of target PDF
- /*
window.pageastextviaxmlsrc = "https://companion.benetou.fr/saved/pdfxml/"+truncated_filename+".xml"
pageAsTextViaXML()
highlightcommands.setAttribute("visible", true)
roundedpageborders.setAttribute("visible", true)
- */
-
}
+ */
+
}
})
})
diff --git a/data/filters/keymap.js b/data/filters/keymap.js
new file mode 100644
index 0000000..86784fd
--- /dev/null
+++ b/data/filters/keymap.js
@@ -0,0 +1,32 @@
+function filterKeymap( contentFilename ){
+ // if (!file) return
+ // special filter, done via URL, no local file (for now at least)
+
+ if ( contentFilename.endsWith(".keymap") ) { // not very reliable
+ console.log('it is a .keymap file', contentFilename)
+
+ fetch( contentFilename )
+ .then( r => r.text() ).then( r => {
+ // console.log('Keymap', r)
+ let parsedMap = r.split("_layer").filter( l => l.includes('-----') ).map( l => l.split('\n').filter( l => (l.startsWith('//') && !l.includes('------'))).map(l => l.replace("// ",'').split('| ') ) )
+ // WARNING : this is NOT a normal keymap, rather this is about the comments (!) with a visual representation of a keymap
+ // see for example https://github.com/Utopiah/zmk-config-zen-2/blob/main/config/corneish_zen.keymap
+
+ r.split("_layer").filter( l => l.includes('-----') ).map( l => l.split('\n').filter( l => (l.startsWith('//') && !l.includes('------'))).map(l => l.replace("// ",'') ) ).map( (layer, l) => {
+ let el = addNewNote( layer.join('\n') )
+ el.id = 'keymap_layer'+l
+ el.classList.add( 'keymap_layer' )
+ el.highlightLetter = ( letter ) => { keymap_layer0.setAttribute("value", keymap_layer0.getAttribute("value").replace(' '+letter+' ','_'+letter+'_') ) }
+ el.unhighlightLetter = ( letter ) => { keymap_layer0.setAttribute("value", keymap_layer0.getAttribute("value").replace('_'+letter+'_',' '+letter+' ') ) }
+ // then available e.g. keymap_layer0.unhighlightLetter("J")
+ })
+
+ AFRAME.scenes[0].emit('keymaploaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("keymaploaded", e => console.log(e))
+ })
+ }
+ applyNextFilter( contentFilename )
+}
+
+sequentialFilters.push( filterKeymap )
diff --git a/data/filters/layout.json.js b/data/filters/layout.json.js
new file mode 100644
index 0000000..56c4a0c
--- /dev/null
+++ b/data/filters/layout.json.js
@@ -0,0 +1,36 @@
+// see e.g. https://git.benetou.fr/utopiah/text-code-xr-engine/src/branch/fot-sloan-companion/public/index.html#L806
+ // overall newContent() from that branch of Q3/Q4 2024
+function filterLayoutJSON( contentFilename ){
+ let file = filesWithMetadata[contentFilename]
+ if (!file) return
+
+ let contentType = file.contentType
+
+ if ( contentType.includes("json") && contentFilename.endsWith(".layout.json") ) {
+ console.warn('not fully implemented yet, only display file name, not their content with the right pose')
+ console.log('it is a JSON layout file', contentFilename)
+ fetch( contentFilename ).then( r => r.json() ).then( json => {
+ console.log( json )
+ // could here rely on showFile for each element but maybe some are NOT files
+ // different format than previous save
+ // must insure that moved also to the right position/rotation after loading
+ json.map( i => {
+ showFile( i.filename )
+ // AFRAME.scenes[0].emit('layoutjsonloaded', contentFilename)
+ // would have to wait for any file to be loaded...
+ // el.setAttribute("position", AFRAME.utils.coordinates.stringify( i.position ) )
+ // el.setAttribute("rotation", AFRAME.utils.coordinates.stringify( i.rotation ) )
+ // el.id = i.id
+
+ })
+
+ AFRAME.scenes[0].emit('layoutjsonloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("layoutjsonloaded", e => console.log(e))
+ })
+ }
+
+ applyNextFilter( contentFilename )
+}
+
+sequentialFilters.push( filterLayoutJSON )
diff --git a/data/filters/mapvisualmeta.json.js b/data/filters/mapvisualmeta.json.js
new file mode 100644
index 0000000..83deb74
--- /dev/null
+++ b/data/filters/mapvisualmeta.json.js
@@ -0,0 +1,63 @@
+function filterMapVisualMetaJSON( contentFilename ){
+ let file = filesWithMetadata[contentFilename]
+ if (!file) return
+
+ let contentType = file.contentType
+
+ let sidecarFile = contentFilename.replace("dynamicview", "glossary")
+ const scale = 1/1000
+ const xOffset = 1
+ const yOffset = 1
+
+ const idprefix = 'visualmetaexport_'
+
+ if ( contentType.includes("json") && contentFilename.endsWith("_dynamicviewvisualmetaexport.json")) {
+ console.log('it is a map visualmeta export file', contentFilename)
+ fetch( contentFilename ).then( r => r.json() ).then( json => {
+ fetch( sidecarFile ).then( r => r.json() ).then( sidecarjson => {
+ let glossary = Object.entries( sidecarjson.entries ).map( i => i[1].phrase )
+
+ // console.log( json, sidecarjson )
+
+ json.nodes.map( n => {
+ let x = json.layout.nodePositions[ n.identifier ].x * scale + xOffset
+ let y = -json.layout.nodePositions[ n.identifier ].y * scale + yOffset
+ let z = -1/2
+ let el = addNewNote( n.title, "" + x + " " + y + " " + z )
+ el.id = "visualmetaexport_"+n.title.toLowerCase().replaceAll(" ","_").replaceAll(".","_").replaceAll("'","_")
+ el.setAttribute("outline-width", 0)
+ // optional background color "#7c7c7c"
+ // could use onpicked/onreleased to show links from glossary
+ // check if any word from title is in glossary
+ // if so, display line or highlight visually by removing outline
+ // reset onreleased
+ })
+
+ Object.entries( sidecarjson.entries ).map( i => i[1].phrase ).map( n => {
+ Object.entries( sidecarjson.entries )
+ .map( i => { return {phrase:i[1].phrase, entry:i[1].entry} } )
+ .filter( i => i.entry.toLowerCase().includes(n.toLowerCase()) )
+ .map( i => {
+ let el = document.createElement("a-entity")
+ el.setAttribute('live-selector-line',
+ 'start: #'+(idprefix+n.toLowerCase().replaceAll(" ","_").replaceAll(".","_").replaceAll("'","_"))
+ +'; end: #'+(idprefix+i.phrase.toLowerCase().replaceAll(" ","_").replaceAll(".","_").replaceAll("'","_"))
+ +';' )
+ el.classList.add('visualmeta_export_link')
+ AFRAME.scenes[0].appendChild(el)
+ })
+
+ })
+ Array.from( document.querySelectorAll("[line]") ).map( el => el.setAttribute("line", "color", "black"))
+ // should filter a bit better otherwise take lines from other components and elements e.g. raycaster
+
+ AFRAME.scenes[0].emit('mapvisualmetajsonloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("mapvisualmetajsonloaded", e => console.log(e))
+ })
+ })
+ }
+ applyNextFilter( contentFilename )
+}
+
+sequentialFilters.push( filterMapVisualMetaJSON )
diff --git a/data/filters/mapvisualmeta.jsons.zip.js b/data/filters/mapvisualmeta.jsons.zip.js
new file mode 100644
index 0000000..e055f2d
--- /dev/null
+++ b/data/filters/mapvisualmeta.jsons.zip.js
@@ -0,0 +1,82 @@
+function filterPackedMapVisualMetaJSON( contentFilename ){
+ let file = filesWithMetadata[contentFilename]
+ if (!file) return
+
+ let contentType = file.contentType
+
+ let openingOptions = filesWithMetadata[contentFilename].openingOptions
+
+ // could overwrite via URL
+ const scale = 1/1000
+ const xOffset = 0
+ const yOffset = 1
+
+ const idprefix = 'visualmetaexport_'
+
+ if ( contentType.includes("zip") && contentFilename.includes("dynamicviewvisualmetaexport.jsons") ) {
+ // naming issue due to WordPress
+ console.log('it is a packed map visualmeta export file', contentFilename)
+ fetch( contentFilename )
+ // see for packed filters/docx_packed.xml.js
+ .then( r => r.blob(r) )
+ .then( file => zip.loadAsync(file) )
+ .then( f => {
+ // console.log(f.files)
+ // check for optional export directory
+ let filenameDynamicView = Object.entries( f.files ).filter( i => i[0].includes("_dynamicviewvisualmetaexport.json") ).map( i => i[0] )[0]
+ let filenameGlossary = Object.entries( f.files ).filter( i => i[0].includes("_glossaryvisualmetaexport.json") ).map( i => i[0] )[0]
+ f.files[filenameDynamicView].async("string").then( dyn_content => {
+ json = JSON.parse( dyn_content )
+ f.files[filenameGlossary].async("string").then( gloss_content => {
+ let sidecarjson = JSON.parse( gloss_content )
+ let glossary = Object.entries( sidecarjson.entries ).map( i => i[1].phrase )
+
+ console.log( json, sidecarjson )
+ //console.log( glossary )
+
+ json.nodes.map( n => {
+ let x = json.layout.nodePositions[ n.identifier ].x * scale + xOffset
+ let y = -json.layout.nodePositions[ n.identifier ].y * scale + yOffset
+ let z = -1/2
+ let el = addNewNote( n.title, "" + x + " " + y + " " + z )
+ el.id = "visualmetaexport_"+n.title.toLowerCase().replaceAll(" ","_").replaceAll(".","_").replaceAll("'","_")
+ el.setAttribute("outline-width", 0)
+ // optional background color "#7c7c7c"
+ // could use onpicked/onreleased to show links from glossary
+ // check if any word from title is in glossary
+ // if so, display line or highlight visually by removing outline
+ // reset onreleased
+
+ let definitionFound = Object.entries( sidecarjson.entries ).filter( i => i[1].phrase == n.title ).map( f => f[1].entry )
+ if (openingOptions.showDefinitions && definitionFound ){ el.setAttribute("annotation", "content", definitionFound[0] ) }
+ })
+
+ Object.entries( sidecarjson.entries ).map( i => i[1].phrase ).map( n => {
+ Object.entries( sidecarjson.entries )
+ .map( i => { return {phrase:i[1].phrase, entry:i[1].entry} } )
+ .filter( i => i.entry.toLowerCase().includes(n.toLowerCase()) )
+ .map( i => {
+ let el = document.createElement("a-entity")
+ el.setAttribute('live-selector-line',
+ 'start: #'+(idprefix+n.toLowerCase().replaceAll(' ','_').replaceAll(".","_").replaceAll("'","_"))
+ +'; end: #'+(idprefix+i.phrase.toLowerCase().replaceAll(' ','_').replaceAll(".","_").replaceAll("'","_"))
+ +';' )
+ el.classList.add('visualmeta_export_link')
+ AFRAME.scenes[0].appendChild(el)
+ })
+
+ })
+ Array.from( document.querySelectorAll("[line]") ).map( el => el.setAttribute("line", "color", "black"))
+ // should filter a bit better otherwise take lines from other components and elements e.g. raycaster
+
+ AFRAME.scenes[0].emit('mapvisualmetajsonloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("mapvisualmetajsonloaded", e => console.log(e))
+ })
+ } )
+ })
+ }
+ applyNextFilter( contentFilename )
+}
+
+sequentialFilters.push( filterPackedMapVisualMetaJSON )
diff --git a/data/filters/markdown.js b/data/filters/markdown.js
new file mode 100644
index 0000000..b44787c
--- /dev/null
+++ b/data/filters/markdown.js
@@ -0,0 +1,42 @@
+function filterMarkdown( contentFilename ){
+ // if (!file) return
+ // special filter, done via URL, no local file (for now at least)
+
+ if ( contentFilename.endsWith(".md") ) {
+ console.log('it is a markdown file', contentFilename)
+
+ // mereology option
+ let openingOptions = filesWithMetadata[contentFilename].openingOptions
+ // can be used via e.g. showFile("https://fabien.benetou.fr/?action=source",{ mereology:"whole"})
+
+ fetch( contentFilename )
+ .then( r => r.text() ).then( r => {
+ console.log('markdown', r)
+
+ console.log( "mereology", openingOptions.mereology )
+ switch( openingOptions.mereology ) {
+ case "whole":
+ addNewNote(r, "0 1.4 -.8")
+ break;
+ case "section":
+ r.split(/! /).map( (c,i) => addNewNote(c, "0 "+(1+i/10)+" -.8") ) // untested
+ break;
+ case "line":
+ r.split('\n').reverse().map( (c,i) => addNewNote(c, "0 "+(1+i/10)+" -.8") )
+ break;
+ case "listonly":
+ r.replaceAll('* ','').split('\n').map( (c,i) => addNewNote(c, "0 "+(1+i/10)+" -.8") )
+ // based on pmwiki, should also work with + or -
+ default:
+ r.split('\n').reverse().map( (c,i) => addNewNote(c, "0 "+(1+i/10)+" -.8") )
+ }
+
+ AFRAME.scenes[0].emit('markdownloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("markdownloaded", e => console.log(e))
+ })
+ }
+ applyNextFilter( contentFilename )
+}
+
+sequentialFilters.push( filterMarkdown )
diff --git a/data/filters/odt_unpacked.xml.js b/data/filters/odt_unpacked.xml.js
new file mode 100644
index 0000000..b58c9ea
--- /dev/null
+++ b/data/filters/odt_unpacked.xml.js
@@ -0,0 +1,84 @@
+function filterOdtJSON( contentFilename ){
+ let file = filesWithMetadata[contentFilename]
+ if (!file) return
+
+ let contentType = file.contentType
+
+ if ( contentType.includes("xml") && contentFilename.includes("_content.odt/") && contentFilename.endsWith(".xml")) {
+ console.log('it is a visualmeta export file', contentFilename)
+ fetch( contentFilename )
+ .then(response => response.text())
+ .then(str => new window.DOMParser().parseFromString(str, "text/xml"))
+ .then(data => {
+ console.log(data)
+
+ el = addNewNote( data.childNodes[0].textContent )
+ });
+ /*
+ fetch( contentFilename ).then( r => r.json() ).then( json => {
+ // not JSON!
+ console.log( json )
+ let thumbnailPath = json.thumbnail.replace("file:/","/tapestry/")
+ let el = document.createElement("a-image")
+ el.setAttribute("position", "0 1.5 0.7")
+ el.setAttribute("rotation", "0 180 0")
+ el.setAttribute("src", thumbnailPath)
+ el.setAttribute("width", json.startView.size.width/10000)
+ el.setAttribute("height", json.startView.size.height/10000)
+ AFRAME.scenes[0].appendChild(el)
+
+ let positioned = {}
+ json.items.map( i => {
+ let x = 1-i.position.x/1000
+ let y = 2-i.position.y/1000
+
+ let el = null
+ if (i.type == "text") {
+ el = addNewNoteAsPostItNote( i.text, x + " " + y + " 1", ".1 .1 .1", "tapestry_"+i.id, "tapestry_text", true, "0 180 0")
+ // addNewNoteAsPostItNote( text, position=`-0.2 1.1 -0.1`, scale= "0.1 0.1 0.1", id=null, classes="notes", visible="true", rotation="0 0 0" ){
+ }
+
+ if (i.thumbnail) {
+ let thumbnailPath = i.thumbnail.source.replace("file:/","/tapestry/")
+ el = document.createElement("a-image")
+ el.setAttribute("position", x + " " + y + " 1")
+ el.setAttribute("rotation", "0 180 0")
+ el.setAttribute("src", thumbnailPath)
+ el.setAttribute("width", i.size.width/1000)
+ el.setAttribute("height", i.size.height/1000)
+ AFRAME.scenes[0].appendChild(el)
+ }
+ positioned[i.id] = el
+ if ( el ) el.setAttribute("target", "")
+ })
+
+ setTimeout( _ => {
+ // must be done after the items are positioned
+ json.rels.map( i => {
+ let el = document.createElement("a-tube")
+ console.log( i.from.itemId, i.to.itemId )
+ let start = positioned[i.from.itemId].getAttribute("position")
+ let end = positioned[i.to.itemId].getAttribute("position")
+ let mid = new THREE.Vector3()
+ mid.copy ( start )
+ mid.add ( end )
+ mid.divideScalar(2)
+ mid.z += 0.3
+ let path = [start, mid, end].map( p => AFRAME.utils.coordinates.stringify( p ) ).join(", ")
+
+ el.setAttribute("radius", 0.01)
+ el.setAttribute("path", path)
+ AFRAME.scenes[0].appendChild(el)
+ })
+ }, 1000 )
+
+ AFRAME.scenes[0].emit('visualmetajsonloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("visualmetajsonloaded", e => console.log(e))
+ })
+ */
+ }
+ applyNextFilter( contentFilename )
+}
+
+sequentialFilters.push( filterOdtJSON )
diff --git a/data/filters/pdf_unpacked.xml.js b/data/filters/pdf_unpacked.xml.js
new file mode 100644
index 0000000..90091c7
--- /dev/null
+++ b/data/filters/pdf_unpacked.xml.js
@@ -0,0 +1,121 @@
+function filterPDFUnpackedXml( contentFilename ){
+ const path = "/saved/pdfxml/"
+ let file = filesWithMetadata[contentFilename]
+ // might have to hijack again
+ if (!file) return
+
+ let contentType = file.contentType
+
+ if ( contentType.includes("xml") && contentFilename.includes("pdfxml/") ) {
+ console.log('it is a pdf unpacked file', contentFilename)
+ pageAsTextViaXML(contentFilename, page=0)
+ }
+
+ applyNextFilter( contentFilename )
+
+ function pageAsTextViaXML(src, page=0){
+ fetch( src ).then( r => r.text() ).then( txt => {
+ const rootEl = document.createElement("a-box")
+ file.filteredEl = rootEl
+ rootEl.nextPage = (el) => {
+ // not tested in depth
+ Array.from( rootEl.querySelectorAll(".highlightabletext") ).map( el => el.remove() )
+ Array.from( doc.children[0].children[page++].querySelectorAll("text") ).map( (l,n) => {
+ let tktxt = document.createElement("a-troika-text")
+ let pos = ""+(l.attributes.left.value*scalingFactor+xOffset) + " " + (1-l.attributes.top.value*scalingFactor+yOffset) + " "+zPos
+ let scale = "0.045 0.045 0.045"
+ tktxt.setAttribute("position", pos)
+ tktxt.setAttribute("font-size", "0.009")
+ tktxt.setAttribute("color", "black")
+ tktxt.setAttribute("value", l.textContent)
+ tktxt.setAttribute("anchor", "left")
+ tktxt.classList.add("highlightabletext")
+ rootEl.appendChild(tktxt)
+ })
+ }
+ rootEl.previousPage = _ => console.log( 'test' )
+ const sheetEl = document.createElement("a-box")
+ sheetEl.setAttribute("width", "1")
+ sheetEl.setAttribute("height", "1.2")
+ sheetEl.setAttribute("depth", ".01")
+ sheetEl.setAttribute("position", "0.5 -.6 0")
+ rootEl.appendChild(sheetEl)
+ rootEl.setAttribute("width", ".01")
+ rootEl.setAttribute("height", ".01")
+ rootEl.setAttribute("depth", ".01")
+ rootEl.setAttribute("color", "brown")
+ rootEl.setAttribute("target", "") // "hidden" by the highlightable texts as targets
+ // can temporarily remove target from .highlightabletext elements
+ // alternatively could have a handle, a la lens demo
+ let disabledHighlight = true // for testing manipulation
+ disabledHighlight=false
+ // highlight ball still hidden due to thickness
+ rootEl.setAttribute("position", "-0.5 1.7 -.6")
+ rootEl.id = "page_from_"+src.replaceAll("https://","").replaceAll("/","_")
+ rootEl.classList.add ( "page_from_pdf" )
+ AFRAME.scenes[0].appendChild(rootEl)
+
+ const parser = new DOMParser();
+ let doc = parser.parseFromString(txt, "application/xml")
+ const scalingFactor = 1/1000 // used for position of text and images
+ // could also use x/y/z offsets
+ // probably easier to append to an entity, either empty or used as (white) background
+ const xOffset = 0.1 // changed by rootEl position
+ const yOffset = -1.0
+ const zPos = 0.01
+ //Array.from( doc.children[0].children[page].querySelectorAll("text") ).map( (l,n) => addNewNote(l.textContent, ""+(l.attributes.left.value*scalingFactor+xOffset) + " " + (1-l.attributes.top.value*scalingFactor+yOffset) + " "+zPos, "0.045 0.045 0.045", "highlighttextfromxml_"+n, "highlighttextfromxmlitem" ) )
+ Array.from( doc.children[0].children[page].querySelectorAll("text") ).map( (l,n) => {
+ // addNewNote(l.textContent, ""+(l.attributes.left.value*scalingFactor+xOffset) + " " + (1-l.attributes.top.value*scalingFactor+yOffset) + " "+zPos, "0.045 0.045 0.045", "highlighttextfromxml_"+n, "highlighttextfromxmlitem" ) )
+ let tktxt = document.createElement("a-troika-text")
+ let pos = ""+(l.attributes.left.value*scalingFactor+xOffset) + " " + (1-l.attributes.top.value*scalingFactor+yOffset) + " "+zPos
+ let scale = "0.045 0.045 0.045"
+ tktxt.setAttribute("position", pos)
+ tktxt.setAttribute("originalposition", pos)
+ tktxt.setAttribute("originalpage", page)
+ // FIXME
+ //tktxt.setAttribute("originalsource", fileContent.meta.metadata['dc:title'])
+ //tktxt.setAttribute("originalidentifier", fileContent.meta.metadata['dc:identifier'])
+ tktxt.setAttribute("font-size", "0.009")
+ tktxt.setAttribute("color", "black")
+ if (!disabledHighlight) tktxt.setAttribute("target", "")
+ tktxt.classList.add("highlightabletext")
+ tktxt.setAttribute("onpicked", "console.log(selectedElements.at(-1).element.getAttribute('value'))")
+ tktxt.setAttribute("onreleased", "let el = selectedElements.at(-1).element; if (true) el.setAttribute('color', highlightColor); el.setAttribute('rotation', ''); el.setAttribute('position', el.getAttribute('originalposition') )")
+ // resets back...
+ // change color
+ // only if above a certain threshold, e.g. held a long time, or released close to specific other item
+ // could also toggle coloring
+ // can be based on coloring pick with jxr
+ tktxt.setAttribute("value", l.textContent)
+ tktxt.setAttribute("anchor", "left")
+ rootEl.appendChild(tktxt)
+ })
+
+ Array.from( doc.children[0].children[page].querySelectorAll("image") ).map( (l,n) => {
+ let el = document.createElement("a-box")
+ // is position via center of element so should offset it
+ el.setAttribute("src", path+l.attributes.src.value); // somehow set to #transparent...
+ el.setAttribute("width", l.attributes.width.value*scalingFactor);
+ el.setAttribute("height", l.attributes.height.value*scalingFactor);
+ el.setAttribute("depth", .01);
+ el.setAttribute("target", "")
+ el.id = "highlightimagefromxml_"+n
+ el.classList.add("highlightimagefromxmlitem")
+ el.classList.add("highlightabletext")
+ let w = l.attributes.width.value*scalingFactor
+ let h = l.attributes.height.value*scalingFactor
+ el.setAttribute("position", ""+ ""+(w/2+l.attributes.left.value*scalingFactor+xOffset)+" "+ (-h/2+1-l.attributes.top.value*scalingFactor+yOffset)+ " "+zPos)
+ rootEl.appendChild(el)
+ })
+
+ AFRAME.scenes[0].emit('pdfxmlloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("visualmetajsonloaded", e => console.log(e))
+
+ } );
+
+ }
+
+}
+
+sequentialFilters.push( filterPDFUnpackedXml )
diff --git a/data/filters/peertubeapi.js b/data/filters/peertubeapi.js
new file mode 100644
index 0000000..5932cab
--- /dev/null
+++ b/data/filters/peertubeapi.js
@@ -0,0 +1,49 @@
+function videoAPIPeerTube( contentFilename ){
+ let file = filesWithMetadata[contentFilename]
+ //if (!file) return
+ // can be removed for URLs as those are not with metadata
+
+ let contentType = file.contentType
+
+ // mereology option
+ let openingOptions = filesWithMetadata[contentFilename].openingOptions
+ // can be used via e.g. showFile("https://fabien.benetou.fr/?action=source",{ mereology:"whole"})
+
+ // filtering, only applying what's next to a certain content type and/or with filename filtering with .startsWith() .endsWith() .includes() or regex
+ if ( contentFilename.includes("/api/v1/search/videos") ) { // peertube API, not specific enough
+ // could verify instead via header i.e x-powered-by: PeerTube
+ console.log('it is a PeerTube API call', contentFilename)
+
+ fetch( contentFilename ).then( r => r.json() ).then( json => {
+ const rootEl = document.createElement("a-entity")
+ rootEl.jsonfromapi = json // rarely so big that it makes any difference to have it twice, on here and on individual items
+ // having a root element to attach on makes manipulation later on easier and safer, bringing a context for later modifications
+ file.filteredEl = rootEl
+ rootEl.test = _ => console.log( 'test' )
+ // function that can then be called on the created element later on
+ // e.g. filesWithMetadata["https://companion.benetou.fr/saved/pdfxml/3209542.3209570.xml"].filteredEl.nextPage('ok')
+ AFRAME.scenes[0].appendChild(rootEl)
+ json.data.map( (d,i) => {
+ let el = document.createElement("a-image")
+ rootEl.appendChild(el)
+ el.setAttribute("position", ""+(-1+Math.random())+" "+(Math.random()+1)+" -0.5" )
+ el.setAttribute("scale", ".2 .1 .1")
+ el.setAttribute("src", contentFilename.replace(/\/api.*/,'') + d.thumbnailPath)
+ el.setAttribute("target", "")
+ el.jsonfromapi = d
+ el.id = "video_"+d.name
+ })
+
+
+ AFRAME.scenes[0].emit('videoapipeertube_loaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("videoapipeertube_loaded", e => console.log(e))
+
+ } );
+ }
+ applyNextFilter( contentFilename )
+ // can stop here or move on to the next filter that might or not be applied
+}
+sequentialFilters.push( videoAPIPeerTube )
+// adding this to the list of filters to go through, order matters
+ // typically one would be generic filters first then more specific ones after
diff --git a/data/filters/pmwiki.js b/data/filters/pmwiki.js
new file mode 100644
index 0000000..9b7fa58
--- /dev/null
+++ b/data/filters/pmwiki.js
@@ -0,0 +1,50 @@
+function filterPmWiki( contentFilename ){
+ // if (!file) return
+ // special filter, done via URL, no local file (for now at least)
+
+ if ( contentFilename.endsWith("?action=source") ) { // not very reliable
+ console.log('it is a pmwiki file', contentFilename)
+
+ // mereology option
+ let openingOptions = filesWithMetadata[contentFilename].openingOptions
+ // can be used via e.g. showFile("https://fabien.benetou.fr/?action=source",{ mereology:"whole"})
+
+ fetch( contentFilename )
+ .then( r => r.text() ).then( r => {
+ // console.log('pmwiki', r)
+ // TODO
+ // diff support to replay over time, e.g. https://fabien.benetou.fr/PIMVRdata/ItemsStates?action=diff
+ // index as a special case https://fabien.benetou.fr/Site/AllRecentChanges?action=source
+ // rendered previews e.g https://vatelier.benetou.fr/MyDemo/newtooling/textures/fabien.benetou.fr_PIMVRdata_ItemsStates.png
+ // server side graph generation e.g. https://vatelier.benetou.fr/MyDemo/newtooling/wiki_graph.json
+ // very specific
+
+ let elements = []
+ console.log( "mereology", openingOptions.mereology )
+ switch( openingOptions.mereology ) {
+ case "whole":
+ elements.push( addNewNote(r, "0 1.4 -.8") )
+ break;
+ case "section":
+ r.split(/! /).map( (c,i) => elements.push( addNewNote(c, "0 "+(1+i/10)+" -.8") ) ) // untested
+ break;
+ case "line":
+ r.split('\n').map( (c,i) => elements.push( addNewNote(c, "0 "+(1+i/10)+" -.8") ) )
+ break;
+ case "listonly":
+ default:
+ r.replaceAll('* ','').split('\n').map( (c,i) => elements.push( addNewNote(c, "0 "+(1+i/10)+" -.8") ) )
+ // this is specific to https://fabien.benetou.fr/PersonalInformationStream/WithoutNotesMay2025 and similar
+ }
+ elements.map( el => { el.classList.add('pmwikifilter'); el.classList.add('filterimport') } )
+ // for something more generic see https://www.pmwiki.org/wiki/PmWiki/PageFileFormat
+
+ AFRAME.scenes[0].emit('pmwikiloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("pmwikiloaded", e => console.log(e))
+ })
+ }
+ applyNextFilter( contentFilename )
+}
+
+sequentialFilters.push( filterPmWiki )
diff --git a/data/filters/q2layout.json.js b/data/filters/q2layout.json.js
new file mode 100644
index 0000000..6bd6700
--- /dev/null
+++ b/data/filters/q2layout.json.js
@@ -0,0 +1,40 @@
+function filterQ2LayoutJSON( contentFilename ){
+ let file = filesWithMetadata[contentFilename]
+ if (!file) return
+ // can be removed for URLs as those are not with metadata
+
+ let contentType = file.contentType
+
+ // mereology option
+ let openingOptions = filesWithMetadata[contentFilename].openingOptions
+ // can be used via e.g. showFile("https://fabien.benetou.fr/?action=source",{ mereology:"whole"})
+
+ // filtering, only applying what's next to a certain content type and/or with filename filtering with .startsWith() .endsWith() .includes() or regex
+ if ( contentType.includes("json") && contentFilename.endsWith("_q2layout.json") ) {
+ console.log('it is a Q2 Layout JSON file', contentFilename)
+
+ fetch( contentFilename ).then( r => r.json() ).then( savedDataFromPreviousSession => {
+ // console.log( savedDataFromPreviousSession )
+ savedDataFromPreviousSession.map( savedData =>
+ Array.from( document.querySelectorAll('.'+classNameItemsToSave) )
+ .filter( noteEl => noteEl.getAttribute("value") == savedData.value )
+ // search by value as in theory those are constant (but not necessarily unique, even though usually are)
+ .map( foundNoteEl => {
+ foundNoteEl.setAttribute("position", AFRAME.utils.coordinates.stringify(savedData.position) )
+ foundNoteEl.setAttribute("rotation", AFRAME.utils.coordinates.stringify(savedData.rotation) )
+ } )
+ // should also debug the unfound ones
+ )
+
+ AFRAME.scenes[0].emit('q2layoutjsonloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("templateexampleloaded", e => console.log(e))
+
+ } );
+ }
+ applyNextFilter( contentFilename )
+ // can stop here or move on to the next filter that might or not be applied
+}
+sequentialFilters.push( filterQ2LayoutJSON )
+// adding this to the list of filters to go through, order matters
+ // typically one would be generic filters first then more specific ones after
diff --git a/data/filters/rete.bitbybit.json.js b/data/filters/rete.bitbybit.json.js
new file mode 100644
index 0000000..06b3364
--- /dev/null
+++ b/data/filters/rete.bitbybit.json.js
@@ -0,0 +1,112 @@
+function filterReteBiByBit( contentFilename ){
+ let file = filesWithMetadata[contentFilename]
+ if (!file) return
+ // can be removed for URLs as those are not with metadata
+
+ let contentType = file.contentType
+
+ // mereology option
+ let openingOptions = filesWithMetadata[contentFilename].openingOptions
+ // can be used via e.g. showFile("https://fabien.benetou.fr/?action=source",{ mereology:"whole"})
+
+ // filtering, only applying what's next to a certain content type and/or with filename filtering with .startsWith() .endsWith() .includes() or regex
+ if ( contentType.includes("") && (contentFilename.endsWith("-rete.bitbybit") || contentFilename.endsWith("rete-runner.bitbybit") )) {
+ console.log('it is a Rete BitByBit file', contentFilename)
+ // assuming non minified code... but maybe works the same way
+ // TODO try with minified code, would be preferred in order to do execution after
+
+ const filenameIdPrefix = "retebitbybit_id_"
+ if (contentFilename.endsWith("rete-runner.bitbybit") ) {
+ fetch( contentFilename ).then( r => r.text() ).then( txt => {
+ executeBitbybit(txt)
+ // can keep track of generate meshes via
+ // AFRAME.scenes[0].object3D.children.filter( o => o.name.includes("brepMesh") )
+ // filtering with before the call and after (even though async)
+ })
+ } else {
+ fetch( contentFilename ).then( r => r.json() ).then( txt => {
+ let nodes = Object.entries( JSON.parse(txt.script).nodes )
+ //console.log( nodes )
+
+ const rootEl = document.createElement("a-entity")
+ file.filteredEl = rootEl
+ rootEl.execute = _ => console.warn( 'Not implemented for now, see executeBitbybit()')
+ // TODO see executeBitbybit() , needs runner code too
+
+ // function that can then be called on the created element later on
+ // e.g. filesWithMetadata["https://companion.benetou.fr/saved/pdfxml/3209542.3209570.xml"].filteredEl.nextPage('ok')
+ nodes.map( n => {
+ let el = document.createElement("a-troika-text")
+ let boxEl = document.createElement("a-box")
+ boxEl.setAttribute("wireframe", "true" )
+ boxEl.setAttribute("width", .3 )
+ boxEl.setAttribute("height", .4 )
+ boxEl.setAttribute("depth", .01 )
+ boxEl.setAttribute("position", "0 -.18 0" )
+ el.setAttribute("value", n[1].customName )
+ el.setAttribute("font-size", ".05")
+ el.setAttribute("target", "")
+ let x = 1+n[1].position[0]/1000
+ let y = 2-n[1].position[1]/1000
+ let z = -1
+ el.setAttribute("position", ""+x+" "+y+" "+z)
+ rootEl.appendChild(el)
+ el.appendChild(boxEl)
+ el.id = filenameIdPrefix + n[1].id
+ })
+ nodes.map( p => {
+ let n = p[1]
+ let inputs = Object.entries( n.inputs ).map( o => o[1] )
+ if (inputs.length) {
+ // console.log( n.id, inputs )
+ inputs.map( c => {
+ let el = document.createElement("a-entity")
+ el.setAttribute('live-selector-line',
+ 'start: #'+(filenameIdPrefix+n.id)
+ +'; end: #'+(filenameIdPrefix+c.connections[0].node)
+ +';' )
+ el.classList.add('retebitbybit_export_link')
+ rootEl.appendChild(el)
+ })
+ }
+ })
+ AFRAME.scenes[0].appendChild(rootEl)
+
+ AFRAME.scenes[0].emit('retebitbybitloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("retebitbybitloaded", e => console.log(e))
+
+ } );
+ }
+ }
+ applyNextFilter( contentFilename )
+ // can stop here or move on to the next filter that might or not be applied
+}
+sequentialFilters.push( filterReteBiByBit )
+// adding this to the list of filters to go through, order matters
+ // typically one would be generic filters first then more specific ones after
+
+async function executeBitbybit(code){
+ window.THREEJS = window.THREE;
+ await import("https://cdn.jsdelivr.net/gh/bitbybit-dev/bitbybit-assets@0.20.4/runner/bitbybit-runner-lite-threejs.js")
+ const aframeScene = document.querySelector('a-scene').object3D;
+ const runnerOptions = {
+ canvasZoneClass: 'myCanvasZone',
+ enableOCCT: true,
+ enableJSCAD: false,
+ enableManifold: false,
+ loadFonts: ['Roboto'],
+ externalThreeJSSettings: {
+ scene: aframeScene,
+ camera: AFRAME.systems.camera,
+ }
+ };
+
+ const runner = window.bitbybitRunner.getRunnerInstance();
+ const { bitbybit, Bit, camera, scene, renderer } = await runner.run( runnerOptions);
+ window.bitbybit = bitbybit;
+ window.Bit = Bit;
+ window.runner = runner
+
+ runner.executeScript(code)
+}
diff --git a/data/filters/sqlite.js b/data/filters/sqlite.js
new file mode 100644
index 0000000..dc07945
--- /dev/null
+++ b/data/filters/sqlite.js
@@ -0,0 +1,61 @@
+// https://cdnjs.com/libraries/sql.js
+// https://github.com/sql-js/sql.js?tab=readme-ov-file#loading-a-database-from-a-server
+
+function filterTemplateExample( contentFilename ){
+ let file = filesWithMetadata[contentFilename]
+ if (!file) return
+ // can be removed for URLs as those are not with metadata
+
+ let contentType = file.contentType
+
+ // mereology option
+ let openingOptions = filesWithMetadata[contentFilename].openingOptions
+ // can be used via e.g. showFile("https://fabien.benetou.fr/?action=source",{ mereology:"whole"})
+
+ // filtering, only applying what's next to a certain content type and/or with filename filtering with .startsWith() .endsWith() .includes() or regex
+ if ( contentType.includes("xml") && contentFilename.includes("pdfxml/") ) {
+ console.log('it is a pdf unpacked file', contentFilename)
+
+ /*
+ // example of zip support
+ fetch( contentFilename ).then( r => r.blob(r) ).then( file => zip.loadAsync(file) ).then( f => {
+ console.log(f.files)
+ let filenameFromZip = Object.entries( f.files ).filter( i => i[0].includes("partialfilename.ext") ).map( i => i[0] )[0]
+ f.files[filenameFromZip].async("string").then( dyn_content => {
+ json = JSON.parse( dyn_content )
+ })
+ })
+ */
+
+ fetch( contentFilename ).then( r => r.text() ).then( txt => {
+
+ console.log( "mereology", openingOptions.mereology )
+ switch( openingOptions.mereology ) {
+ case "whole":
+ addNewNote(txt, "0 1.4 -.8")
+ break;
+ case "listonly":
+ default:
+ txt.replaceAll('* ','').split('\n').map( (c,i) => addNewNote(c, "0 "+(1+i/10)+" -.8") )
+ }
+
+ const rootEl = document.createElement("a-entity")
+ // having a root element to attach on makes manipulation later on easier and safer, bringing a context for later modifications
+ file.filteredEl = rootEl
+ rootEl.test = _ => console.log( 'test' )
+ // function that can then be called on the created element later on
+ // e.g. filesWithMetadata["https://companion.benetou.fr/saved/pdfxml/3209542.3209570.xml"].filteredEl.nextPage('ok')
+ AFRAME.scenes[0].appendChild(rootEl)
+
+ AFRAME.scenes[0].emit('templateexampleloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("templateexampleloaded", e => console.log(e))
+
+ } );
+ }
+ applyNextFilter( contentFilename )
+ // can stop here or move on to the next filter that might or not be applied
+}
+sequentialFilters.push( filterTemplateExample )
+// adding this to the list of filters to go through, order matters
+ // typically one would be generic filters first then more specific ones after
diff --git a/data/filters/srt_to_json.js b/data/filters/srt_to_json.js
index 4547d53..77d1cc6 100644
--- a/data/filters/srt_to_json.js
+++ b/data/filters/srt_to_json.js
@@ -8,11 +8,14 @@ function filterTextModifications( contentFilename ){
console.log('it is an modification scheme', contentFilename)
console.log('try to pass it to parametersViaURL(data)')
fetch( contentFilename ).then( r => r.text() ).then( txt => {
- console.log(txt.split(/$\n/).map(l=>{
+ let jsonFromSRT = txt.split(/$\n/).map(l=>{
let subItem = l.split('\n')
let timings = subItem[1].split(' --> ')
return { id:Number(subItem[0]), timingStart:timings[0], timingEnd:timings[1], text:subItem[2] }
- } ))
+ } )
+ console.log( jsonFromSRT )
+ addNewNote( jsonFromSRT.map( i => i.text).join('\n') )
+ // could instead delegate that later as event
})
}
applyNextFilter( contentFilename )
diff --git a/data/filters/another_content_filter_example.js b/data/filters/svg.js
similarity index 74%
rename from data/filters/another_content_filter_example.js
rename to data/filters/svg.js
index f4a1bc9..4142449 100644
--- a/data/filters/another_content_filter_example.js
+++ b/data/filters/svg.js
@@ -6,6 +6,9 @@ function filterSVGImage( contentFilename ){
if ( contentType.includes("image") && contentFilename.endsWith(".svg")) {
console.log('it is an SVG image', contentFilename)
+ // could also try to parse the SVG itself, for now delegated potentially via event
+ AFRAME.scenes[0].emit('svgloaded', contentFilename)
+
}
applyNextFilter( contentFilename )
}
diff --git a/data/filters/tapestry.json.js b/data/filters/tapestry.json.js
new file mode 100644
index 0000000..f45daf0
--- /dev/null
+++ b/data/filters/tapestry.json.js
@@ -0,0 +1,86 @@
+function filterTapestryJSON( contentFilename ){
+ let file = filesWithMetadata[contentFilename]
+ if (!file) return
+
+ let contentType = file.contentType
+
+ // TODO support zipped version directly, cf e.g. mapvisualmeta.jsons.zip.js
+
+ /*
+ // example of zip support
+ fetch( contentFilename ).then( r => r.blob(r) ).then( file => zip.loadAsync(file) ).then( f => {
+ console.log(f.files)
+ let filenameFromZip = Object.entries( f.files ).filter( i => i[0].includes("partialfilename.ext") ).map( i => i[0] )[0]
+ f.files[filenameFromZip].async("string").then( dyn_content => {
+ json = JSON.parse( dyn_content )
+ })
+ })
+ */
+
+ if ( contentType.includes("json") && contentFilename.startsWith("tapestry/") && contentFilename.endsWith("root.json")) {
+ console.log('it is a tapestry export file', contentFilename)
+ fetch( contentFilename ).then( r => r.json() ).then( json => {
+ // console.log( json )
+ let thumbnailPath = json.thumbnail.replace("file:/","/tapestry/")
+ let el = document.createElement("a-image")
+ el.setAttribute("position", "0 1.5 0.7")
+ el.setAttribute("rotation", "0 180 0")
+ el.setAttribute("src", thumbnailPath)
+ el.setAttribute("width", json.startView.size.width/10000)
+ el.setAttribute("height", json.startView.size.height/10000)
+ AFRAME.scenes[0].appendChild(el)
+
+ let positioned = {}
+ json.items.map( i => {
+ let x = 1-i.position.x/1000
+ let y = 2-i.position.y/1000
+
+ let el = null
+ if (i.type == "text") {
+ el = addNewNoteAsPostItNote( i.text, x + " " + y + " 1", ".1 .1 .1", "tapestry_"+i.id, "tapestry_text", true, "0 180 0")
+ // addNewNoteAsPostItNote( text, position=`-0.2 1.1 -0.1`, scale= "0.1 0.1 0.1", id=null, classes="notes", visible="true", rotation="0 0 0" ){
+ }
+
+ if (i.thumbnail) {
+ let thumbnailPath = i.thumbnail.source.replace("file:/","/tapestry/")
+ el = document.createElement("a-image")
+ el.setAttribute("position", x + " " + y + " 1")
+ el.setAttribute("rotation", "0 180 0")
+ el.setAttribute("src", thumbnailPath)
+ el.setAttribute("width", i.size.width/1000)
+ el.setAttribute("height", i.size.height/1000)
+ AFRAME.scenes[0].appendChild(el)
+ }
+ positioned[i.id] = el
+ if ( el ) el.setAttribute("target", "")
+ })
+
+ setTimeout( _ => {
+ // must be done after the items are positioned
+ json.rels.map( i => {
+ let el = document.createElement("a-tube")
+ console.log( i.from.itemId, i.to.itemId )
+ let start = positioned[i.from.itemId].getAttribute("position")
+ let end = positioned[i.to.itemId].getAttribute("position")
+ let mid = new THREE.Vector3()
+ mid.copy ( start )
+ mid.add ( end )
+ mid.divideScalar(2)
+ mid.z += 0.3
+ let path = [start, mid, end].map( p => AFRAME.utils.coordinates.stringify( p ) ).join(", ")
+
+ el.setAttribute("radius", 0.01)
+ el.setAttribute("path", path)
+ AFRAME.scenes[0].appendChild(el)
+ })
+ }, 1000 )
+
+ AFRAME.scenes[0].emit('tapestryjsonloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("visualmetajsonloaded", e => console.log(e))
+ })
+ }
+ applyNextFilter( contentFilename )
+}
+
+sequentialFilters.push( filterTapestryJSON )
diff --git a/data/filters/template_example.js b/data/filters/template_example.js
new file mode 100644
index 0000000..23e158b
--- /dev/null
+++ b/data/filters/template_example.js
@@ -0,0 +1,61 @@
+function filterTemplateExample( contentFilename ){
+ let file = filesWithMetadata[contentFilename]
+ if (!file) return
+ // can be removed for URLs as those are not with metadata
+
+ let contentType = file.contentType
+
+ // mereology option
+ let openingOptions = filesWithMetadata[contentFilename].openingOptions
+ // can be used via e.g. showFile("https://fabien.benetou.fr/?action=source",{ mereology:"whole"})
+
+ // filtering, only applying what's next to a certain content type and/or with filename filtering with .startsWith() .endsWith() .includes() or regex
+ if ( contentType.includes("xml") && contentFilename.includes("pdfxml/") ) {
+ console.log('it is a pdf unpacked file', contentFilename)
+
+ /*
+ // example of zip support
+ fetch( contentFilename ).then( r => r.blob(r) ).then( file => zip.loadAsync(file) ).then( f => {
+ console.log(f.files)
+ let filenameFromZip = Object.entries( f.files ).filter( i => i[0].includes("partialfilename.ext") ).map( i => i[0] )[0]
+ f.files[filenameFromZip].async("string").then( dyn_content => {
+ json = JSON.parse( dyn_content )
+ })
+ })
+ */
+
+ fetch( contentFilename ).then( r => r.text() ).then( txt => {
+
+ console.log( "mereology", openingOptions.mereology )
+ switch( openingOptions.mereology ) {
+ case "whole":
+ addNewNote(txt, "0 1.4 -.8")
+ break;
+ case "listonly":
+ default:
+ txt.replaceAll('* ','').split('\n').map( (c,i) => addNewNote(c, "0 "+(1+i/10)+" -.8") )
+ // could had here
+ }
+
+ const rootEl = document.createElement("a-entity")
+ // having a root element to attach on makes manipulation later on easier and safer, bringing a context for later modifications
+ rootEl.fromfile = txt
+ // to make the data usable, better with JSON or per element but still potentially useful by other elements
+ file.filteredEl = rootEl
+ rootEl.test = _ => console.log( 'test' )
+ // function that can then be called on the created element later on
+ // e.g. filesWithMetadata["https://companion.benetou.fr/saved/pdfxml/3209542.3209570.xml"].filteredEl.nextPage('ok')
+ AFRAME.scenes[0].appendChild(rootEl)
+
+ AFRAME.scenes[0].emit('templateexampleloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("templateexampleloaded", e => console.log(e))
+
+ } );
+ }
+ applyNextFilter( contentFilename )
+ // can stop here or move on to the next filter that might or not be applied
+}
+sequentialFilters.push( filterTemplateExample )
+// adding this to the list of filters to go through, order matters
+ // typically one would be generic filters first then more specific ones after
diff --git a/data/filters/visualmeta.json.js b/data/filters/visualmeta.json.js
new file mode 100644
index 0000000..1e264c1
--- /dev/null
+++ b/data/filters/visualmeta.json.js
@@ -0,0 +1,41 @@
+function filterVisualMetaJSON( contentFilename ){
+ let file = filesWithMetadata[contentFilename]
+ if (!file) return
+
+ let contentType = file.contentType
+
+ if ( contentType.includes("json") && contentFilename.endsWith(".visualmetaexport.json")) {
+ console.log('it is a visualmeta export file', contentFilename)
+ fetch( contentFilename ).then( r => r.json() ).then( json => {
+ //console.log( json )
+
+ let titleEl = addNewNote( json["visual-meta-bibtex-self-citation"].article.title, "0 1.8 -.3" )
+
+ json["visual-meta-full-document-text"].text.split("\n").reverse().map( (e, i) => {
+ let el = addNewNote( e, "0 "+(1+i/40)+" -.5" )
+ if ( e.startsWith("#") ){
+ //el.addEventListener("object3dset", _ => el.setAttribute("font-size", el.getAttribute("font-size")*20 ) )
+ // not yet set, events loaded and object3dset do not help
+ el.setAttribute("font-size", 0.3)
+ el.setAttribute("value", e.replaceAll("#","") )
+ // should be a bit more subtle...
+ }
+ })
+
+ Object.values( json["Glossary"].entries ).map( (e, i) => { let el = addNewNote( e.phrase + ' ' + e.entry, "-1.3 "+(1+i/10)+" -.6" ) })
+
+ Object.values( json["document-headings"] ).reverse().map( (e, i) => {
+ let el = addNewNote( e.name, "-1.1 "+(1+i/10)+" -.6" )
+ el.setAttribute("font-size", Number(e.level.replace("level",""))/10 )
+ el.addEventListener("object3dset", _ => el.object3D.translateX( 1-Number(e.level.replace("level",""))/10 ) )
+
+ })
+ AFRAME.scenes[0].emit('visualmetajsonloaded', contentFilename)
+ // to use the event consider :
+ //AFRAME.scenes[0].addEventListener("visualmetajsonloaded", e => console.log(e))
+ })
+ }
+ applyNextFilter( contentFilename )
+}
+
+sequentialFilters.push( filterVisualMetaJSON )
diff --git a/data/found_set_param.html b/data/found_set_param.html
new file mode 100644
index 0000000..1167aeb
--- /dev/null
+++ b/data/found_set_param.html
@@ -0,0 +1,337 @@
+Documentation of parameters
+
+The experience can be configured via its URL. This allow for customization by anyone while also allowing to share the result back with anyone else. There is no code modification needed, no saving needed. The URL itself becomes the modified experience.
+
+
+
+Within that context there are 3 main query parameters to understand, namely username , set and showfile . Specifically :
+
+
+username
: set a pre-defined experiment with its dedicated interface, data, etc. Note that some do require more parameters. An example could be https://companion.benetou.fr/index.html?username=demoqueueq1 which allowed for a sequence of experiments. It required no additional parameters. Alternatively https://companion.benetou.fr/index.html?username=q2_step_refcards_filtering&emulatexr=true has both a username (here q2_step_refcards_filtering ) and another parameter (emulatexr with value true). See the dedicated list below.
+set
: modify any element of the page. Typically this is done to visually modify something, showing or hiding elements. An example could be https://companion.benetou.fr/index.html?set_a-sky_color=purple&showfile=Fortress.glb which set the color of the sky to purple and show a file that is a 3D model of a castle where the user starts their experience. See the dedicated list below.
+showfile
: display the content of a file. This is done thanks to filters (see recorded video) and allows for a relatively wide range of files. The most basic ones, e.g. 3D model, show 3D models in space but some filters allow parsing of specific file formats, e.g. PmWiki lists, docx text content, zipped exports, etc. An example could be a remote VisualMeta export hosted on a WordPress instance. The filter unpacks the zip file and display its content while making each item interactable. Note also that some specific formats, e.g. _modifications.txt files, can contain set parameters and thus multiple modifications back. Shown files can also contain layouts of displayed content in order to resume the state of a past session, e.g. https://companion.benetou.fr/index.html?username=q2_annotated_bibliography_week2&showfile=q2_annotated_bibliography_week2_test_q2layout.json .
+
+
+
+
+Note on live documentation of experiments
+
+
+Most links should work, yes surely some will not work or won't make sense outside of XR or need some interactions.
+
+
+
+
+A lot of the ?set
examples (see below) do not show a visual difference as we decided at the end of the 1st quarter to hide a lot of elements to faciliate demonstrations. This unfortunately had the side effect of preventing those examples later on despite them working back then. For example at the end of Q2 ?set_IDenvironment_gltf-model=world-bake.glb
does not show the 3D model despite the file being present and showfile working because we are not hiding the element with ID environment.
+From an experimentation perspective it is arguably not useful to "fix" those historical links as doings will most likely prevent newer ones from working and thus rather unpredictively. It is though important to know that they did work.
+
+
+
+
+Regardless of those technical limitations please do feel free to report links that seem, according to you, not to work as expected. Plenty do provide :
+
+
+the URL (full including all parameters used),
+ hardware used, browser used (e.g. Vision Pro with OS version 123 and browser version 456),
+ date and hour of test (as things are live, maybe something was edited as you tried)
+ the current behavior (what you expected to see),
+ the expected behavior (what actually happened),
+ any supporting materials you think would be useful, e.g. video recording, screenshots, etc.
+
+
+
+
+Parameters
+
+
+allowNtfyFeedbackHUD
+ emulatexr
+ forcecontrollers
+ itemsfile
+ partialfilename
+ query
+ remote_keyboard_group // should be URL encoded, assuming for now alphanum only
+ shareerrors
+ showdebug
+ showdefinitions
+ showdemoexample
+ sourceFromNextDemo
+ speedup_emulatexr_test
+ username
+
+
+
+
+Note that some parameters (e.g. emulatorxr
) are only used in conjonction with other parameters. Here emulatorxr
with username
itself within only a range of values, see below.
+
+
+
+Some parameters added after the creation of this documenation (early June 2025) are not documented.
+
+How to populate that list
+Ran server side
+grep urlParams.get /transition/webdav/data/fotsave/fot_sloan_companion_public/index.html | sed "s/.*get//" | sort | uniq
+
+
+Username values
+for username which is itself a just a parameter
+
+
+
+cubetester
+ demoqueueq1
+ icon_tags
+ instructionsonhands
+ jsonrefmanualtester
+ metatester10032025
+ metatester13032025
+ poweruser
+ refoncubetester
+ ring_discovery
+ ring_discovery_with_keyboard
+ ring_highlights
+ skating_rings
+ spreadsheetcolumns
+ tabletest
+ temple_test
+ thicknesstesteruser
+
+
+
Quarter 1
+
+q1_step_audio
+ q1_step_highlights
+ q1_step_refcards
+ q1_step_screenshot
+ q1_step_showfile
+ q1_step_urlcustom
+
+
+
Quarter 2
+
+q2_annotated_bibliography
+ q2_annotated_bibliography_week2
+ q2_arcade
+ q2_bbox_per_filter_source
+ q2_drop_for_graph
+ q2_fingersmenu
+ q2_handswap
+ q2_immersive_console
+ q2_json_collaborations
+ q2_keydrumsticks
+ q2_keymap
+ q2_lego_map
+ q2_lense
+ q2_most_recent_file
+ q2_noneuclidian
+ q2_nouploadfile
+ q2_ntfy_keyboard_with_keymap_visual_feedback
+ q2_onrelease_lookat
+ q2_os_keyboard
+ q2_pasting
+ q2_picker
+ q2_remote_ntfy_keyboard
+ q2_ring_keyboard
+ q2_secondarypinch_singlehanded
+ q2_secondarypinch_singlehanded_spatial
+ q2_spatialknowledgeobject
+ q2_step_contextuallayouts
+ q2_step_end
+ q2_step_highlight
+ q2_step_jsonedit
+ q2_step_layout_animationtests
+ q2_step_refcards_filtering
+ q2_step_start
+ q2_step_volumetric_frames
+ q2_visualmetaexport
+ q2_visualmetaexport_map
+ q2_visualmetaexport_map_via_wordpress
+ q2_visualmetaexport_map_via_wordpress_with_keyboard
+ q2_visualmetaexport_map_via_wordpress_with_lookat
+ q2_wrist_rotations
+ q2_yubikeyotp
+
+
+
+
+How to populate that list
+Ran server side
+grep username /transition/webdav/data/fotsave/fot_sloan_companion_public/index.html | sed "s/.*== //" | sed "s/) {//" | grep -v dictionaryForCompletion | sort | uniq
+
+
+emulatexr working with Username values
+and for test scenarii only which is with emulatexr parameter
+
+
+q2_lense
+ q2_step_layout_animationtests
+ q2_step_refcards_filtering
+ q2_step_volumetric_frames
+
+
+
+
+How to populate that list
+Ran server side
+head -300 /transition/webdav/data/fotsave/fot_sloan_companion_public/index.html | grep username | sed "s/.*== //" | sed "s/) {//" | grep -v dictionaryForCompletion | sort | uniq
+
+there are a quite a few more from Q1 via specifically parametersViaURL because that one https://git.benetou.fr/utopiah/spasca-fot-sloan-q1/src/branch/main/data/index.html#L602 can do a lot, letting anybody changing what is shown or not, how, etc
+
+
+Set values
+
+Syntax
+
+selector ID environment (not # as this is not a valid URL query parameter!) or .classname
+ attribute name (e.g. color)
+ value, (e.g. blue)
+
+
+For example ?set_.pannel_color=red
will set to "red" the attribute named "color" for all elements of class name "pannel".
+
+Example of values
+
+color=blue
can be a string of HTML colors
+gltf-model=world-bake.glb
can be a relative or absolute URL (where CORS is supported)
+visible=false
to show or hide an element, a boolean string that can be either true or false (not True or False or TRUE or FALSE!)
+src=https://webdav.benetou.fr/fotsave/fot_sloan_companion_public/world-bake.glb
can be a full URL (where CORS is supported)
+src=pano4s.png
can also be the relative URL of an image, using it as texture
+scale=1%201%201
as a scale for width height and depth, e.g. here a uniform scale of 1 (URI encoded, where %20 becomes an empty space " ")
+position=0%201%200
as metric position along X, Y and Z axis, X=-1 is 1 to the left of the starting position, Y=1 is 1m up and Z=-1 is 1m ahead
+rotation=0%20-30%200
as angle rotation in degrees along X, Y and Z axis relative to the object center, here 0 deg pitch, -30 yaw and 0 roll (see details )
+
+
+Your browser will automatically URI encode your URL, thus one can type position=0 1.4 -1
and get position=0%201.4%20-1
+
+
+To get a URL to later on use as a parameter value consider using the upload function of the prototype https://companion.benetou.fr which in turns make the resulting file available at https://companion.benetou.fr/filename
+
+Values used
+
+
+?set_.manuscript_color=blue
+ ?set_.notes_visible=false
+ ?set_IDenvironment_visible=false&set_.pannel_color=blue&set_a-sky_color=grey&showfile=Fortress.glb
+ ?set_IDenvironment_visible=false&set_.pannel_color=blue&set_a-sky_color=grey&showfile=Fortress.glb&showfile=augmented_paper.pdf-0.jpg
+ ?set_IDenvironment_visible=false&set_.pannel_color=blue&set_a-sky_color=grey&showfile=Fortress.glb&showfile=augmented_paper.pdf-1.jpg
+ ?set_IDenvironment_visible=false&set_.pannel_color=blue&set_a-sky_color=grey&showfile=Fortress.glb&showfile=fabien_modifications_test.txt
+ ?set_IDenvironment_visible=false&showfile=Apartment.glb
+ ?set_IDmanuscript_color=blue
+ ?set_IDmanuscript_color=lightbrown
+ ?set_IDmanuscript_color=lightyellow
+ ?set_a-sky_src=Solstice-sunrise-cylinder-105-degrees.png
+ ?set_a-sky_src=Solstice-sunrise-cylinder-105-degrees.png
+ ?set_a-sky_src=Solstice-sunrise-cylinder-105-degrees.png&set_IDgroundfor360_visible=true
+ ?set_a-sky_src=Solstice-sunrise-cylinder-105-degrees.png&set_IDgroundfor360_visible=true
+ ?set_a-sky_src=faded-ground-keithm.jpg
+ ?set_a-sky_src=faded-ground-keithm.jpg&set_IDgroundfor360_visible=true
+ ?set_a-sky_src=office%20windows%20closed%20edited.jpg
+ ?set_a-sky_src=office%20windows%20closed%20edited.jpg
+ ?set_a-sky_src=office-windows-closed-edited25.png
+ ?set_a-sky_src=pano3s.png
+ ?set_a-sky_src=pano4s.png
+ ?set_a-sky_src=spacemed.jpg&set_IDgroundfor360_visible=true
+ ?set_a-sky_src=spacemed.jpg&set_IDgroundfor360_visible=true
+ ?set_a-sky_src=spacemed.jpg&set_IDgroundfor360_visible=true
+ ?set_a-sky_src=spacemed.jpg&set_IDgroundfor360_visible=true&set_IDgroundfor360_scale=2%202%202
+ ?set_a-sky_src=spacemed.jpg&set_IDgroundfor360_visible=true&set_IDgroundfor360_scale=20%202%2020
+ ?set_environmentsky_color=green
+ ?set_environmentsky_color=red
+
+
+Known problematic ones (usually not a bug, just not visible change)
+
+?set_.notes_visible=false&set_IDpanopticonpannels_visible=false
+ ?set_IDenvironment_visible=false&set_.pannels_color=blue
+ ?set_IDenvironment_visible=false&set_IDpanopticonpannels_visible=false
+ ?set_IDenvironment_src=https://webdav.benetou.fr/fotsave/fot_sloan_companion_public/world-bake.glb
+ ?set_IDenvironment_visible=false&set_.pannel_color=blue
+ ?set_IDenvironment_visible=false&set_.pannel_color=blue&set_a-sky_color=grey
+ ?set_IDenvironment_gltf-model=world-bake.glb
+ ?set_IDenvironment_gltf-model=world-bake.glb&set_IDenvironment_scale=1%201%201
+ ?set_IDenvironment_gltf-model=world-bake.glb&set_IDenvironment_scale=1%201%201&set_IDenvironment_position=0%200%200
+ ?set_IDenvironment_gltf-model=world-bake.glb&set_IDenvironment_scale=1%201%201&set_IDenvironment_position=0%201%200&set_IDenvironment_rotation=0%20-30%200
+ ?set_IDenvironment_gltf-model=world-bake.glb&set_IDenvironment_scale=1%201%201&set_IDenvironment_position=0%201%200&set_IDenvironment_rotation=0%200%200
+ ?set_IDenvironment_gltf-model=world-bake.glb&set_IDenvironment_scale=1%201%201&set_IDenvironment_position=0%201%200&set_IDenvironment_rotation=0%2030%200
+ ?set_IDenvironment_gltf-model=world-bake.glb&set_IDenvironment_scale=1%201%201&set_IDenvironment_position=0%202%20-7&set_IDenvironment_rotation=0%20-30%200
+ ?set_IDenvironment_gltf-model=world-bake.glb&set_IDenvironment_scale=1%201%201&set_IDenvironment_position=0%203%200&set_IDenvironment_rotation=0%20-30%200
+ ?set_.pannel_color=blue
+ ?set_.pannel_color=red
+ ?set_.environment_visible=false&set_.panopticonpannels_visible=false
+ ?set_IDpanopticonpannels_position=0%200%200&set_IDpanopticonpannels_rotation=0%2045%200
+ ?set_IDpanopticonpannels_position=0%200%200&set_IDpanopticonpannels_rotation=0%2090%200
+ ?set_IDpanopticonpannels_position=0%200.5%200
+ ?set_IDpanopticonpannels_position=0%200.5%200&set_IDpanopticonpannels_rotation=0%2045%200
+ ?set_IDpanopticonpannels_position=0%202%200
+ ?set_panopticonpannels_position=0%201%200
+ ?set_panopticonpannels_scale=.1%20.1%20.1
+ ?set_panopticonpannels_scale=2%202%202
+ ?set_IDenvironment_src=url(world-bake.glb)
+ ?set_IDenvironment_src=world_bake.glb
+ ?set_environment_glb=Fortress.glb
+ ?set_environment_glb=url(Fortress.glb)
+ ?set_environment_gltf-model=url(Fortress.glb)
+ ?set_environment_gltf-model=url(Fortress.glb)&set_environment_position=0%20-1%200
+ ?set_environment_gltf-model=url(Fortress.glb)&set_environment_position=0%20-10%200
+ ?set_environment_gltf-model=url(Fortress.glb)&set_environment_position=0%200%200
+ ?set_environment_visible=false&set_panopticonpannels_visible=false
+ ?set_environment_visible=false&set_panopticonpannels_visible=false
+
+
+
+
+How to populate that list
+Ran client side
+sqlite3 /home/fabien/Prototypes/places.sqlite "select url from moz_places where url like '%?set_%';" | sort | uniq
+
+
+See also
+
+listing of Q1 demos https://companion.benetou.fr/demos_example.html?filename=demo_q1.json
+ listing of Q2 demos (still to be edited) https://companion.benetou.fr/demos_example.html?filename=demo_q2.json
+ to provide feedback on demo sets https://companion.benetou.fr/demos_feedback_example.html
+ to visually edit a demo set https://companion.benetou.fr/demos_editor_example.html
+
diff --git a/data/gesture-exploration.js b/data/gesture-exploration.js
index 852b0ae..7ac0c1e 100644
--- a/data/gesture-exploration.js
+++ b/data/gesture-exploration.js
@@ -8,9 +8,18 @@ generalize showGestureDebug to any joint, not just thumb-tip of right hand
*/
+// consider which gestures are more inclusive, more minimalist to be more accessible
+
targetGesture = {"microgesture":{"type":"glyph","action":"Extension","context":["Contact","Air"],"parameters":{"pressure":{"start":null,"end":null,"type":"no_end"},"amplitude":{"start":null,"end":null,"type":"no_end"},"time":{"start":null,"end":null,"leftType":"none","rightType":"bigger"}},"actuator":[["thumb"]],"phalanx":[]}}
// supports both hands
+otherTargetGesture = {"microgesture":{"type":"seq","quantificators":{"x":[[["thumb"],["index"],["middle"],["ring"],["pinky"]],[["thumb"],["index"],["middle"],["ring"],["pinky"]]],"y":[]},"seq":{"type":"and","operands":[{"type":"glyph","action":"Any","context":["Air","Contact"],"parameters":{"pressure":{"start":null,"end":null,"type":"no_end"},"amplitude":{"start":null,"end":null,"type":"no_end"},"time":{"start":null,"end":null,"leftType":"none","rightType":"bigger"}},"actuator":[["x0"]],"contact":{"type":"contact","action":"Contact","parameters":{"pressure":{"start":null,"end":null,"type":"no_end"},"amplitude":{"start":null,"end":null,"type":"no_end"},"time":{"start":null,"end":null,"leftType":"none","rightType":"bigger"}},"actuator":[["x1"]]},"phalanx":[]},{"type":"glyph","action":"Any","context":["Air","Contact"],"parameters":{"pressure":{"start":null,"end":null,"type":"no_end"},"amplitude":{"start":null,"end":null,"type":"no_end"},"time":{"start":null,"end":null,"leftType":"none","rightType":"bigger"}},"actuator":[["x1"]],"contact":{"type":"contact","action":"Contact","parameters":{"pressure":{"start":null,"end":null,"type":"no_end"},"amplitude":{"start":null,"end":null,"type":"no_end"},"time":{"start":null,"end":null,"leftType":"none","rightType":"bigger"}},"actuator":[["x0"]]},"phalanx":[]}]}}}
+// name = closed gap touch
+// text description = Press the specified fingers together.
+// different from "finger pinch" which is just between thumb and another finger, at specified locations
+
+// from https://lig-microglyph.imag.fr
+
const fingersNames = ["index-finger", "middle-finger", "ring-finger", "pinky-finger","thumb"]
const tips = fingersNames.map( f => f+"-tip" )
const thumbParts = ["metacarpal", "phalanx-proximal", "phalanx-distal"] // no phalanx-intermediate for thumb
@@ -114,6 +123,212 @@ function drawPoints(points){
return el
}
+// gesture(s) to detect, callback on detection, by default debugging via console or new debuggraph
+ // should also emit event
+
+ // might need some form of hierarchy, so that composable gestures don't overlap or re-use components from other gestures
+ // e.g. if there is a fingertip to metacarpal gesture for the index, don't re-write the same for the little finger
+ // same for other hand, i.e. left vs right
+ // could be computationally interesting too, e.g. stop checking for a gesture if part of it failed already, e.g. stop checking for a gesture if part of it failed already
+
+
+AFRAME.registerComponent('cube-pull', {
+ init: function () {
+ this.tick = AFRAME.utils.throttleTick(this.tick, 50, this);
+ },
+ tick: function (t, dt) {
+ const myScene = AFRAME.scenes[0].object3D
+ if (!myScene.getObjectByName("l_handMeshNode") ) return
+ const wrist = myScene.getObjectByName("r_handMeshNode").parent.getObjectByName("wrist")
+ let sum = Math.abs(wrist.rotation.y) + Math.abs(wrist.rotation.y) + Math.abs(wrist.rotation.z)
+ console.log( sum )
+ if ( sum < .3 ) cubetest.setAttribute("position", AFRAME.utils.coordinates.stringify( wrist.position ) ) // doesn't look good, cube on wrist is moving quite a bit too
+ // could emit event too
+ // could check if all joints have close to 0 rotation on ...
+ // are roughly on the same y-plane of the wrist (facing up or down)
+ }
+})
+
+AFRAME.registerComponent('verticalflatpalmrighthand', {
+ schema: {
+ command: {type: 'string'},
+ },
+ init: function () {
+ this.tick = AFRAME.utils.throttleTick(this.tick, 50, this);
+ window.lastGesture = Date.now() // to initialize
+ },
+ tick: function (t, dt) {
+ const myScene = AFRAME.scenes[0].object3D
+ if (!myScene.getObjectByName("l_handMeshNode") ) return
+ const wrist = myScene.getObjectByName("r_handMeshNode").parent.getObjectByName("wrist")
+ let sum = Math.abs(wrist.rotation.y) + Math.abs(wrist.rotation.y) + Math.abs(wrist.rotation.z)
+ console.log( sum )
+ //document.querySelector('[isoterminal]').emit("send", [sum + (window.lastGesture+1000>now)]) // +"\n"]) // problematic for special commands
+ //document.querySelector('[isoterminal]').emit("send", ["\n"]) // kind of horizontal
+ if ( sum < .3 ) {
+ // spams the terminal
+ // could emit event too
+ let now = Date.now()
+ if ( now - window.lastGesture > 1000 ){
+ document.querySelector('[isoterminal]').emit("send", [this.data.command]) // +"\n"]) // problematic for special commands
+ window.lastGesture = now
+ AFRAME.scenes[0].emit('gesture', {date:now, type:'vertical flat palm', value:sum})
+ }
+ // no event if inhibited due to refractory period
+ }
+ if ( sum > 1 ) {
+ let now = Date.now()
+ if ( now - window.lastGesture > 1000 ){
+ document.querySelector('[isoterminal]').emit("send", ["\n"]) // kind of horizontal
+ window.lastGesture = now
+ AFRAME.scenes[0].emit('gesture', {date:now, type:'horizontal flat palm', value:sum})
+ }
+ // no event if inhibited due to refractory period
+ }
+ // could check if all joints have close to 0 rotation on ...
+ // are roughly on the same y-plane of the wrist (facing up or down)
+ }
+})
+
+AFRAME.registerComponent('gestures', {
+ init: function () {
+ this.tick = AFRAME.utils.throttleTick(this.tick, 50, this);
+ // maybe every 50ms is too often, or not often enough, to check
+ },
+ // consider also tock, cf https://aframe.io/docs/1.7.0/core/component.html#before-after
+ tick: function (t, dt) {
+ // consider here the order or hierarchy
+ // optimisation? reconciliation? cascading?
+
+ // example of context, e.g. using distance of wrist to a target object, or time, or any other condition
+
+ // could be defined else as long as it respect a specific format, like filters/converters/etc
+ // starting with emiting an event, e.g.
+ // AFRAME.scenes[0].emit('gesture', {date:now, type:'horizontal flat palm', value:sum})
+ // or context (unsure if truly has to be separated)
+ // AFRAME.scenes[0].emit('gesturecontext', {date:now, type:'right wrist in volume of target' })
+
+ // detect gesture A
+ // emit event
+ // detect gesture B
+ // emit event
+ // detect gesture C
+ // emit event
+ },
+})
+
+AFRAME.registerComponent('gesture-listneres', {
+ events: {
+ gesture : function (event) {
+ console.log( 'gesture captured:', event.detail )
+ },
+ gesturecontext : function (event) {
+ console.log( 'gesture context captured:', event.detail )
+ }
+ }
+})
+
+AFRAME.registerComponent('positional-context', {
+ schema: {
+ target: {type: 'selector'},
+ attribute: {type: 'string'},
+ value: {type: 'string'},
+ threshold: {type: 'float'}
+ },
+ init: function () {
+ this.tick = AFRAME.utils.throttleTick(this.tick, 500, this);
+ },
+ tick: function (t, dt) {
+ const myScene = AFRAME.scenes[0].object3D
+ if (!myScene.getObjectByName("l_handMeshNode") ) return
+ const wrist = myScene.getObjectByName("r_handMeshNode").parent.getObjectByName("wrist").position
+ // console.log ( this.data.target.object3D.position, this.data.threshold, wrist.distanceTo( this.data.target.object3D.position ) < this.data.threshold )
+ if ( wrist.distanceTo( this.data.target.object3D.position ) < this.data.threshold ){
+ // could emit event too
+ AFRAME.scenes[0].setAttribute(this.data.attribute, "command:"+ this.data.value)
+ // wrong... this should still delegate instead to a gesture (or gestures) that would THEN conditionally pass the command
+ AFRAME.scenes[0].emit('gesturecontext', {date:now, type:'right wrist in volume of target' })
+ // could be used to check for enter/leave based on previous state until now
+ } else {
+ AFRAME.scenes[0].removeAttribute(this.data.attribute)
+ }
+ }
+})
+
+setTimeout( _ => {
+ // username based to isolate testing
+ if (username && username == "cubepull") {
+ AFRAME.scenes[0].setAttribute("cube-pull", "")
+ }
+ // multi-users as multi-gestures?
+
+ // cascading gestures
+
+ console.log ( window.location, window.location.search.includes("xrsh") )
+ //if ( window.location.search.includes("xrsh") ) {
+ if ( window.location.host.includes("fabien.benetou.fr") ) {
+ // URL modified by XRSH
+ // if (username && username == "xrsh") {
+ let el = document.createElement("a-sphere")
+ el.id = "contextsphere"
+ el.setAttribute("radius", "0.3") // fails on .3 or 0.3
+ //el.setAttribute("scale", "0.3)
+ el.setAttribute("wireframe", "true")
+ el.setAttribute("position", "0 1.4 -1")
+ AFRAME.scenes[0].appendChild(el)
+
+console.log( 1337 )
+ let cube = document.createElement("a-box")
+ cube.setAttribute("target", "") // no effect, probably over written by XRSH
+ cube.setAttribute("color", "green")
+ cube.id = "cubetest"
+ cube.setAttribute("scale", ".1 .1 .1")
+ cube.setAttribute("position", "0 1.4 -1")
+ AFRAME.scenes[0].appendChild(cube)
+
+ AFRAME.scenes[0].setAttribute("positional-context", "target:#contextsphere; threshold:0.3; attribute:verticalflatpalmrighthand; value: ls -l\\n;")
+
+ AFRAME.scenes[0].setAttribute('gesture-listneres','')
+ // gesture to
+ // go back in history (then down again)
+ // to execute
+ /*
+ // enter
+ document.querySelector('[isoterminal]').emit("send", ["\n"])
+
+ // arrow keyup
+ document.querySelector('[isoterminal]').emit("send", ["\x1b[A"])
+
+ // arrow keydown
+ document.querySelector('[isoterminal]').emit("send", ["\x1b[B"])
+ */
+
+ }
+ // that can also be visible, e.g. wireframe
+ if (username && username == "cubepullwithincontext") {
+
+ let cube = addCubeWithAnimations()
+ cube.setAttribute("target", "")
+ //cube.setAttribute("visible", "false")
+
+ el = document.createElement("a-sphere")
+ el.id = "contextsphere"
+ el.setAttribute("radius", .3)
+ el.setAttribute("wireframe", "true")
+ el.setAttribute("position", "0 1.4 -1")
+ AFRAME.scenes[0].appendChild(el)
+
+ AFRAME.scenes[0].setAttribute("positional-context", "target:#contextsphere; threshold:0.3; attribute:cube-pull;")
+ }
+ // that can also be visible, e.g. wireframe
+
+ // could do so with example of index finger tip within range of targets, if so make a visual change
+ // on within cube, off if outside of it
+ // could change the cube color to show the difference too
+
+ // try perf on Quest1
+}, 1000 )
+
// should be a component instead...
setTimeout( _ => {
const myScene = AFRAME.scenes[0].object3D
diff --git a/data/index.html b/data/index.html
index 4bf92dc..909f861 100644
--- a/data/index.html
+++ b/data/index.html
@@ -2,22 +2,345 @@
+
JXR filesystem and mimetype based explorations
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
-
+
@@ -25,14 +348,30 @@ let sequentialFiltersInteractionOnReleased = []
-
+
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -47,6 +386,12 @@ let sequentialFilters = []
+// -----=========== ring system =============--------------------------------------------------------------------------------------------
+
+/* AFrame example
+
+
+
+
+
+
+
+
+*/
+
+function sceneIdsToElementForRings(){
+ // does not actually traverse the scene, only gets the top level entities
+ Array.from( document.querySelector("a-scene").childNodes ).filter( el => el.id && !el.id.startsWith("note_") ).reverse().map( (el,i) => {
+ let noteEl = addNewNote("#"+el.id, "1 "+(1+i/50)+" -1")
+ noteEl.setAttribute("onpicked","startRingCheck()")
+ noteEl.setAttribute("onreleased","endRingCheck()")
+ })
+}
+
+function getElementsCurrentTag(){
+ // including rings...
+ if (!window.currentTag || !window.currentTag.model) {
+ console.warn('tag missing')
+ return []
+ }
+ let res = Array.from( document.querySelectorAll("a-gltf-model[src='"+window.currentTag.model+"']") )
+ // should filter out with internal_tag
+ res = res.filter( el => el.getAttribute("tag") != "internal_tag" ) // special tag value to ignore
+ Array.from( document.querySelectorAll(".selected_via_tag") ).map( i => i.classList.remove("selected_via_tag") )
+ res.map( i => i.parentNode.classList.add( 'selected_via_tag' ) )
+ console.log(res)
+ return res
+ // then used with resolvedElements (and possibly clipboard-ish), approach to unify
+}
+
+let ringCheckInterval = null
+let ringAdded = false
+let ringsUsedInLastSession = []
+let trailingLineEnabled = false
+
+function startRingCheck( feedbackDistance = .2, activationDistance = .1 ){
+ const refractoryPeriodRing = 500 // ms, consider lower value for keystrokes equivalent
+ // document.querySelector("a-console").setAttribute("visible", true)
+ let pos = new THREE.Vector3()
+ let el = selectedElements.at(-1)?.element
+ if (!el) return
+
+ let historyPositions = []
+ document.querySelector("a-console").setAttribute("visible", "true")
+ ringCheckInterval = setInterval( el => {
+ el = selectedElements.at(-1)?.element
+ el.object3D.getWorldPosition( pos )
+ let handAvailable = AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode")
+
+ // trailing line
+ if ( trailingLineEnabled && handAvailable ){
+ historyPositions.push( AFRAME.utils.coordinates.stringify( AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode").parent.getObjectByName("thumb-tip").position ) )
+ if (historyPositions.length > 10){
+ let path = historyPositions.slice(-10).join(", ")
+ // console.log( path )
+ rings.querySelector("a-tube").setAttribute("path", path)
+ // console.log( rings.querySelector("a-tube") )
+ }
+ }
-
+ Array.from( active_rings.children )
+ .concat( Array.from( tagging_active_rings.children ) ) // mixing ring sets
+ .concat( [ring_dial_test] ) // should find a more generalizable way... but convenient too
+ .concat( [ ring_dial_color, ring_dial_color_r, ring_dial_color_g, ring_dial_color_b, ] )
+ .filter( r => {
+ // hidden rings should not be active (probably has a threejs helper...)
+ let hidden = false
+ r?.object3D?.traverseAncestors( a => { if (!a.visible) hidden = true } )
+ // causing errors with ring dial tests, hence the ?. way
+ return !hidden
+ })
+ .map( r => {
-
-
-
-
-
-
-
+ r.setAttribute("animation", "property: rotation.z; from: 0; to: 360; dur: 1000; startEvents:startAnimation;")
-
+ let pos_ring = new THREE.Vector3()
+ r.object3D.getWorldPosition( pos_ring )
-
-
+ let d = pos.distanceTo( pos_ring )
-
-
-
-
-
+ // visual feedback on proximity
+ if (d refractoryPeriodRing){
+ console.log('+++ safe to re-run')
+ ringsUsedInLastSession.push( { ring: r, target: el, timestamp: Date.now() } )
+ executeRing( r, el )
+ // tested just a bit, seems fine
-
-
-
-
+ // append as clone of r to rings_stack done in endRingCheck()
+ } else {
+ console.log('!!! ignored, too fast and same ring')
+ }
+ }
+ })
+ }, 100)
+}
-
-
+function executeRing( r, el ){
+ // already used... should try another visual property
+ // r.setAttribute("opacity", .5)
+ r.setAttribute("wireframe", "true")
-
-
+ // ding sound
+ audio.play()
+
+ // restart every time
+ r.emit('startAnimation', null, false)
+
+ let elValue = el.getAttribute("value")
+ // to clean up otherwise always expect a value, e.g. on gltf-model
+
+ // ---------------------------------------- resolving -----------------------------------------------------
+ let resolvedElements = []
+
+// consider threejs named objects e.g.
+ // myScene.getObjectByName("r_handMeshNode").parent.getObjectByName("wrist")
+ if ( elValue && elValue.length > 0 && elValue.startsWith(".") || elValue.startsWith("#") || elValue.startsWith("a-") ){
+ console.log('elValue', elValue)
+ getElementsCurrentTag() // implicitly called
+ if ( elValue.startsWith(".") ){
+ resolvedElements = Array.from( AFRAME.scenes[0].querySelectorAll(elValue) )
+ setFeedbackHUD('got class '+ elValue+' element numbers: '+ resolvedElements.length )
+ } else {
+ el = document.querySelector(elValue)
+ // assuming "a-..." is unique, e.g. a-sky, which isn't true for others, e.g. a-troika-text
+ }
+ }
+ let value = r.querySelector("a-troika-text").getAttribute("value")
+ // should instead "just" apply the jxr, done here just for testing
+ // probably need some refractory period too, otherwise stacking transformations that are probably not required
+
+ // ---------------------------------------- this should be based on value itself, i.e. another eval() -----------------------------------------------------
+ // see applyFunctionToSelection() ... which does not exist
+ // can be replaced by something more generic, there are already jxr shortcuts e.g. sa attributeName attributeValue
+ // could be done as filters are done, i.e. filters.map( f => f(element) )
+
+ if (resolvedElements.length == 0) resolvedElements.push(el)
+
+ resolvedElements.map( rEl => {
+ if (value.includes("scaleUp") ) rEl.setAttribute("scale", ".01 .02 .02")
+ if (value.includes("White") ) rEl.setAttribute("color", "white")
+ if (value.includes("Blue") ) rEl.setAttribute("color", "blue")
+ if (value.includes("Red") ) rEl.setAttribute("color", "red")
+ if (value.includes("Green") ) rEl.setAttribute("color", "green")
+ if (value.includes("setFontSize") ) rEl.setAttribute("font-size", Number(r.querySelector("a-troika-text").getAttribute("value").split("(")[1].split(")")[0])/10 )
+ if (value.includes("setColorFull") ) rEl.setAttribute("color", r.querySelector("a-troika-text").getAttribute("value").split('"')[1] )
+ // update from 3 values of R/G/B done at the ring dial level
+ })
+
+ if (value.includes("filterBibtex") ) {
+ // generate new class name based on filter value and UUID-ish
+ const newClassName = value.replace(/\W/g, '')+Date.now()
+ // attach class name to all resolvedElements matching that condition filter
+ resolvedElements
+ .filter( el => ! (el.data["bibtex-data"].year > 2000) ) // hardcoded for now
+ .map( el => el.classList.add( newClassName ) )
+ // make that new class name available for further processing
+ let newNoteFromNewClassName = addNewNote( '.'+newClassName )
+ newNoteFromNewClassName.setAttribute("onpicked", "startRingCheck()")
+ newNoteFromNewClassName.setAttribute("onreleased", "endRingCheck()" )
+ }
-
+ // could include resolving too, e.g .collidable doesn't get appended as-is but rather its resulting elements do
+ // could be better to do it later on, at the transformation level, going through ring
+ if (value.includes("addToRingSelection") ) {
+ // try to get the selection value and append to it, if none make new one
+ const selectionNodeId = "selectionnote"
+ let selectionNoteEl = document.getElementById( selectionNodeId )
+ if (!selectionNoteEl){
+ let newPos = r.getAttribute("position").clone()
+ newPos.y += .3
+ selectionNoteEl = addNewNote( elValue, AFRAME.utils.coordinates.stringify( newPos ), "0.1 0.1 0.1", selectionNodeId)
+ // text only, not 3D model, for this would try to get its ID instead
+ selectionNoteEl.setAttribute("onpicked", "startRingCheck()")
+ selectionNoteEl.setAttribute("onreleased", "endRingCheck()")
+ } else {
+ selectionNoteEl.setAttribute("value", selectionNoteEl.getAttribute("value") + "\n" + elValue )
+ }
+ }
-
+ if (value.includes("addRingFromCode") ) {
+ let newRing = addRing( elValue )
+ // if it is jxr should then rotate within
+ if ( elValue && elValue.startsWith("jxr "))
+ setTimeout( _ => {
+ let textEl = newRing.querySelector("a-troika-text")
+ textEl.setAttribute("curve-radius","1")
+ textEl.setAttribute("rotation","90 0 0")
+ textEl.setAttribute("position",".0 .1 .0")
+ })
+ }
-
-
+ if (value.includes("setAsActiveTagging")) {
+ // "type" check, not everything can be the right input
+ let modelSrc = el.getAttribute("gltf-model")
+ if (!modelSrc) {
+ setFeedbackHUD('tag not set, use a 3D model tag')
+ } else {
+ window.currentTag = {}
+ window.currentTag.model = el.getAttribute("gltf-model")
+ window.currentTag.scale = el.getAttribute("scale")
+ window.currentTag.rotation = el.getAttribute("rotation") // gets overwritten, should be offset applied also while moving it
+ // TODO should get the initial one, not the current rotation
+
+ // visual impact
+ setFeedbackHUD('tag set as', window.currentTag.model) // something is of, as it is empty
+ addOrUpdateTagToElement( r, "internal_tag" )
+ }
+ }
-
-
-
+ if (value.length==1) {
+ el.setAttribute("value", el.getAttribute("value")+value)
+ // character append
+ // could then make a keyboard this way...
+ // but 26 characters make for very large movements
+ // assuming a meta-ring but the layout itself can be totally different
+ // can try much smaller rings
+ // also need a refractory period
+ // add another ring to
+ // create new word (how?)
+ // split existing word on " "
+ AFRAME.scenes[0].emit('virtualkeypress', value )
+ }
-
-
-
+ // ---------------------------------------- -----------------------------------------------------
-
-
-
-
-
+ // should then
+ // clone ring then append it to rings_stack (for follow up macro)
+ // this should be permanent, i.e. JSON WebDAV save, and movable, i.e add target attribute (done)
+ // show/unhide/unfold the "following" rings based on context
-
-
+ // special rings ideas
-
-
-
-
-
-
-
-
-
+ // key aspect of UI/UX
-
-
-
-
+ // could also be limited to a specific ring
+ if (!ringAdded){
+ // here we are considering a sequential "positive" flow
+ // but could also be negative, a la ! or conditional e.g. && or ||
+ // so rings could have a type or return value equivalent
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ // if used as contextual menu, could reset/hide at endRingCheck()
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ ringAdded = true
-
+ /*
+ // need refractory period or maximum amount of times added
+ let newRing = addRing("jxr console.log('Red')", r.getAttribute("position") )
+ newRing.setAttribute("color", "red")
+ setTimeout( _ => newRing.object3D.position.z += .3, 100 )
+ // offset from current ring position
+ */
-
-
-
-
-
+ // this itself should enable the creation of a 4th ring, etc
+ addContextualRings( r )
+ }
+}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+function layoutsSwitch( elements=[] ){
+ // elements = "abcdefghijklmnopqrstuvwxyz ,.{}()[]".split('').reverse()
+ elements = Array.from( document.querySelectorAll( '.kbd_key' ) )
+ // ignored parameter for testing, should be an array
+
+ let hOffset = 1
+ let dOffset = -1.3
+
+ let layouts = {}
+ layouts.horizontal = []
+ layouts.vertical = []
+ layouts.spiral = []
+ layouts.circle = []
+ let layoutNames = [ "horizontal", "vertical", "spiral", "circle" ]
+ //layoutNames = [ "horizontal" ]
+ // layoutNames = [ "vertical" ]
+
+ elements.map( (el,i) => {
+ layouts.horizontal.push( ""+ i/10+ " 1 -1" )
+ layouts.vertical.push( "0 "+ i/10+ " -1" )
+
+ theta = i*360/elements.length
+ x = hOffset * Math.cos(theta*Math.PI/180)
+ y = hOffset * Math.sin(theta*Math.PI/180)
+ z = dOffset
+ layouts.circle.push( "" + x + " " + y + " " + z )
+
+ x = i/10 * hOffset * Math.cos(theta*Math.PI/180)
+ y = i/10 * hOffset * Math.sin(theta*Math.PI/180)
+ z = i/10 * dOffset
+ layouts.spiral.push( "" + x + " " + y + " " + z )
+
+ /*
+ layoutNames.map( ln =>
+ el.setAttribute("animation__"+ln,
+ "property: position; to: "
+ + layouts[ln].at(-1)
+ + "; dur: 1000; startEvents:startAnimation;"
+ )
+ )
+ */
+ //el.setAttribute("animation__horizontal", "property: position; to: " + layouts["horizontal"].at(-1) + "; dur: 1000; startEvents:startAnimation; autoplay:false;")
+ // el.setAttribute("animation__vertical", "property: position; to: " + layouts["vertical"].at(-1) + "; dur: 1000; startEvents:startAnimation; autoplay:false;")
+
+ })
+
+ return layouts
+}
+
+// consider passing another parameter with the set of code (and optional annotation) to populate that menu
+ /* e.g.
+ addContextualRings( r, [
+ 'jxr applyFunctionToSelection("changeColorToBlue")',
+ 'jxr applyFunctionToSelection("changeColorToWhite")',
+ 'jxr applyFunctionToSelection("changeColorToRed")',
+ 'jxr applyFunctionToSelection("changeColorToGreen")',
+ ])
+ */
+function addContextualRings( r ){
+
+ active_rings.setAttribute("visible", "true") // might be overwhelming in other contexts, e.g. ring tags
+ // would be better to have its own parent, unrelated to active_rings
+ // would also allow to show/hide entire set
+
+ let hOffset = .2 // horizontal offset
+ let dOffset = .3 // depth offset, i.e. closer to the person assuming they are facing the z axis
+ let contextualRings = []
+ let code = [
+ 'jxr applyFunctionToSelection("changeColorToBlue")',
+ 'jxr applyFunctionToSelection("changeColorToWhite")',
+ 'jxr applyFunctionToSelection("changeColorToRed")',
+ 'jxr applyFunctionToSelection("changeColorToGreen")',
+ ]
+
+ let spiral = true
+ spiral = false
+ if (spiral) code = "abcdefghijklmnopqrstuvwxyz ,.{}()[]".split('').reverse()
+ if (spiral) hOffset = 2.2 // for spiral testing
+ let layoutParts = []
+ slices = code.length;
+ // consider multiple loops
+ // advantage of spiral, namely dense without overlap
+ // and predictable, linear despite being in 2D
+ // repeat in N loops where each new loop start its radius beyond previous one
+ for (i=0;i
+ contextualRings.push( addRing(c, r.getAttribute("position").clone().add( layoutParts[i]) ) )
+ )
+ contextualRings.map( cr => cr.classList.add( "contextual_ring" ) )
+ if (spiral) contextualRings.map( cr => cr.setAttribute("color", "purple") ) // spiral testing
+ return contextualRings
+}
+
+function removeContextualRings(){
+ Array.from( document.querySelectorAll(".contextual_ring") ).map( el => el.remove())
+}
+
+function addOrUpdateTagToElement( el, tagValue = "tagged" ){
+ console.log('tagging with', window.currentTag.model )
+ //let hasTag = el.querySelector("a-gltf-model") // querySelector on [tag] doesnt work
+ let hasTag = el.querySelector("[tag='"+tagValue+"'") // querySelector on [tag] doesnt work
+ let tagEl
+ if (!hasTag) {
+ console.log('no tag found on', el, 'adding one' )
+ tagEl = document.createElement("a-gltf-model")
+ el.appendChild( tagEl )
+ tagEl.setAttribute("position", ".2 .2 0") // trying to be in the top right corner... but totally depends on the volume of the element
+ tagEl.setAttribute("src", window.currentTag.model)
+ // parenting surely messes up the scale!
+ // tagEl.setAttribute("scale", window.currentTag.scale)
+ //console.log('scale set', window.currentTag.scale)
+ //console.log('scale set', AFRAME.utils.coordinates.stringify( window.currentTag.scale) )
+ tagEl.setAttribute("scale", ".001 .001 .001" ) // somehow rotation works but not scale..?
+ // tagEl.setAttribute("rotation", window.currentTag.rotation)
+ tagEl.setAttribute("tag", tagValue)
+ } else {
+ console.log('tag found on', el, 'replacing it' )
+ el.querySelector("a-gltf-model").setAttribute("src", window.currentTag.model)
+ // undefined on tagEl?
+ }
+}
+
+function addKeyboardRings(){
+ // addRing(code, position)
+ return "abcdefghijklmnopqrstuvwxyz ,.{}()[]".split('').reverse().map( (c,i) => {
+ let x = (i%3)/10 +.5
+ let y = 1+(i/3)/10
+ let r = addRing(c, x+" "+y+ " -.5", .03, .005, .005)
+ r.classList.add('kbd_key')
+ return r
+ } )
+ // could probably be done once they repositioned instead via a well known parent with id (or unique selector)
+}
+
+function addRing(code, position="0 1.5 -.7", radius=.1, radiusTubular=.01, codeVerticalOffset=.15){
+ let el = document.createElement("a-torus")
+ el.setAttribute("position", position)
+ el.setAttribute("segments-radial", "4")
+ el.setAttribute("segments-tubular", "12")
+ el.setAttribute("opacity", ".3")
+ el.setAttribute("radius", radius)
+ el.setAttribute("radius-tubular", radiusTubular)
+ let elCode = document.createElement("a-troika-text")
+ // consider binding an existing jxr command to an existing ring to have a similar behavior
+ // e.g. drop jxr on ring, bind them so that next time an element goes through, it's applied via binded ring
+ elCode.setAttribute("anchor", "left")
+ elCode.setAttribute("target", "")
+ elCode.setAttribute("value", code)
+ elCode.setAttribute("position", ".0 "+codeVerticalOffset+" .0")
+ elCode.setAttribute("scale", "0.1 0.1 0.1")
+ el.appendChild( elCode )
+ active_rings.appendChild( el )
+ return el
+}
+
+function endRingCheck(){
+ console.log('rings used', ringsUsedInLastSession)
+
+ // TODO test
+ if (ringsUsedInLastSession.length > 1){
+ console.log('more than 1 ring used, preparing stack', ringsUsedInLastSession.length)
+ ringsUsedInLastSession.map( currentRing => {
+ let r = currentRing.ring
+ let value = r.querySelector("a-troika-text").getAttribute("value")
+ if (value.length>1) { // ignoring virtual keypresses
+ console.log('cloned ', r, ' on stack', rings_stack )
+ let clonedRing = r.cloneNode(true)
+ rings_stack.appendChild( clonedRing )
+ // they are really combined though, they should have a single parent entity with a target on it
+ // and a way to tag or name
+ // could be another ring
+ }
+ })
+ }
+
+ // keeping visually track of what has been executed so far
+ // ringsUsedInLastSession.map( r => r.ring.setAttribute("opacity", 1) )
+ // already used... should try another visual property
+ ringsUsedInLastSession.map( r => r.ring.setAttribute("wireframe", "false") )
+
+ ringsUsedInLastSession = [] // ending the session
+
+ // if used as contextual menu, reset/hide here
+ removeContextualRings()
+ ringAdded = false
+
+ clearInterval(ringCheckInterval)
+}
+
+let ringDialUpdateInterval = null
+function startRingDialUpdate(){
+ ringDialUpdateInterval = setInterval( _ => {
+ let el = selectedElements.at(-1)?.element
+ let z = el.getAttribute('rotation').z.toFixed(2)
+ // consider target range then normalize over it
+ // consider also acceptable input range, e.g. -30deg to +30deg, not 360 as it's ergonomically impossible
+ let value = el.querySelector('a-troika-text').getAttribute('value')
+ el.querySelector('a-troika-text').setAttribute('value', value.split('(')[0]+'('+z+')')
+
+ if ( el == ring_dial_color_r || el == ring_dial_color_g || el == ring_dial_color_b ){
+ let channels = ['r', 'g', 'b']
+ let colors = []
+ channels.map( c => {
+ colors.push( Math.round( Number( document.querySelector("#ring_dial_color_"+c).querySelector("a-troika-text").getAttribute("value").split("(")[1].split(")")[0] ) ).toString(16) )
+ if ( (colors.at(-1).includes("-")) || (colors.at(-1).length < 2)) colors[colors.length-1] = "00"
+ })
+
+ // TODO seems there is a problem in here,
+ let jxrValue = ring_dial_color.querySelector("a-troika-text").getAttribute("value").split("(")[0]
+ ring_dial_color.querySelector("a-troika-text").setAttribute("value", jxrValue+'("'+'#'+colors.join("")+'")' )
+ ring_dial_color.querySelector("a-troika-text").setAttribute("color", '#'+colors.join("") )
+ // maybe to here...
+
+ ring_dial_color.setAttribute("color", '#'+colors.join("") )
+ }
+ }, 100)
+}
+
+function endRingDialUpdate(){
+ clearInterval(ringDialUpdateInterval)
+
+ let el = selectedElements.at(-1)?.element
+ if (el) el.object3D.rotation.z = 0
+ // ring rotation reset so that value is readible after
+}
+
+// -----=========== end of ring system =============--------------------------------------------------------------------------------------------
+
+// --------------------------------------------------- skating specific ----------------------------------------------------
+
+let positions = []
+AFRAME.registerComponent('odometer',{
+ init: function () {
+ this.tick = AFRAME.utils.throttleTick(this.tick, 50, this);
+ },
+ tick: function () {
+ let el = this.el
+ let pos = document.getElementById('player').getAttribute('position').clone()
+ positions.push( pos )
+ if (positions.length > 20) {
+ let dist = pos.distanceTo( positions[positions.length-20] )
+ //console.log(dist) // could be done via setFeedbackHUD() instead
+ el.setAttribute('value', dist.toFixed(2))
+ }
+}})
+
+function prepareCloneAndReposition(){
+ window.startingElementValues = selectedElements.at(-1).element.cloneNode(true)
+}
+
+const classNameItemsToSave = "itemsclonedtosave"
+
+function cloneAndReposition(){
+ let lastElement = selectedElements.at(-1).element
+ let cloned = lastElement.cloneNode(true)
+ cloned.classList.add( classNameItemsToSave )
+ AFRAME.scenes[0].appendChild( cloned )
+ lastElement = window.startingElementValues
+ // not tested deeply
+}
+
+const itemsfile = urlParams.get('itemsfile');
+if ( itemsfile ) setTimeout( _ => loadItemsClonedViaWebDAV( itemsfile ) , 1000 )
+// should do so only after scene has loaded
+
+function loadItemsClonedViaWebDAV(filename){
+ fetch('../content/'+filename).then( r => r.json() ).then( r => {
+ // fetch('https://companion.benetou.fr/'+filename).then( r => r.json() ).then( r => {
+ // fails due to CORS...
+ r.map( e => {
+ let cloned = document.getElementById(e.templating_id).cloneNode(true)
+ cloned.classList.add( classNameItemsToSave )
+ cloned.setAttribute("position", e.position)
+ cloned.setAttribute("rotation", e.rotation)
+ AFRAME.scenes[0].appendChild( cloned )
+ })
+ })
+}
+
+function saveItemsClonedViaWebDAV(classtosave=classNameItemsToSave){
+ // should prevent from saving multiples times a second (due to pinching)
+
+ // let data = Array.from( document.querySelectorAll("."+classtosave) ).map( i => { return {nodeName:i.nodeName, attributes:i.attributes} } ) // somehow empty attributes... or rather plenty of values, e.g 10, but empty
+ let data = Array.from( document.querySelectorAll("."+classtosave) ).map( i => { return {templating_id:i.id, position:i.getAttribute("position"), rotation:i.getAttribute("rotation"), } } )
+ // somehow saves the templating element too rather than the first cloned element
+
+ // assuming other properties are important here because they can't be modified by the user
+
+ // from saveHighlights()
+ let filename = "gameitems_"+Date.now()+".json"
+ async function w(path = "/file.txt"){ return await webdavClient.putFileContents(path, JSON.stringify(data)); }
+ // note that this only single page saving, should instead consider highlightsBetweenPageChanges but after dedup
+ written = w(subdirWebDAV+usernamePrefix+filename)
+ if (written){
+ fetch('https://ntfy.benetou.fr/fileuploadtowebdav', { method: 'POST', body: 'added '+usernamePrefix+filename })
+ }
+
+ let url = '?itemsfile='+usernamePrefix+filename
+
+ setTimeout( _ => window.open(url, '_blank'), 1000 )
+
+ return usernamePrefix+filename
+}
+
+// --------------------------------------------------- end of skating specific ----------------------------------------------------
+
+// --------------------------------------------------- in-XR JSON editing ----------------------------------------------------
+
+function wordsToNotes( words ){
+console.log(words)
+ words.split(' ').map( (w,i) => {
+ let el = addNewNote( w, "-.5 "+ (1+i/10) +" -.5" )
+ el.setAttribute("onreleased", "setFeedbackHUD( selectedElements.at(-1).element.getAttribute('value'))" )
+ })
+}
+
+// default test URL
+function editJSON( url = 'https://companion.benetou.fr/demo_q1.json' ){
+ fetch( url ).then( res => res.json() ).then( res => {
+ keys = []
+ findKeys ( res )
+
+ let content = []
+ keys.map( (k,i) => { t = getValueByDottedKeys( res, k )
+ if (typeof t == "string" && t.length > 42) t = t.substring(0, 42)+"..."
+ let line = k + " = " + t.replaceAll('\n','')
+ content.push( line )
+ let y = 2-i/50
+ let el = addNewNote( line, "0 "+ y +" -1" )
+ let range = {}
+ range[0] = 0xffffff
+ range[k.length + " = ".length ] = 0x0099ff
+ // range[end] = 0xffffff
+ el.setAttribute("troika-text", {colorRanges: range})
+ // el.setAttribute("onreleased", "setFeedbackHUD( selectedElements.at(-1).element.getAttribute('value'))" )
+ // could add as selection JSON property to replace
+ // includes everything, should keep only the property name with path
+
+ // could have recursive precision edition depth
+ // i.e. edit selection or word in it, or character in word
+ el.setAttribute("onreleased", "wordsToNotes(selectedElements.at(-1).element.getAttribute('value'))" )
+ } )
+ })
+
+ var keys = []
+ const findKeys = (object, prevKey = '') => {
+ Object.keys(object).forEach((key) => {
+ const nestedKey = prevKey === '' ? key : `${prevKey}.${key}`
+
+ if (typeof object[key] !== 'object') return keys.push(nestedKey)
+
+ findKeys(object[key], nestedKey)
+ })
+ }
+ // from https://stackoverflow.com/a/65345406/1442164
+
+ const getValueByDottedKeys = (obj, strKey)=>{
+ let keys = strKey.split(".")
+ let value = obj[keys[0]];
+ for(let i=1;i {
+ // should check if lastPick is including in vf.snappedOn then remove it
+ })
+}
+
+function endVolumetricFrameMoving(){
+ // should stop moving all associating volumetric frames
+}
+
+function startVolumetricFrameMoving(){
+ // should move all associating volumetric frames
+}
+
+// --------------------------------------------------- end of volumetric frames ----------------------------------------------------
+
+async function getMostRecentFile(partialfilename=''){
+ const contents = await webdavClient.getDirectoryContents(subdirWebDAV);
+ // consider instead search https://github.com/perry-mitchell/webdav-client#search
+ return contents.filter(f => f.basename.includes(partialfilename))
+ .filter(f=>f.type=="file")
+ .sort( (a,b) => new Date(a.lastmod).getTime() < new Date(b.lastmod).getTime() ) // newest first
+}
+// once working could update addRecentAudioFiles() accordingly (even though takes last 4 for now)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/data/interactions/onreleased/color_change.js b/data/interactions/onreleased/color_change.js
new file mode 100755
index 0000000..f461b1e
--- /dev/null
+++ b/data/interactions/onreleased/color_change.js
@@ -0,0 +1,9 @@
+// inspired by http://expressjs.com/en/guide/using-middleware.html
+
+function colorChange( el ){
+ if (false) el.setAttribute("color", "lime")
+ // could test to filter more, e.g.
+ // if ( el.getAttribute("value")?.includes("change my color to red") )
+}
+
+sequentialFiltersInteractionOnReleased.push( colorChange )
diff --git a/data/manuscript.txt b/data/manuscript.txt
new file mode 100644
index 0000000..87bec79
--- /dev/null
+++ b/data/manuscript.txt
@@ -0,0 +1 @@
+There is a lot of attention rightly focused on the many aspects of Artificial Intelligence already in use and being developed. From generative AI for text, images and 3D to AI for image and speech recognition and AI for analysis and so on, the impact on how we learn, think and communicate has already been fundamental. There is also another tech revolution going on and so far the attention paid to it is primarily that as a gaming , social and media consumption tech; that of Virtual and Augmented Reality (VR & AR), as most prominently on the market as the Meta Quest and the Apple Vision Pro.
\ No newline at end of file
diff --git a/data/references_manual_v04.json b/data/references_manual_v04.json
deleted file mode 100644
index 2e9d26e..0000000
--- a/data/references_manual_v04.json
+++ /dev/null
@@ -1,695 +0,0 @@
-{
- "data-objects": [
- {
- "object-id": "alexander:1978:apl",
- "object-type": "reference data",
- "bibtex-type": "@book",
- "bibtex-data": {
- "citeKey": "alexander:1978:apl",
- "author": [
- "Alexander, Christopher"
- ],
- "title": "A Pattern Language: Towns, Buildings, Construction",
- "volume": "",
- "pages": "1216",
- "editor": [
- ""
- ],
- "publisher": "OUP USA",
- "address": "",
- "year": "1978",
- "doi": "",
- "isbn": "0195019199",
- "keywords": [
- "design patterns"
- ],
- "note": "Webtwo ipsum etsy lanyrd meevee glogster, joyent kno. Sifteo etsy waze odeo, kazaa appjet.",
- "annote": "",
- "source-url": "https://global.oup.com/academic/product/a-pattern-language-9780195019193",
- "source-pdf": "",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "false"
- }
- },
- {
- "object-id": "atzenbeck:2018:mia",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "atzenbeck:2018:mia",
- "author": [
- "Atzenbeck, Clau",
- "Roßner, Daniel",
- "Tzagarakis, Manolis"
- ],
- "title": "Mother: An Integrated Approach to Hypertext Domains",
- "booktitle": "Proceedings of the 29th on Hypertext and Social Media",
- "publisher": "ACM",
- "address": "New York, NY, USA",
- "pages": "145–149",
- "year": "2018",
- "doi": "10.1145/3209542.3209570",
- "isbn": "978-1-4503-5427-1",
- "location": "Baltimore, MD",
- "keywords": [
- "asgard",
- "cb-ohs",
- "hel",
- "midgard",
- "hypertext infrastructure",
- "mother",
- "navigational hypertext",
- "open hypermedia systems"
- ],
- "note": "Appjet omgpop babblely heroku zillow, zapier yammer. Scribd woopra flickr shopify qeyno hojoki wikia, chegg udemy oooj kno.",
- "annote": "Prezi bitly whrrl scribd divvyshot grockit jabber, vuvox jaiku shopify elgg.",
- "source-url": "",
- "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/3209542.3209570",
- "open-access": "true",
- "free-acm-access": "true"
- }
- },
- {
- "object-id": "atzenbeck:2019:ham",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "atzenbeck:2019:ham",
- "author": [
- "Atzenbeck, Claus",
- "Nürnberg, Peter J."
- ],
- "title": "Hypertext as Method",
- "booktitle": "Proceedings of the 30th ACM Conference on Hypertext and Social Media",
- "publisher": "Association for Computing Machinery",
- "address": "New York, NY, USA",
- "pages": "29–38",
- "year": "2019",
- "doi": "10.1145/3342220.3343669",
- "isbn": "9781450368858",
- "location": "Hof, DE",
- "keywords": [
- "research communities",
- "structures",
- "infrastructure",
- "ai",
- "intelligence",
- "hypertext",
- "man-machine",
- "hypertext history",
- "augmentation",
- "intellect",
- "context"
- ],
- "note": "",
- "annote": "Zapier chegg elgg stypi yoono elgg wikia zooomr yuntaa, movity xobni loopt stypi lanyrd foodzie ngmoco. kazaa kiko foodzie.",
- "source-url": "https://dl.acm.org/doi/10.1145/3342220.3343669",
- "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/3342220.3343669",
- "source-html": "",
- "open-access": "true",
- "free-acm-access": "true"
- }
- },
- {
- "object-id": "atzenbeck:2023:btr",
- "object-type": "@reference data",
- "bibtex-type": "article",
- "bibtex-data": {
- "citeKey": "atzenbeck:2023:btr",
- "author": [
- "Atzenbeck, Claus",
- "Herder, Eelco",
- "Roßner, Daniel"
- ],
- "title": "Breaking The Routine: Spatial Hypertext Concepts for Active Decision Making in Recommender Systems",
- "journal": "New Review of Hypermedia and Multimedia",
- "volume": "29",
- "number": "",
- "pages": "1–35",
- "year": "2023",
- "location": "Milton Park, UK",
- "doi": "10.1080/13614568.2023.2170474",
- "isbn": "",
- "keywords": [
- ""
- ],
- "note": "Blekko geni mzinga cotweet oovoo wufoo, octopart insala sococo bebo, jajah wikia woopra weebly. Bubbli chegg chumby kaboodle blekko, zappos zinch woopra. imeem.\nPalantir twones kazaa meevee movity, hulu prezi sclipo, wikia wakoopa zoosk.",
- "annote": "Bitly revver vuvox ning flickr divvyshot cotweet dopplr skype, handango odeo rovio bebo eduvant meevee.",
- "source-url": "https://www.tandfonline.com/doi/full/10.1080/13614568.2023.2170474",
- "source-pdf": "",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "false"
- }
- },
- {
- "object-id": "bernstein:1991:storyspace",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "bernstein:1991:storyspace",
- "author": [
- "Bernstein, Mark"
- ],
- "title": "Storyspace: Hypertext and the Process of Writing",
- "booktitle": "Hypertext/Hypermedia Handbook",
- "editor": [
- "Berk, Emily",
- "Devlin, Joseph"
- ],
- "publisher": "McGraw-Hill Inc.,US",
- "address": "",
- "pages": "529–533",
- "year": "1991",
- "doi": "",
- "isbn": "0070166226",
- "keywords": [
- "hypertext; storyspace"
- ],
- "note": "",
- "annote": "Wikia zlio imeem zanga jumo sifteo, divvyshot nuvvo ideeli.",
- "source-url": "",
- "source-pdf": "",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "false"
- }
- },
- {
- "object-id": "bernstein:1998:poh",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "bernstein:1998:poh",
- "author": [
- "Bernstein, Mark"
- ],
- "title": "Patterns of Hypertext",
- "booktitle": "Proceedings of the Ninth ACM Conference on Hypertext and Hypermedia : Links, Objects, Time and Space—Structure in Hypermedia Systems",
- "publisher": "ACM",
- "address": "New York, NY, USA",
- "pages": "21–29",
- "year": "1998",
- "doi": "10.1145/276627.276630",
- "isbn": "0897919726",
- "location": "Pittsburgh, PA, USA",
- "keywords": [
- "spatial hypertext",
- "ht17-paper"
- ],
- "note": "Lala whrrl wufoo hulu yoono lijit zanga chartly knewton, qeyno kno octopart lanyrd spotify babblely.",
- "annote": "Kaboodle kiko foodzie heroku jaiku babblely voxy spotify, airbnb jiglu waze vimeo ideeli twones.",
- "source-url": "https://dl.acm.org/doi/10.1145/276627.276630",
- "source-pdf": "https://dl.acm.org/doi/pdf10.1145/276627.276630",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "true"
- }
- },
- {
- "object-id": "bernstein:2010:csahr",
- "object-type": "reference data",
- "bibtex-type": "@online",
- "bibtex-data": {
- "citeKey": "bernstein:2010:csahr",
- "author": [
- "Bernstein, Mark"
- ],
- "year": "2010",
- "title": "Card Sharks and Holy Scrollers",
- "organization": "markbernstein.org",
- "url": "https://www.markbernstein.org/Oct10/CardSharksandHolyScrollers.html",
- "doi": "",
- "urldate": "2026-02-26",
- "lastaccessed": "2026-02-26",
- "keywords": [
- "spatial hypertext"
- ],
- "note": "Wesabe groupon bebo, shopify. Dropio cloudera empressr rovio jajah lanyrd, oovoo boxbe meebo.",
- "annote": "Babblely doostang yammer ifttt etsy insala.",
- "source-url": "https://www.markbernstein.org/Oct10/CardSharksandHolyScrollers.html",
- "source-pdf": "",
- "source-html": "https://www.markbernstein.org/Oct10/CardSharksandHolyScrollers.html",
- "open-access": "false",
- "free-acm-access": "false"
- }
- },
- {
- "object-id": "bernstein:2011:tash",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "bernstein:2011:tash",
- "author": [
- "Bernstein, Mark"
- ],
- "title": "Can We Talk About Spatial Hypertext?",
- "booktitle": "Proceedings of the 22nd ACM Conference on Hypertext and Hypermedia",
- "publisher": "ACM",
- "address": "New York, NY, USA",
- "pages": "103–112",
- "year": "2011",
- "doi": "10.1145/1995966.1995983",
- "isbn": "1450302564",
- "location": "Eindhoven, NL",
- "keywords": [
- "spatial hypertext",
- "ht17-paper"
- ],
- "note": "",
- "annote": "Wufoo odeo voki airbnb voki wufoo, weebly bebo scribd. voxy groupon. Wikia zlio imeem zanga jumo sifteo, divvyshot nuvvo ideeli.",
- "source-url": "https://dl.acm.org/doi/10.1145/1995966.1995983",
- "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/1995966.1995983",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "true"
- }
- },
- {
- "object-id": "hseih:2009:svpssh",
- "object-type": "reference data",
- "bibtex-type": "@article",
- "bibtex-data": {
- "citeKey": "hseih:2009:svpssh",
- "author": [
- "Hsieh, Haowei",
- "Shipman, III, Frank M."
- ],
- "title": "Supporting Visual Problem Solving in Spatial Hypertext",
- "journal": "Journal of Digital Information (JoDI)",
- "volume": "10",
- "number": "3",
- "pages": "",
- "year": "2009",
- "location": "",
- "doi": "",
- "isbn": "",
- "keywords": [
- "spatial hypertext"
- ],
- "note": "Tivo babblely plugg heekya joukuu groupon vuvox, edmodo octopart joyent kazaa heekya.\nGlogster divvyshot convore zimbra, orkut zlio, movity zooomr.",
- "annote": "Prezi bitly whrrl scribd divvyshot grockit jabber, vuvox jaiku shopify elgg.",
- "source-url": "https://journals.tdl.org/jodi/index.php/jodi/article/view/173",
- "source-pdf": "",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "false"
- }
- },
- {
- "object-id": "joyce:1988:ss",
- "object-type": "reference data",
- "bibtex-type": "@article",
- "bibtex-data": {
- "citeKey": "joyce:1988:ss",
- "author": [
- "Joyce, Michael"
- ],
- "title": "Siren Shapes",
- "journal": "Academic Computing",
- "volume": "3",
- "number": "4",
- "pages": "10–14, 37",
- "year": "1988",
- "location": "McKinney, TX, USA",
- "doi": "",
- "isbn": "",
- "keywords": [
- "storyspace",
- "hypertext"
- ],
- "note": "lala plickers dropio. Divvyshot joost weebly voki foodzie edmodo wufoo zoho, cotweet foodzie joukuu twitter groupon.",
- "annote": "Ebay weebly chumby gooru unigo, chegg vimeo.",
- "source-url": "",
- "source-pdf": "",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "false"
- }
- },
- {
- "object-id": "marshall:1987:erpuh",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "marshall:1987:erpuh",
- "author": [
- "Marshall, Catherine C."
- ],
- "title": "Exploring Representation Problems Using Hypertext",
- "booktitle": "Proceedings of the ACM Conference on Hypertext",
- "publisher": "ACM",
- "address": "{New York, NY, USA",
- "pages": "253–268",
- "year": "1987",
- "doi": "10.1145/317426.317445",
- "isbn": "089791340X",
- "location": "",
- "keywords": [
- "spatial hypertext",
- "notecards"
- ],
- "note": "Scribd tivo xobni tivo udemy, glogster wufoo plugg chumby appjet, oooj sifteo etsy. babblely etsy sococo.\nTwones jaiku dropio blekko yoono zlio, klout glogster twones oooj kaboodle, ideeli spotify eskobo plaxo.",
- "annote": "Scribd woopra flickr shopify qeyno hojoki .",
- "source-url": "https://dl.acm.org/doi/10.1145/317426.317445",
- "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/317426.317445",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "true"
- }
- },
- {
- "object-id": "marshall:1994:viki",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "marshall:1994:viki",
- "author": [
- "Marshall, Catherine C.",
- "Shipman, III, Frank M.",
- "Coombs, James H."
- ],
- "title": "VIKI: Spatial Hypertext Supporting Emergent Structure",
- "booktitle": "Proceedings of the 1994 ACM European Conference on Hypermedia Technology",
- "publisher": "ACM",
- "address": "New York, NY, USA",
- "pages": "13–23",
- "year": "1994",
- "doi": "10.1145/192757.192759",
- "isbn": "0897916409",
- "location": "Edinburgh, UK",
- "keywords": [
- "spatial hypertext",
- "viki"
- ],
- "note": "Meebo sococo zynga blekko orkut wakoopa joukuu, wikia jibjab stypi heekya.",
- "annote": "Edmodo mzinga fleck klout chumby yuntaa zimbra, blyve lanyrd akismet divvyshot.",
- "source-url": "https://dl.acm.org/doi/10.1145/317426.317445",
- "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/317426.317445",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "true"
- }
- },
- {
- "object-id": "marshall:1995:shdfc",
- "object-type": "reference data",
- "bibtex-type": "@article",
- "bibtex-data": {
- "citeKey": "marshall:1995:shdfc",
- "author": [
- "Marshall, Catherine C.",
- "Shipman, III, Frank M."
- ],
- "title": "Spatial Hypertext: Designing for Change",
- "journal": "Communications of the ACM (CACM)",
- "volume": "38",
- "number": "8",
- "pages": "88–97",
- "year": "1995",
- "location": "",
- "doi": "10.1145/208344.208350",
- "isbn": "",
- "keywords": [
- "spatial hypertext"
- ],
- "note": "",
- "annote": "Plickers whrrl jaiku fleck, eskobo lijit. Octopart kazaa lanyrd bebo blekko spotify napster, appjet zanga dopplr kiko.",
- "source-url": "https://dl.acm.org/doi/10.1145/208344.208350",
- "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/208344.208350",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "true"
- }
- },
- {
- "object-id": "marshall:1997:shpit",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "marshall:1997:shpit",
- "author": [
- "Marshall, Catherine C.",
- "Shipman, III, Frank M."
- ],
- "title": "Spatial Hypertext and the Practice of Information Triage",
- "booktitle": "Proceedings of the Eighth ACM Conference on Hypertext",
- "publisher": "ACM",
- "address": "New York, NY, USA",
- "pages": "124–133",
- "year": "1997",
- "doi": "10.1145/267437.267451",
- "isbn": "0897918665",
- "location": "",
- "keywords": [
- "spatial hypertext"
- ],
- "note": "Imvu spotify rovio dopplr woopra, jajah divvyshot plugg, diigo wesabe ebay.",
- "annote": "Glogster tivo revver, jiglu.",
- "source-url": "https://dl.acm.org/doi/10.1145/267437.267451",
- "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/267437.267451",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "true"
- }
- },
- {
- "object-id": "nakakoji:2002:psshw",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "nakakoji:2002:psshw",
- "author": [
- "Nakakoji, Kumiyo",
- "Yamamoto, Yasuhiro"
- ],
- "title": "Position Statement for Spatial Hypertext Workshop at Hypertext 2002",
- "booktitle": "Second Workshop on Spatial Hypertext",
- "publisher": "ACM",
- "address": "College Park, Maryland",
- "pages": "2",
- "year": "2002",
- "doi": "",
- "isbn": "",
- "location": "",
- "keywords": [
- "spatial hypertext"
- ],
- "note": "",
- "annote": "Mog joost gsnap zooomr zynga gsnap cuil trulia, meevee handango hulu reddit odeo bubbli.",
- "source-url": "https://web.archive.org/web/20030923074942/http://www.csdl.tamu.edu/~shipman/SpatialHypertext/SH2/nakakoji.pdf",
- "source-pdf": "https://web.archive.org/web/20030923074942/http://www.csdl.tamu.edu/~shipman/SpatialHypertext/SH2/nakakoji.pdf",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "false"
- }
- },
- {
- "object-id": "rossner:2023:spore",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "rossner:2023:spore",
- "author": [
- "Roßner, Daniel",
- "Atzenbeck, Claus",
- "Brooker, Sam"
- ],
- "title": "SPORE: A Storybreaking Machine",
- "booktitle": "Proceedings of the 34th ACM Conference on Hypertext and Social Media",
- "publisher": "Association for Computing Machinery",
- "address": "New York, NY, USA",
- "pages": "",
- "year": "2023",
- "doi": "10.1145/3603163.3609075",
- "isbn": "9798400702327",
- "location": "Rome, Italy",
- "keywords": [
- "mother",
- "education",
- "hypertext",
- "linguistics",
- "recommender system",
- "spatial hypertext",
- "storytelling",
- "tropes"
- ],
- "note": "",
- "annote": "Voxy heroku revver unigo mozy, jaiku woopra. Udemy zoho tivo, divvyshot.",
- "source-url": "https://dl.acm.org/doi/10.1145/3603163.3609075",
- "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/3603163.3609075",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "false"
- }
- },
- {
- "object-id": "shipman:1999:spatial",
- "object-type": "reference data",
- "bibtex-type": "@article",
- "bibtex-data": {
- "citeKey": "shipman:1999:spatial",
- "author": [
- "Shipman, III, Frank M.",
- "Marshall, Catherine C"
- ],
- "title": "Spatial Hypertext: An Alternative to Navigational and Semantic Links",
- "journal": "ACM Computing Surveys (CSUR)",
- "volume": "31",
- "number": "4es",
- "pages": "14",
- "year": "1999",
- "location": "",
- "doi": "10.1145/345966.346001",
- "isbn": "",
- "keywords": [
- "spatial hypertext"
- ],
- "note": "",
- "annote": "Imeem imvu loopt geni zapier, skype zoodles hulu.",
- "source-url": "https://dl.acm.org/doi/10.1145/345966.346001",
- "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/345966.346001",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "true"
- }
- },
- {
- "object-id": "shipman:2001:sdshr",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "shipman:2001:sdshr",
- "author": [
- "Shipman, III, Frank M."
- ],
- "title": "Seven Directions for Spatial Hypertext Research",
- "booktitle": "First International Workshop on Spatial Hypertext",
- "publisher": "",
- "address": "",
- "pages": "",
- "year": "2001",
- "doi": "",
- "isbn": "",
- "location": "",
- "keywords": [
- "spatial hypertext"
- ],
- "note": "Orkut flickr squidoo blyve yuntaa imeem, chegg yammer fleck reddit.",
- "annote": "Ngmoco blekko shopify, kno.",
- "source-url": "https://web.archive.org/web/20041128133432/http://www.csdl.tamu.edu/~shipman/SpatialHypertext/SH1/shipman.pdf",
- "source-pdf": "https://web.archive.org/web/20041128133432/http://www.csdl.tamu.edu/~shipman/SpatialHypertext/SH1/shipman.pdf",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "false"
- }
- },
- {
- "object-id": "shipman:2001:vkb",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "shipman:2001:vkb",
- "author": [
- "Shipman, III, Frank M.",
- "Hsieh, Haowei",
- "Maloor, Preetam",
- " Moore, J. Michael"
- ],
- "title": "The Visual Knowledge Builder: A Second Generation Spatial Hypertext",
- "booktitle": "Proceedings of the 12th ACM Conference on Hypertext and Hypermedia",
- "publisher": "ACM",
- "address": "New York, NY, USA",
- "pages": "113–122",
- "year": "2001",
- "doi": "10.1145/504216.504245",
- "isbn": "1581134207",
- "location": "",
- "keywords": [
- "spatial hypertext",
- "vkb"
- ],
- "note": "",
- "annote": "Vuvox jibjab hojoki geni odeo, balihoo twones kippt koofers odeo, convore woopra klout.",
- "source-url": "https://dl.acm.org/doi/10.1145/504216.504245",
- "source-pdf": "https://dl.acm.org/doi/10.1145/504216.504245",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "true"
- }
- },
- {
- "object-id": "shipman:2002:sh",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "shipman:2002:sh",
- "author": [
- "Shipman, III, Frank M.",
- "Moore, J. Michael",
- "Maloor, Preetam",
- "Hsieh, Haowei",
- "Akkapeddi, Raghu"
- ],
- "title": "Semantics Happen: Knowledge Building in Spatial Hypertext",
- "booktitle": "Proceedings of the Thirteenth ACM Conference on Hypertext and Hypermedia",
- "publisher": "ACM",
- "address": "",
- "pages": "25–34",
- "year": "2002",
- "doi": "10.1145/513338.513350",
- "isbn": "1581134770",
- "location": "",
- "keywords": [
- "spatial hypertext"
- ],
- "note": "",
- "annote": "Hojoki jibjab movity qeyno lijit, flickr ideeli.",
- "source-url": "https://dl.acm.org/doi/10.1145/513338.513350",
- "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/513338.513350",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "false"
- }
- },
- {
- "object-id": "yip:2020:dash",
- "object-type": "reference data",
- "bibtex-type": "@inproceedings",
- "bibtex-data": {
- "citeKey": "yip:2020:dash",
- "author": [
- "Yip, Stanley",
- "Zeleznik, Bob",
- "Wilkins, Samuel",
- "Schicke, Tyler",
- "van Dam, Andries"
- ],
- "title": "Dash: A Hyper Framework",
- "booktitle": "Proceedings of the 31st ACM Conference on Hypertext and Social Media",
- "publisher": "Association for Computing Machinery",
- "address": "New York, NY, USA Virtual Event, USA",
- "pages": "237–238",
- "year": "2020",
- "doi": "10.1145/3372923.3404807",
- "isbn": "9781450370981",
- "location": "",
- "keywords": [
- "document engineering",
- "hypertext",
- "hypermedia",
- "collaborative editing",
- "workflow support"
- ],
- "note": "",
- "annote": "Zapier chegg elgg stypi yoono elgg wikia zooomr yuntaa, movity xobni loopt stypi lanyrd foodzie ngmoco.",
- "source-url": "https://doi.org/10.1145/3372923.3404807",
- "source-pdf": "https://doi.org/pdf/10.1145/3372923.3404807",
- "source-html": "",
- "open-access": "false",
- "free-acm-access": "false"
- }
- }
- ]
-}
diff --git a/data/references_manual_v14.json b/data/references_manual_v14.json
new file mode 100644
index 0000000..a3c9c64
--- /dev/null
+++ b/data/references_manual_v14.json
@@ -0,0 +1,838 @@
+{
+ "version": "12",
+ "date": "2025-05-21T18:18Z",
+ "data-objects": [
+ {
+ "object-id": "alexander:1978:apl",
+ "object-type": "reference data",
+ "bibtex-type": "@book",
+ "bibtex-data": {
+ "citeKey": "alexander:1978:apl",
+ "author": [
+ "Alexander, Christopher"
+ ],
+ "title": "A Pattern Language: Towns, Buildings, Construction",
+ "volume": "",
+ "pages": "1216",
+ "editor": [
+ ""
+ ],
+ "publisher": "OUP USA",
+ "address": "",
+ "year": "1978",
+ "doi": "",
+ "isbn": "0195019199",
+ "keywords": [
+ "design patterns"
+ ]
+ },
+ "note": "Webtwo ipsum etsy lanyrd meevee glogster, joyent kno. Sifteo etsy waze odeo, kazaa appjet.",
+ "annote": "",
+ "cover-image-url": "https://upload.wikimedia.org/wikipedia/en/thumb/e/e6/A_Pattern_Language.jpg/220px-A_Pattern_Language.jpg",
+ "source-url": "https://global.oup.com/academic/product/a-pattern-language-9780195019193",
+ "source-pdf": "",
+ "source-pdf-text": false,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "false",
+ "cross-references": []
+ },
+ {
+ "object-id": "anderson:2023:sh",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings ",
+ "bibtex-data": {
+ "citeKey": "anderson:2023:sh",
+ "author": [
+ "Anderson, Mark W. R.",
+ "Millard, David E."
+ ],
+ "title": "Seven Hypertexts",
+ "booktitle": "Proceedings of the 34th ACM Conference on Hypertext and Social Media",
+ "publisher": "Association for Computing Machinery",
+ "address": "New York, NY, USA",
+ "pages": "42: 1--157",
+ "year": "2023",
+ "doi": "3603163.3609048",
+ "isbn": "9798400702327",
+ "location": "Rome, Italy",
+ "keywords": [
+ "ai",
+ "ar",
+ "vr",
+ "xr",
+ "addressing",
+ "citation",
+ "data",
+ "documents",
+ "exploration",
+ "hypertext",
+ "linkbases",
+ "machine reading",
+ "metadata",
+ "narrative",
+ "provenance",
+ "remediation",
+ "stand-off metadata",
+ "viewspecs",
+ "visualisation"
+ ]
+ },
+ "note": "",
+ "annote": "store annotation text here",
+ "source-url": "https://dl.acm.org/doi/10.1145/3603163.3609048",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/3603163.3609048",
+ "source-pdf-text": true,
+ "source-html": "https://dl.acm.org/doi/fullHtml/10.1145/10.1145/3603163.3609048",
+ "open-access": "false",
+ "free-acm-access": "falsee",
+ "cross-references": ["atzenbeck:2018:mia","atzenbeck:2019:ham","atzenbeck:2023:btr","bernstein:1998:poh","marshall:1994:viki"]
+ },
+ {
+ "object-id": "anderson:2024:beyond",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings ",
+ "bibtex-data": {
+ "citeKey": "anderson:2024:beyond",
+ "author": [
+ "Anderson, Mark W. R."
+ ],
+ "title": "Beyond The Page-Break: Towards Better Tools for Remediation of Born-Digital Documents",
+ "booktitle": "Proceedings of the 35th ACM Conference on Hypertext and Social Media",
+ "publisher": "Association for Computing Machinery",
+ "address": "New York, NY, USA",
+ "pages": "70–77",
+ "year": "2024",
+ "doi": "10.1145/3648188.3678215",
+ "isbn": "9798400705953",
+ "location": "Poznan, Poland",
+ "keywords": [
+ "ai",
+ "ar",
+ "vr",
+ "xr",
+ "addressing",
+ "citation",
+ "data",
+ "documents",
+ "exploration",
+ "hypertext",
+ "linkbases",
+ "machine reading",
+ "metadata",
+ "narrative",
+ "provenance",
+ "remediation",
+ "stand-off metadata",
+ "viewspecs",
+ "visualisation"
+ ]
+ },
+ "note": "",
+ "annote": "If all the trees were paper\n and all the seas were ink\nand all the trees were bread and cheese\nwhat would we have to drink?.",
+ "source-url": "https://dl.acm.org/doi/10.1145/3648188.3678215",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/3648188.3678215",
+ "source-pdf-text": true,
+ "source-html": "https://dl.acm.org/doi/fullHtml/10.1145/3648188.3678215",
+ "open-access": "true",
+ "free-acm-access": "false",
+ "cross-references": ["atzenbeck:2018:mia","atzenbeck:2023:btr","bernstein:1998:poh","bernstein:2011:tash","joyce:1988:ss","marshall:1994:viki","shipman:2001:vkb"]
+ },
+ {
+ "object-id": "atzenbeck:2018:mia",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings",
+ "bibtex-data": {
+ "citeKey": "atzenbeck:2018:mia",
+ "author": [
+ "Atzenbeck, Claus",
+ "Roßner, Daniel",
+ "Tzagarakis, Manolis"
+ ],
+ "title": "Mother: An Integrated Approach to Hypertext Domains",
+ "booktitle": "Proceedings of the 29th Conference on Hypertext and Social Media",
+ "publisher": "ACM",
+ "address": "New York, NY, USA",
+ "pages": "145–149",
+ "year": "2018",
+ "doi": "10.1145/3209542.3209570",
+ "isbn": "978-1-4503-5427-1",
+ "location": "Baltimore, MD, USA",
+ "keywords": [
+ "asgard",
+ "cb-ohs",
+ "hel",
+ "midgard",
+ "hypertext infrastructure",
+ "mother",
+ "navigational hypertext",
+ "open hypermedia systems"
+ ]
+ },
+ "note": "Appjet omgpop babblely heroku zillow, zapier yammer. Scribd woopra flickr shopify qeyno hojoki wikia, chegg udemy oooj kno.",
+ "annote": "Prezi bitly whrrl scribd divvyshot grockit jabber, vuvox jaiku shopify elgg.",
+ "source-url": "https://dl.acm.org/doi/10.1145/3209542.3209570",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/3209542.3209570",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "true",
+ "free-acm-access": "true",
+ "cross-references": ["bernstein:1998:poh","marshall:1994:viki"]
+ },
+ {
+ "object-id": "atzenbeck:2019:ham",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings",
+ "bibtex-data": {
+ "citeKey": "atzenbeck:2019:ham",
+ "author": [
+ "Atzenbeck, Claus",
+ "Nürnberg, Peter J."
+ ],
+ "title": "Hypertext as Method",
+ "booktitle": "Proceedings of the 30th ACM Conference on Hypertext and Social Media",
+ "publisher": "Association for Computing Machinery",
+ "address": "New York, NY, USA",
+ "pages": "29–38",
+ "year": "2019",
+ "doi": "10.1145/3342220.3343669",
+ "isbn": "9781450368858",
+ "location": "Hof, Germany",
+ "keywords": [
+ "research communities",
+ "structures",
+ "infrastructure",
+ "ai",
+ "intelligence",
+ "hypertext",
+ "man-machine",
+ "hypertext history",
+ "augmentation",
+ "intellect",
+ "context"
+ ]
+ },
+ "note": "",
+ "annote": "Zapier chegg elgg stypi yoono elgg wikia zooomr yuntaa, movity xobni loopt stypi lanyrd foodzie ngmoco. kazaa kiko foodzie.",
+ "source-url": "https://dl.acm.org/doi/10.1145/3342220.3343669",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/3342220.3343669",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "true",
+ "free-acm-access": "true",
+ "cross-references": ["atzenbeck:2018:mia"]
+ },
+ {
+ "object-id": "atzenbeck:2023:btr",
+ "object-type": "reference data",
+ "bibtex-type": "@article",
+ "bibtex-data": {
+ "citeKey": "atzenbeck:2023:btr",
+ "author": [
+ "Atzenbeck, Claus",
+ "Herder, Eelco",
+ "Roßner, Daniel"
+ ],
+ "title": "Breaking The Routine: Spatial Hypertext Concepts for Active Decision Making in Recommender Systems",
+ "journal": "New Review of Hypermedia and Multimedia",
+ "volume": "29",
+ "number": "",
+ "pages": "1–35",
+ "year": "2023",
+ "doi": "10.1080/13614568.2023.2170474",
+ "isbn": "",
+ "keywords": [
+ ""
+ ]
+ },
+ "note": "Blekko geni mzinga cotweet oovoo wufoo, octopart insala sococo bebo, jajah wikia woopra weebly. Bubbli chegg chumby kaboodle blekko, zappos zinch woopra. imeem.\nPalantir twones kazaa meevee movity, hulu prezi sclipo, wikia wakoopa zoosk.",
+ "annote": "Bitly revver vuvox ning flickr divvyshot cotweet dopplr skype, handango odeo rovio bebo eduvant meevee.",
+ "source-url": "https://www.tandfonline.com/doi/full/10.1080/13614568.2023.2170474",
+ "source-pdf": "https://www.tandfonline.com/doi/epdf/10.1080/13614568.2023.2170474?needAccess=true",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "false",
+ "cross-references": ["atzenbeck:2018:mia","marshall:1995:shdfc","shipman:2002:sh"]
+ },
+ {
+ "object-id": "bernstein:1991:storyspace",
+ "object-type": "reference data",
+ "bibtex-type": "@inbook",
+ "bibtex-data": {
+ "citeKey": "bernstein:1991:storyspace",
+ "author": [
+ "Bernstein, Mark"
+ ],
+ "title": "Storyspace: Hypertext and the Process of Writing",
+ "booktitle": "Hypertext/Hypermedia Handbook",
+ "editor": [
+ "Berk, Emily",
+ "Devlin, Joseph"
+ ],
+ "publisher": "McGraw-Hill Inc., USA",
+ "address": "",
+ "pages": "529–533",
+ "year": "1991",
+ "doi": "",
+ "isbn": "0070166226",
+ "keywords": [
+ "hypertext; storyspace"
+ ]
+ },
+ "note": "",
+ "annote": "Wikia zlio imeem zanga jumo sifteo, divvyshot nuvvo ideeli.",
+ "cover-image-url": "https://archive.org/services/img/hypertexthyperme0000unse/full/pct:200/0/default.jpg",
+ "source-url": "",
+ "source-pdf": "",
+ "source-pdf-text": false,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "false",
+ "cross-references": []
+ },
+ {
+ "object-id": "bernstein:1998:poh",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings",
+ "bibtex-data": {
+ "citeKey": "bernstein:1998:poh",
+ "author": [
+ "Bernstein, Mark"
+ ],
+ "title": "Patterns of Hypertext",
+ "booktitle": "Proceedings of the Ninth ACM Conference on Hypertext and Hypermedia : Links, Objects, Time and Space—Structure in Hypermedia Systems",
+ "publisher": "ACM",
+ "address": "New York, NY, USA",
+ "pages": "21–29",
+ "year": "1998",
+ "doi": "10.1145/276627.276630",
+ "isbn": "0897919726",
+ "location": "Pittsburgh, PA, USA",
+ "keywords": [
+ "spatial hypertext",
+ "ht17-paper"
+ ]
+ },
+ "note": "Lala whrrl wufoo hulu yoono lijit zanga chartly knewton, qeyno kno octopart lanyrd spotify babblely.",
+ "annote": "Kaboodle kiko foodzie heroku jaiku babblely voxy spotify, airbnb jiglu waze vimeo ideeli twones.",
+ "source-url": "https://dl.acm.org/doi/10.1145/276627.276630",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/276627.276630",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "true",
+ "cross-references": ["alexander:1978:apl","joyce:1988:ss","marshall:1994:viki"]
+ },
+ {
+ "object-id": "bernstein:2010:csahr",
+ "object-type": "reference data",
+ "bibtex-type": "@online",
+ "bibtex-data": {
+ "citeKey": "bernstein:2010:csahr",
+ "author": [
+ "Bernstein, Mark"
+ ],
+ "year": "2010",
+ "title": "Card Sharks and Holy Scrollers",
+ "organization": "markbernstein.org",
+ "url": "https://www.markbernstein.org/Oct10/CardSharksandHolyScrollers.html",
+ "doi": "",
+ "urldate": "2026-02-26",
+ "lastaccessed": "2026-02-26",
+ "keywords": [
+ "spatial hypertext"
+ ]
+ },
+ "note": "Wesabe groupon bebo, shopify. Dropio cloudera empressr rovio jajah lanyrd, oovoo boxbe meebo.",
+ "annote": "Babblely doostang yammer ifttt etsy insala.",
+ "source-url": "https://www.markbernstein.org/Oct10/CardSharksandHolyScrollers.html",
+ "source-pdf": "",
+ "source-pdf-text": false,
+ "source-html": "https://www.markbernstein.org/Oct10/CardSharksandHolyScrollers.html",
+ "open-access": "false",
+ "free-acm-access": "false",
+ "cross-references": []
+ },
+ {
+ "object-id": "bernstein:2011:tash",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings",
+ "bibtex-data": {
+ "citeKey": "bernstein:2011:tash",
+ "author": [
+ "Bernstein, Mark"
+ ],
+ "title": "Can We Talk About Spatial Hypertext?",
+ "booktitle": "Proceedings of the 22nd ACM Conference on Hypertext and Hypermedia",
+ "publisher": "ACM",
+ "address": "New York, NY, USA",
+ "pages": "103–112",
+ "year": "2011",
+ "doi": "10.1145/1995966.1995983",
+ "isbn": "1450302564",
+ "location": "Eindhoven, Netherlands",
+ "keywords": [
+ "spatial hypertext",
+ "ht17-paper"
+ ]
+ },
+ "note": "",
+ "annote": "Wufoo odeo voki airbnb voki wufoo, weebly bebo scribd. voxy groupon. Wikia zlio imeem zanga jumo sifteo, divvyshot nuvvo ideeli.",
+ "source-url": "https://dl.acm.org/doi/10.1145/1995966.1995983",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/1995966.1995983",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "true",
+ "cross-references": ["bernstein:1998:poh","marshall:1994:viki","marshall:1997:shpit"]
+ },
+ {
+ "object-id": "hseih:2009:svpssh",
+ "object-type": "reference data",
+ "bibtex-type": "@article",
+ "bibtex-data": {
+ "citeKey": "hseih:2009:svpssh",
+ "author": [
+ "Hsieh, Haowei",
+ "Shipman, III, Frank M."
+ ],
+ "title": "Supporting Visual Problem Solving in Spatial Hypertext",
+ "journal": "Journal of Digital Information (JoDI)",
+ "volume": "10",
+ "number": "3",
+ "pages": "",
+ "year": "2009",
+ "doi": "",
+ "isbn": "",
+ "keywords": [
+ "spatial hypertext"
+ ]
+ },
+ "note": "Tivo babblely plugg heekya joukuu groupon vuvox, edmodo octopart joyent kazaa heekya.\nGlogster divvyshot convore zimbra, orkut zlio, movity zooomr.",
+ "annote": "Prezi bitly whrrl scribd divvyshot grockit jabber, vuvox jaiku shopify elgg.",
+ "source-url": "https://journals.tdl.org/jodi/index.php/jodi/article/view/173",
+ "source-pdf": "https://jodi-ojs-tdl.tdl.org/jodi/article/view/173/486",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "false",
+ "cross-references": ["marshall:1997:shpit"]
+ },
+ {
+ "object-id": "joyce:1988:ss",
+ "object-type": "reference data",
+ "bibtex-type": "@article",
+ "bibtex-data": {
+ "citeKey": "joyce:1988:ss",
+ "author": [
+ "Joyce, Michael"
+ ],
+ "title": "Siren Shapes",
+ "journal": "Academic Computing",
+ "volume": "3",
+ "number": "4",
+ "pages": "10–14, 37",
+ "year": "1988",
+ "doi": "",
+ "isbn": "",
+ "keywords": [
+ "storyspace",
+ "hypertext"
+ ]
+ },
+ "note": "lala plickers dropio. Divvyshot joost weebly voki foodzie edmodo wufoo zoho, cotweet foodzie joukuu twitter groupon.",
+ "annote": "Ebay weebly chumby gooru unigo, chegg vimeo.",
+ "source-url": "",
+ "source-pdf": "",
+ "source-pdf-text": false,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "false",
+ "cross-references": []
+ },
+ {
+ "object-id": "marshall:1987:erpuh",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings",
+ "bibtex-data": {
+ "citeKey": "marshall:1987:erpuh",
+ "author": [
+ "Marshall, Catherine C."
+ ],
+ "title": "Exploring Representation Problems Using Hypertext",
+ "booktitle": "Proceedings of the ACM Conference on Hypertext",
+ "publisher": "ACM",
+ "address": "{New York, NY, USA",
+ "pages": "253–268",
+ "year": "1987",
+ "doi": "10.1145/317426.317445",
+ "isbn": "089791340X",
+ "location": "Chapel Hill, NC, USA",
+ "keywords": [
+ "spatial hypertext",
+ "notecards"
+ ]
+ },
+ "note": "Scribd tivo xobni tivo udemy, glogster wufoo plugg chumby appjet, oooj sifteo etsy. babblely etsy sococo.\nTwones jaiku dropio blekko yoono zlio, klout glogster twones oooj kaboodle, ideeli spotify eskobo plaxo.",
+ "annote": "Scribd woopra flickr shopify qeyno hojoki .",
+ "source-url": "https://dl.acm.org/doi/10.1145/317426.317445",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/317426.317445",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "true",
+ "cross-references": []
+ },
+ {
+ "object-id": "marshall:1994:viki",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings",
+ "bibtex-data": {
+ "citeKey": "marshall:1994:viki",
+ "author": [
+ "Marshall, Catherine C.",
+ "Shipman, III, Frank M.",
+ "Coombs, James H."
+ ],
+ "title": "VIKI: Spatial Hypertext Supporting Emergent Structure",
+ "booktitle": "Proceedings of the 1994 ACM European Conference on Hypermedia Technology",
+ "publisher": "ACM",
+ "address": "New York, NY, USA",
+ "pages": "13–23",
+ "year": "1994",
+ "doi": "10.1145/192757.192759",
+ "isbn": "0897916409",
+ "location": "Edinburgh, Scotland, UK",
+ "keywords": [
+ "spatial hypertext",
+ "viki"
+ ]
+ },
+ "note": "Meebo sococo zynga blekko orkut wakoopa joukuu, wikia jibjab stypi heekya.",
+ "annote": "Edmodo mzinga fleck klout chumby yuntaa zimbra, blyve lanyrd akismet divvyshot.",
+ "source-url": "https://dl.acm.org/doi/10.1145/317426.317445",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/317426.317445",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "true",
+ "cross-references": []
+ },
+ {
+ "object-id": "marshall:1995:shdfc",
+ "object-type": "reference data",
+ "bibtex-type": "@article",
+ "bibtex-data": {
+ "citeKey": "marshall:1995:shdfc",
+ "author": [
+ "Marshall, Catherine C.",
+ "Shipman, III, Frank M."
+ ],
+ "title": "Spatial Hypertext: Designing for Change",
+ "journal": "Communications of the ACM (CACM)",
+ "volume": "38",
+ "number": "8",
+ "pages": "88–97",
+ "year": "1995",
+ "doi": "10.1145/208344.208350",
+ "isbn": "",
+ "keywords": [
+ "spatial hypertext"
+ ]
+ },
+ "note": "",
+ "annote": "Plickers whrrl jaiku fleck, eskobo lijit. Octopart kazaa lanyrd bebo blekko spotify napster, appjet zanga dopplr kiko.",
+ "source-url": "https://dl.acm.org/doi/10.1145/208344.208350",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/208344.208350",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "true",
+ "cross-references": ["marshall:1994:viki"]
+ },
+ {
+ "object-id": "marshall:1997:shpit",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings",
+ "bibtex-data": {
+ "citeKey": "marshall:1997:shpit",
+ "author": [
+ "Marshall, Catherine C.",
+ "Shipman, III, Frank M."
+ ],
+ "title": "Spatial Hypertext and the Practice of Information Triage",
+ "booktitle": "Proceedings of the Eighth ACM Conference on Hypertext",
+ "publisher": "ACM",
+ "address": "New York, NY, USA",
+ "pages": "124–133",
+ "year": "1997",
+ "doi": "10.1145/267437.267451",
+ "isbn": "0897918665",
+ "location": "Southampton, UK",
+ "keywords": [
+ "spatial hypertext"
+ ]
+ },
+ "note": "Imvu spotify rovio dopplr woopra, jajah divvyshot plugg, diigo wesabe ebay.",
+ "annote": "Glogster tivo revver, jiglu.",
+ "source-url": "https://dl.acm.org/doi/10.1145/267437.267451",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/267437.267451",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "true",
+ "cross-references": ["marshall:1995:shdfc"]
+ },
+ {
+ "object-id": "nakakoji:2002:psshw",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings",
+ "bibtex-data": {
+ "citeKey": "nakakoji:2002:psshw",
+ "author": [
+ "Nakakoji, Kumiyo",
+ "Yamamoto, Yasuhiro"
+ ],
+ "title": "Position Statement for Spatial Hypertext Workshop at Hypertext 2002",
+ "booktitle": "Second Workshop on Spatial Hypertext",
+ "publisher": "ACM",
+ "address": "College Park, MD, USA",
+ "pages": "2",
+ "year": "2002",
+ "doi": "",
+ "isbn": "",
+ "location": "College Park, MD, USA",
+ "keywords": [
+ "spatial hypertext"
+ ]
+ },
+ "note": "",
+ "annote": "Mog joost gsnap zooomr zynga gsnap cuil trulia, meevee handango hulu reddit odeo bubbli.",
+ "source-url": "https://people.engr.tamu.edu/shipman/SpatialHypertext/SH2",
+ "source-pdf": "https://people.engr.tamu.edu/shipman/SpatialHypertext/SH2/nakakoji.pdf",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "false",
+ "cross-references": []
+ },
+ {
+ "object-id": "rossner:2023:spore",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings",
+ "bibtex-data": {
+ "citeKey": "rossner:2023:spore",
+ "author": [
+ "Roßner, Daniel",
+ "Atzenbeck, Claus",
+ "Brooker, Sam"
+ ],
+ "title": "SPORE: A Storybreaking Machine",
+ "booktitle": "Proceedings of the 34th ACM Conference on Hypertext and Social Media",
+ "publisher": "Association for Computing Machinery",
+ "address": "New York, NY, USA",
+ "pages": "",
+ "year": "2023",
+ "doi": "10.1145/3603163.3609075",
+ "isbn": "9798400702327",
+ "location": "Rome, Italy",
+ "keywords": [
+ "mother",
+ "education",
+ "hypertext",
+ "linguistics",
+ "recommender system",
+ "spatial hypertext",
+ "storytelling",
+ "tropes"
+ ]
+ },
+ "note": "",
+ "annote": "Voxy heroku revver unigo mozy, jaiku woopra. Udemy zoho tivo, divvyshot.",
+ "source-url": "https://dl.acm.org/doi/10.1145/3603163.3609075",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/3603163.3609075",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "false",
+ "cross-references": ["anderson:2024:beyond","atzenbeck:2018:mia"]
+ },
+ {
+ "object-id": "shipman:1999:spatial",
+ "object-type": "reference data",
+ "bibtex-type": "@article",
+ "bibtex-data": {
+ "citeKey": "shipman:1999:spatial",
+ "author": [
+ "Shipman, III, Frank M.",
+ "Marshall, Catherine C"
+ ],
+ "title": "Spatial Hypertext: An Alternative to Navigational and Semantic Links",
+ "journal": "ACM Computing Surveys (CSUR)",
+ "volume": "31",
+ "number": "4es",
+ "pages": "14",
+ "year": "1999",
+ "doi": "10.1145/345966.346001",
+ "isbn": "",
+ "keywords": [
+ "spatial hypertext"
+ ]
+ },
+ "note": "",
+ "annote": "Imeem imvu loopt geni zapier, skype zoodles hulu.",
+ "source-url": "https://dl.acm.org/doi/10.1145/345966.346001",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/345966.346001",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "true",
+ "cross-references": ["marshall:1994:viki","marshall:1995:shdfc"]
+ },
+ {
+ "object-id": "shipman:2001:sdshr",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings",
+ "bibtex-data": {
+ "citeKey": "shipman:2001:sdshr",
+ "author": [
+ "Shipman, III, Frank M."
+ ],
+ "title": "Seven Directions for Spatial Hypertext Research",
+ "booktitle": "First International Workshop on Spatial Hypertext",
+ "publisher": "ACM",
+ "address": "",
+ "pages": "",
+ "year": "2001",
+ "doi": "",
+ "isbn": "",
+ "location": "Århus, Denmark",
+ "keywords": [
+ "spatial hypertext"
+ ]
+ },
+ "note": "Orkut flickr squidoo blyve yuntaa imeem, chegg yammer fleck reddit.",
+ "annote": "Ngmoco blekko shopify, kno.",
+ "source-url": "https://people.engr.tamu.edu/shipman/SpatialHypertext/SH1",
+ "source-pdf": "https://people.engr.tamu.edu/shipman/SpatialHypertext/SH1/shipman.pdf",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "false",
+ "cross-references": ["marshall:1994:viki","marshall:1995:shdfc"]
+ },
+ {
+ "object-id": "shipman:2001:vkb",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings",
+ "bibtex-data": {
+ "citeKey": "shipman:2001:vkb",
+ "author": [
+ "Shipman, III, Frank M.",
+ "Hsieh, Haowei",
+ "Maloor, Preetam",
+ " Moore, J. Michael"
+ ],
+ "title": "The Visual Knowledge Builder: A Second Generation Spatial Hypertext",
+ "booktitle": "Proceedings of the 12th ACM Conference on Hypertext and Hypermedia",
+ "publisher": "ACM",
+ "address": "New York, NY, USA",
+ "pages": "113–122",
+ "year": "2001",
+ "doi": "10.1145/504216.504245",
+ "isbn": "1581134207",
+ "location": "Århus, Denmark",
+ "keywords": [
+ "spatial hypertext",
+ "vkb"
+ ]
+ },
+ "note": "",
+ "annote": "Vuvox jibjab hojoki geni odeo, balihoo twones kippt koofers odeo, convore woopra klout.",
+ "source-url": "https://dl.acm.org/doi/10.1145/504216.504245",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/504216.504245",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "true",
+ "cross-references": ["marshall:1995:shdfc"]
+ },
+ {
+ "object-id": "shipman:2002:sh",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings",
+ "bibtex-data": {
+ "citeKey": "shipman:2002:sh",
+ "author": [
+ "Shipman, III, Frank M.",
+ "Moore, J. Michael",
+ "Maloor, Preetam",
+ "Hsieh, Haowei",
+ "Akkapeddi, Raghu"
+ ],
+ "title": "Semantics Happen: Knowledge Building in Spatial Hypertext",
+ "booktitle": "Proceedings of the Thirteenth ACM Conference on Hypertext and Hypermedia",
+ "publisher": "ACM",
+ "address": "New York, NY, USA",
+ "pages": "25–34",
+ "year": "2002",
+ "doi": "10.1145/513338.513350",
+ "isbn": "1581134770",
+ "location": "College Park, MD, USA",
+ "keywords": [
+ "spatial hypertext"
+ ]
+ },
+ "note": "",
+ "annote": "Hojoki jibjab movity qeyno lijit, flickr ideeli.",
+ "source-url": "https://dl.acm.org/doi/10.1145/513338.513350",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/513338.513350",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "false",
+ "cross-references": ["marshall:1995:shdfc"]
+ },
+ {
+ "object-id": "yip:2020:dash",
+ "object-type": "reference data",
+ "bibtex-type": "@inproceedings",
+ "bibtex-data": {
+ "citeKey": "yip:2020:dash",
+ "author": [
+ "Yip, Stanley",
+ "Zeleznik, Bob",
+ "Wilkins, Samuel",
+ "Schicke, Tyler",
+ "van Dam, Andries"
+ ],
+ "title": "Dash: A Hyper Framework",
+ "booktitle": "Proceedings of the 31st ACM Conference on Hypertext and Social Media",
+ "publisher": "ACM",
+ "address": "New York, NY, USA",
+ "pages": "237–238",
+ "year": "2020",
+ "doi": "10.1145/3372923.3404807",
+ "isbn": "9781450370981",
+ "location": "Orlando, FL, USA (Virtual Event)",
+ "keywords": [
+ "document engineering",
+ "hypertext",
+ "hypermedia",
+ "collaborative editing",
+ "workflow support"
+ ]
+ },
+ "note": "",
+ "annote": "Zapier chegg elgg stypi yoono elgg wikia zooomr yuntaa, movity xobni loopt stypi lanyrd foodzie ngmoco.",
+ "source-url": "https://dl.acm.org/doi/10.1145/3372923.3404807",
+ "source-pdf": "https://dl.acm.org/doi/pdf/10.1145/3372923.3404807",
+ "source-pdf-text": true,
+ "source-html": "",
+ "open-access": "false",
+ "free-acm-access": "false",
+ "cross-references": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/data/selfcontained_test.html b/data/selfcontained_test.html
new file mode 100644
index 0000000..703f526
--- /dev/null
+++ b/data/selfcontained_test.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+save
+
+
+
+
+
diff --git a/data/style.css b/data/style.css
new file mode 100644
index 0000000..2e9582e
--- /dev/null
+++ b/data/style.css
@@ -0,0 +1,3 @@
+body {
+ background-color: #d3e3e5;
+}
diff --git a/data/withdefault.jxrstyles.json b/data/withdefault.jxrstyles.json
new file mode 100644
index 0000000..b5abac9
--- /dev/null
+++ b/data/withdefault.jxrstyles.json
@@ -0,0 +1,26 @@
+{
+ "default" : [
+ {"selector":"#start_file_sloan_testtxt_end_file_hello_worldtxt", "attribute":"line", "value": "color:blue"},
+ {"selector":"a-sky", "attribute":"color", "value": "lightblue"},
+ {"selector":".notes", "attribute":"color", "value": "purple"},
+ {"selector":".notes", "attribute":"outline-color", "value": "darkblue"},
+ {"selector":"a-troika-text a-plane", "attribute":"color", "value": "white"},
+ {"selector":"a-troika-text a-triangle", "attribute":"color", "value": "gray"}
+ ],
+ "light" : [
+ {"selector":"#start_file_sloan_testtxt_end_file_hello_worldtxt", "attribute":"line", "value": "color:blue"},
+ {"selector":"a-sky", "attribute":"color", "value": "gray"},
+ {"selector":".notes", "attribute":"color", "value": "black"},
+ {"selector":".notes", "attribute":"outline-color", "value": "white"},
+ {"selector":"a-troika-text a-plane", "attribute":"color", "value": "red"},
+ {"selector":"a-troika-text a-triangle", "attribute":"color", "value": "darkred"}
+ ],
+ "print" : [
+ {"selector":"#start_file_sloan_testtxt_end_file_hello_worldtxt", "attribute":"line", "value": "color:brown"},
+ {"selector":"a-sky", "attribute":"color", "value": "#EEE"},
+ {"selector":".notes", "attribute":"color", "value": "black"},
+ {"selector":".notes", "attribute":"outline-color", "value": "white"},
+ {"selector":"a-troika-text a-plane", "attribute":"color", "value": "lightyellow"},
+ {"selector":"a-troika-text a-triangle", "attribute":"color", "value": "orange"}
+ ]
+}
diff --git a/jxr-core.js b/jxr-core.js
index 1b723a9..169cc66 100644
--- a/jxr-core.js
+++ b/jxr-core.js
@@ -11,6 +11,7 @@ var selectionBox = new THREE.BoxHelper( bbox.object3D, 0x0000ff);
var groupHelpers = []
var primaryPinchStarted = false
var wristShortcut = "jxr switchToWireframe()"
+var otherWristShortcut = "jxr console.log('hi from other wrist)"
var selectionPinchMode = false
var groupingMode = false
var hudTextEl // should instead rely on the #typinghud selector in most cases
@@ -63,10 +64,11 @@ function getClosestTargetElement( pos, threshold=0.05 ){ // 10x lower threshold
// assumes both hands have the same (single) parent, if any
let parentPos = document.getElementById('rig').getAttribute('position')
pos.add( parentPos )
- console.log( "from getClosestTargetElements, pos:", pos ) // relative pos, should thus remove rig position, even though it makes assumptions
+ console.log( "from getClosestTargetElement, pinch pos:", pos ) // relative pos, should thus remove rig position, even though it makes assumptions
const matches = getClosestTargetElements( pos, threshold)
if (matches.length > 0) res = matches[0].el
+ console.log( "from getClosestTargetElements, res:", res, 'among', matches )
return res
}
@@ -227,10 +229,15 @@ AFRAME.registerComponent('pinchprimary', { // currently only 1 hand, the right o
selectedElement.object3D.rotation.copy( v )
selectedElement.object3D.rotateY(1)
selectedElement.object3D.rotateZ(-1.5)
+ // could check if hands are flipped when rightHand has attribute different
+ if ( rightHand.getAttribute("hand-tracking-controls").hand == "left" ){
+ selectedElement.object3D.rotateY(-2)
+ selectedElement.object3D.rotateZ(3)
+ }
}
if (selectedElement) selectedElement.emit("moved")
AFRAME.scenes[0].object3D.getObjectByName("r_handMeshNode").material.wireframe = true
- // doesn't allow hand switching
+ // doesn't allow hand switching, should query via AFrame element instead
});
this.el.addEventListener('pinchstarted', function (event) {
primaryPinchStarted = true
@@ -705,7 +712,7 @@ AFRAME.registerComponent('start-on-press-other', {
this.el.addEventListener('pressedended', function (event) {
console.log(event)
// should ignore that if we entered XR recently
- if (!primaryPinchStarted && wristShortcut.match(prefix)) interpretJXR("jxr toggleShowCube()")
+ if (!primaryPinchStarted && wristShortcut.match(prefix)) interpretJXR(otherWristShortcut)
// if (!primaryPinchStarted && wristShortcut.match(prefix)) interpretJXR("jxr toggleShowFile('manuscript.txt')")
// FIXME should toggle the display of manuscript
// seems to happen also when entering VR
+
+