Began backend serving testing new metadata format with fetching, initial setup completed prior to adjusting parsing, refactored code into helper functions

This commit is contained in:
vzhang03 2024-06-24 14:29:58 -04:00
parent 2049ce4685
commit 0ba7a6fa21
12 changed files with 1942 additions and 3407 deletions

718
backend/package-lock.json generated Normal file
View File

@ -0,0 +1,718 @@
{
"name": "backend",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "backend",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.19.2"
}
},
"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/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/body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
"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.11.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/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/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/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"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/cors": {
"version": "2.8.5",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
"integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
"dependencies": {
"object-assign": "^4",
"vary": "^1"
},
"engines": {
"node": ">= 0.10"
}
},
"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/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/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/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/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/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/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.19.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "1.20.2",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.6.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "2.0.0",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "1.2.0",
"fresh": "0.5.2",
"http-errors": "2.0.0",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "2.4.1",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.7",
"qs": "6.11.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "0.18.0",
"serve-static": "1.15.0",
"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/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
"dependencies": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"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/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/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/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/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/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/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/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"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/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.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w=="
},
"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/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/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"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/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-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
},
"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/qs": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"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/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/send": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
"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/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.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.18.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/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/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/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/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/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"
}
}
}
}

16
backend/package.json Normal file
View File

@ -0,0 +1,16 @@
{
"name": "backend",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.19.2"
}
}

View File

@ -0,0 +1,232 @@
import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych";
import { version } from "../../package.json";
const info = <const>{
name: "html-keyboard-response",
version: version,
parameters: {
/**
* The string to be displayed.
*/
stimulus: {
type: ParameterType.HTML_STRING,
default: undefined,
},
/**
* This array contains the key(s) that the participant is allowed to press in order to respond
* to the stimulus. Keys should be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, `'ArrowDown'`) - see
* {@link https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values this page}
* and
* {@link https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/ this page (event.key column)}
* for more examples. Any key presses that are not listed in the
* array will be ignored. The default value of `"ALL_KEYS"` means that all keys will be accepted as valid responses.
* Specifying `"NO_KEYS"` will mean that no responses are allowed.
*/
choices: {
type: ParameterType.KEYS,
default: "ALL_KEYS",
},
/**
* This string can contain HTML markup. Any content here will be displayed below the stimulus.
* The intention is that it can be used to provide a reminder about the action the participant
* is supposed to take (e.g., which key to press).
*/
prompt: {
type: ParameterType.HTML_STRING,
default: null,
},
/**
* How long to display the stimulus in milliseconds. The visibility CSS property of the stimulus
* will be set to `hidden` after this time has elapsed. If this is null, then the stimulus will
* remain visible until the trial ends.
*/
stimulus_duration: {
type: ParameterType.INT,
default: null,
},
/**
* How long to wait for the participant to make a response before ending the trial in milliseconds.
* If the participant fails to make a response before this timer is reached, the participant's response
* will be recorded as null for the trial and the trial will end. If the value of this parameter is null,
* then the trial will wait for a response indefinitely.
*/
trial_duration: {
type: ParameterType.INT,
default: null,
},
/**
* If true, then the trial will end whenever the participant makes a response (assuming they make their
* response before the cutoff specified by the trial_duration parameter). If false, then the trial will
* continue until the value for trial_duration is reached. You can set this parameter to false to force
* the participant to view a stimulus for a fixed amount of time, even if they respond before the time is complete.
*/
response_ends_trial: {
type: ParameterType.BOOL,
default: true,
},
},
data: {
/** Indicates which key the participant pressed. */
response: {
type: ParameterType.STRING,
},
/** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus first appears on the screen until the participant's response. */
rt: {
type: ParameterType.INT,
},
/** The HTML content that was displayed on the screen. */
stimulus: {
type: ParameterType.STRING,
},
},
};
type Info = typeof info;
/**
* This plugin displays HTML content and records responses generated with the keyboard.
* The stimulus can be displayed until a response is given, or for a pre-determined amount of time.
* The trial can be ended automatically if the participant has failed to respond within a fixed length of time.
*
* @author Josh de Leeuw
* @see {@link https://www.jspsych.org/latest/plugins/html-keyboard-response/ html-keyboard-response plugin documentation on jspsych.org}
*/
class HtmlKeyboardResponsePlugin implements JsPsychPlugin<Info> {
static info = info;
constructor(private jsPsych: JsPsych) {}
trial(display_element: HTMLElement, trial: TrialType<Info>) {
var new_html = '<div id="jspsych-html-keyboard-response-stimulus">' + trial.stimulus + "</div>";
// add prompt
if (trial.prompt !== null) {
new_html += trial.prompt;
}
// draw
display_element.innerHTML = new_html;
// store response
var response = {
rt: null,
key: null,
};
// function to end trial when it is time
const end_trial = () => {
// kill any remaining setTimeout handlers
this.jsPsych.pluginAPI.clearAllTimeouts();
// kill keyboard listeners
if (typeof keyboardListener !== "undefined") {
this.jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener);
}
// gather the data to store for the trial
var trial_data = {
rt: response.rt,
stimulus: trial.stimulus,
response: response.key,
};
// clear the display
display_element.innerHTML = "";
// move on to the next trial
this.jsPsych.finishTrial(trial_data);
};
// function to handle responses by the subject
var after_response = (info) => {
// after a valid response, the stimulus will have the CSS class 'responded'
// which can be used to provide visual feedback that a response was recorded
display_element.querySelector("#jspsych-html-keyboard-response-stimulus").className +=
" responded";
// only record the first response
if (response.key == null) {
response = info;
}
if (trial.response_ends_trial) {
end_trial();
}
};
// start the response listener
if (trial.choices != "NO_KEYS") {
var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({
callback_function: after_response,
valid_responses: trial.choices,
rt_method: "performance",
persist: false,
allow_held_key: false,
});
}
// hide stimulus if stimulus_duration is set
if (trial.stimulus_duration !== null) {
this.jsPsych.pluginAPI.setTimeout(() => {
display_element.querySelector<HTMLElement>(
"#jspsych-html-keyboard-response-stimulus"
).style.visibility = "hidden";
}, trial.stimulus_duration);
}
// end trial if trial_duration is set
if (trial.trial_duration !== null) {
this.jsPsych.pluginAPI.setTimeout(end_trial, trial.trial_duration);
}
}
simulate(
trial: TrialType<Info>,
simulation_mode,
simulation_options: any,
load_callback: () => void
) {
if (simulation_mode == "data-only") {
load_callback();
this.simulate_data_only(trial, simulation_options);
}
if (simulation_mode == "visual") {
this.simulate_visual(trial, simulation_options, load_callback);
}
}
private create_simulation_data(trial: TrialType<Info>, simulation_options) {
const default_data = {
stimulus: trial.stimulus,
rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),
response: this.jsPsych.pluginAPI.getValidKey(trial.choices),
};
const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
return data;
}
private simulate_data_only(trial: TrialType<Info>, simulation_options) {
const data = this.create_simulation_data(trial, simulation_options);
this.jsPsych.finishTrial(data);
}
private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {
const data = this.create_simulation_data(trial, simulation_options);
const display_element = this.jsPsych.getDisplayElement();
this.trial(display_element, trial);
load_callback();
if (data.rt !== null) {
this.jsPsych.pluginAPI.pressKey(data.response, data.rt);
}
}
}
export default HtmlKeyboardResponsePlugin;

View File

@ -0,0 +1,338 @@
import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych";
import { version } from "../../package.json";
const info = <const>{
name: "image-keyboard-response",
version: version,
parameters: {
/** The path of the image file to be displayed. */
stimulus: {
type: ParameterType.IMAGE,
default: undefined,
},
/** Set the height of the image in pixels. If left null (no value specified), then the image will display at its natural height. */
stimulus_height: {
type: ParameterType.INT,
default: null,
},
/** Set the width of the image in pixels. If left null (no value specified), then the image will display at its natural width. */
stimulus_width: {
type: ParameterType.INT,
default: null,
},
/** If setting *only* the width or *only* the height and this parameter is true, then the other dimension will be scaled
* to maintain the image's aspect ratio. */
maintain_aspect_ratio: {
type: ParameterType.BOOL,
default: true,
},
/**his array contains the key(s) that the participant is allowed to press in order to respond to the stimulus. Keys should
* be specified as characters (e.g., `'a'`, `'q'`, `' '`, `'Enter'`, `'ArrowDown'`) - see
* [this page](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values) and
* [this page (event.key column)](https://www.freecodecamp.org/news/javascript-keycode-list-keypress-event-key-codes/)
* for more examples. Any key presses that are not listed in the array will be ignored. The default value of `"ALL_KEYS"`
* means that all keys will be accepted as valid responses. Specifying `"NO_KEYS"` will mean that no responses are allowed. */
choices: {
type: ParameterType.KEYS,
default: "ALL_KEYS",
},
/**This string can contain HTML markup. Any content here will be displayed below the stimulus. The intention is that it can
* be used to provide a reminder about the action the participant is supposed to take (e.g., which key to press). */
prompt: {
type: ParameterType.HTML_STRING,
default: null,
},
/** How long to show the stimulus for in milliseconds. If the value is `null`, then the stimulus will be shown until the
* participant makes a response. */
stimulus_duration: {
type: ParameterType.INT,
default: null,
},
/** How long to wait for the participant to make a response before ending the trial in milliseconds. If the participant
* fails to make a response before this timer is reached, the participant's response will be recorded as null for the
* trial and the trial will end. If the value of this parameter is `null`, then the trial will wait for a response indefinitely. */
trial_duration: {
type: ParameterType.INT,
default: null,
},
/** If true, then the trial will end whenever the participant makes a response (assuming they make their response before
* the cutoff specified by the `trial_duration` parameter). If false, then the trial will continue until the value for
* `trial_duration` is reached. You can set this parameter to `false` to force the participant to view a stimulus for a
* fixed amount of time, even if they respond before the time is complete. */
response_ends_trial: {
type: ParameterType.BOOL,
default: true,
},
/**
* If `true`, the image will be drawn onto a canvas element. This prevents a blank screen (white flash) between consecutive image trials in some browsers, like Firefox and Edge.
* If `false`, the image will be shown via an img element, as in previous versions of jsPsych. If the stimulus is an **animated gif**, you must set this parameter to false, because the canvas rendering method will only present static images.
*/
render_on_canvas: {
type: ParameterType.BOOL,
default: true,
},
},
data: {
/** The path of the image that was displayed. */
stimulus: {
type: ParameterType.STRING,
},
/** Indicates which key the participant pressed. */
response: {
type: ParameterType.STRING,
},
/** The response time in milliseconds for the participant to make a response. The time is measured from when the stimulus
* first appears on the screen until the participant's response. */
rt: {
type: ParameterType.INT,
},
},
};
type Info = typeof info;
/**
* This plugin displays an image and records responses generated with the keyboard. The stimulus can be displayed until a
* response is given, or for a pre-determined amount of time. The trial can be ended automatically if the participant has
* failed to respond within a fixed length of time.
*
* Image files can be automatically preloaded by jsPsych using the [`preload` plugin](preload.md). However, if you are using
* timeline variables or another dynamic method to specify the image stimulus, you will need to
* [manually preload](../overview/media-preloading.md#manual-preloading) the images.
*
* @author Josh de Leeuw
* @see {@link https://www.jspsych.org/latest/plugins/image-keyboard-response/ image-keyboard-response plugin documentation on jspsych.org}
*/
class ImageKeyboardResponsePlugin implements JsPsychPlugin<Info> {
static info = info;
constructor(private jsPsych: JsPsych) {}
trial(display_element: HTMLElement, trial: TrialType<Info>) {
var height, width;
if (trial.render_on_canvas) {
var image_drawn = false;
// first clear the display element (because the render_on_canvas method appends to display_element instead of overwriting it with .innerHTML)
if (display_element.hasChildNodes()) {
// can't loop through child list because the list will be modified by .removeChild()
while (display_element.firstChild) {
display_element.removeChild(display_element.firstChild);
}
}
// create canvas element and image
var canvas = document.createElement("canvas");
canvas.id = "jspsych-image-keyboard-response-stimulus";
canvas.style.margin = "0";
canvas.style.padding = "0";
var ctx = canvas.getContext("2d");
var img = new Image();
img.onload = () => {
// if image wasn't preloaded, then it will need to be drawn whenever it finishes loading
if (!image_drawn) {
getHeightWidth(); // only possible to get width/height after image loads
ctx.drawImage(img, 0, 0, width, height);
}
};
img.src = trial.stimulus;
// get/set image height and width - this can only be done after image loads because uses image's naturalWidth/naturalHeight properties
const getHeightWidth = () => {
if (trial.stimulus_height !== null) {
height = trial.stimulus_height;
if (trial.stimulus_width == null && trial.maintain_aspect_ratio) {
width = img.naturalWidth * (trial.stimulus_height / img.naturalHeight);
}
} else {
height = img.naturalHeight;
}
if (trial.stimulus_width !== null) {
width = trial.stimulus_width;
if (trial.stimulus_height == null && trial.maintain_aspect_ratio) {
height = img.naturalHeight * (trial.stimulus_width / img.naturalWidth);
}
} else if (!(trial.stimulus_height !== null && trial.maintain_aspect_ratio)) {
// if stimulus width is null, only use the image's natural width if the width value wasn't set
// in the if statement above, based on a specified height and maintain_aspect_ratio = true
width = img.naturalWidth;
}
canvas.height = height;
canvas.width = width;
};
getHeightWidth(); // call now, in case image loads immediately (is cached)
// add canvas and draw image
display_element.insertBefore(canvas, null);
if (img.complete && Number.isFinite(width) && Number.isFinite(height)) {
// if image has loaded and width/height have been set, then draw it now
// (don't rely on img onload function to draw image when image is in the cache, because that causes a delay in the image presentation)
ctx.drawImage(img, 0, 0, width, height);
image_drawn = true;
}
// add prompt if there is one
if (trial.prompt !== null) {
display_element.insertAdjacentHTML("beforeend", trial.prompt);
}
} else {
// display stimulus as an image element
var html = '<img src="' + trial.stimulus + '" id="jspsych-image-keyboard-response-stimulus">';
// add prompt
if (trial.prompt !== null) {
html += trial.prompt;
}
// update the page content
display_element.innerHTML = html;
// set image dimensions after image has loaded (so that we have access to naturalHeight/naturalWidth)
var img = display_element.querySelector(
"#jspsych-image-keyboard-response-stimulus"
) as HTMLImageElement;
if (trial.stimulus_height !== null) {
height = trial.stimulus_height;
if (trial.stimulus_width == null && trial.maintain_aspect_ratio) {
width = img.naturalWidth * (trial.stimulus_height / img.naturalHeight);
}
} else {
height = img.naturalHeight;
}
if (trial.stimulus_width !== null) {
width = trial.stimulus_width;
if (trial.stimulus_height == null && trial.maintain_aspect_ratio) {
height = img.naturalHeight * (trial.stimulus_width / img.naturalWidth);
}
} else if (!(trial.stimulus_height !== null && trial.maintain_aspect_ratio)) {
// if stimulus width is null, only use the image's natural width if the width value wasn't set
// in the if statement above, based on a specified height and maintain_aspect_ratio = true
width = img.naturalWidth;
}
img.style.height = height.toString() + "px";
img.style.width = width.toString() + "px";
}
// store response
var response = {
rt: null,
key: null,
};
// function to end trial when it is time
const end_trial = () => {
// kill any remaining setTimeout handlers
this.jsPsych.pluginAPI.clearAllTimeouts();
// kill keyboard listeners
if (typeof keyboardListener !== "undefined") {
this.jsPsych.pluginAPI.cancelKeyboardResponse(keyboardListener);
}
// gather the data to store for the trial
var trial_data = {
rt: response.rt,
stimulus: trial.stimulus,
response: response.key,
};
// clear the display
display_element.innerHTML = "";
// move on to the next trial
this.jsPsych.finishTrial(trial_data);
};
// function to handle responses by the subject
var after_response = (info) => {
// after a valid response, the stimulus will have the CSS class 'responded'
// which can be used to provide visual feedback that a response was recorded
display_element.querySelector("#jspsych-image-keyboard-response-stimulus").className +=
" responded";
// only record the first response
if (response.key == null) {
response = info;
}
if (trial.response_ends_trial) {
end_trial();
}
};
// start the response listener
if (trial.choices != "NO_KEYS") {
var keyboardListener = this.jsPsych.pluginAPI.getKeyboardResponse({
callback_function: after_response,
valid_responses: trial.choices,
rt_method: "performance",
persist: false,
allow_held_key: false,
});
}
// hide stimulus if stimulus_duration is set
if (trial.stimulus_duration !== null) {
this.jsPsych.pluginAPI.setTimeout(() => {
display_element.querySelector<HTMLElement>(
"#jspsych-image-keyboard-response-stimulus"
).style.visibility = "hidden";
}, trial.stimulus_duration);
}
// end trial if trial_duration is set
if (trial.trial_duration !== null) {
this.jsPsych.pluginAPI.setTimeout(() => {
end_trial();
}, trial.trial_duration);
} else if (trial.response_ends_trial === false) {
console.warn(
"The experiment may be deadlocked. Try setting a trial duration or set response_ends_trial to true."
);
}
}
simulate(
trial: TrialType<Info>,
simulation_mode,
simulation_options: any,
load_callback: () => void
) {
if (simulation_mode == "data-only") {
load_callback();
this.simulate_data_only(trial, simulation_options);
}
if (simulation_mode == "visual") {
this.simulate_visual(trial, simulation_options, load_callback);
}
}
private simulate_data_only(trial: TrialType<Info>, simulation_options) {
const data = this.create_simulation_data(trial, simulation_options);
this.jsPsych.finishTrial(data);
}
private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {
const data = this.create_simulation_data(trial, simulation_options);
const display_element = this.jsPsych.getDisplayElement();
this.trial(display_element, trial);
load_callback();
if (data.rt !== null) {
this.jsPsych.pluginAPI.pressKey(data.response, data.rt);
}
}
private create_simulation_data(trial: TrialType<Info>, simulation_options) {
const default_data = {
stimulus: trial.stimulus,
rt: this.jsPsych.randomization.sampleExGaussian(500, 50, 1 / 150, true),
response: this.jsPsych.pluginAPI.getValidKey(trial.choices),
};
const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
this.jsPsych.pluginAPI.ensureSimulationDataConsistency(trial, data);
return data;
}
}
export default ImageKeyboardResponsePlugin;

View File

@ -0,0 +1,467 @@
import { JsPsych, JsPsychPlugin, ParameterType, TrialType } from "jspsych";
import { version } from "../../package.json";
const info = <const>{
name: "preload",
version: version,
parameters: {
/** If `true`, the plugin will preload any files that can be automatically preloaded based on the main experiment
* timeline that is passed to `jsPsych.run`. If `false`, any file(s) to be preloaded should be specified by passing
* a timeline array to the `trials` parameter and/or an array of file paths to the `images`, `audio`, and/or `video`
* parameters. Setting this parameter to `false` is useful when you plan to preload your files in smaller batches
* throughout the experiment. */
auto_preload: {
type: ParameterType.BOOL,
default: false,
},
/** An array containing one or more jsPsych trial or timeline objects. This parameter is useful when you want to
* automatically preload stimuli files from a specific subset of the experiment. See [Creating an Experiment:
* The Timeline](../overview/timeline.md) for information on constructing timelines. */
trials: {
type: ParameterType.TIMELINE,
default: [],
},
/**
* Array with one or more image files to load. This parameter is often used in cases where media files cannot
* be automatically preloaded based on the timeline, e.g. because the media files are passed into an image plugin/parameter with
* timeline variables or dynamic parameters, or because the image is embedded in an HTML string.
*/
images: {
type: ParameterType.STRING,
default: [],
array: true,
},
/**
* Array with one or more audio files to load. This parameter is often used in cases where media files cannot
* be automatically preloaded based on the timeline, e.g. because the media files are passed into an audio plugin/parameter with
* timeline variables or dynamic parameters, or because the audio is embedded in an HTML string.
*/
audio: {
type: ParameterType.STRING,
default: [],
array: true,
},
/**
* Array with one or more video files to load. This parameter is often used in cases where media files cannot
* be automatically preloaded based on the timeline, e.g. because the media files are passed into a video plugin/parameter with
* timeline variables or dynamic parameters, or because the video is embedded in an HTML string.
*/
video: {
type: ParameterType.STRING,
default: [],
array: true,
},
/** HTML-formatted message to show above the progress bar while the files are loading. If `null`, then no message is shown. */
message: {
type: ParameterType.HTML_STRING,
default: null,
},
/** If `true`, a progress bar will be shown while the files are loading. If `false`, no progress bar is shown. */
show_progress_bar: {
type: ParameterType.BOOL,
default: true,
},
/**
* Whether or not to continue with the experiment if a loading error occurs. If false, then if a loading error occurs,
* the error_message will be shown on the page and the trial will not end. If true, then if if a loading error occurs, the trial will end
* and preloading failure will be logged in the trial data.
*/
continue_after_error: {
type: ParameterType.BOOL,
default: false,
},
/** HTML-formatted message to be shown on the page after loading fails or times out. Only applies when `continue_after_error` is `false`.*/
error_message: {
type: ParameterType.HTML_STRING,
default: "The experiment failed to load.",
},
/**
* Whether or not to show a detailed error message on the page. If true, then detailed error messages will be shown on the
* page for all files that failed to load, along with the general error_message. This parameter is only relevant when continue_after_error is false.
*/
show_detailed_errors: {
type: ParameterType.BOOL,
default: false,
},
/**
* The maximum amount of time that the plugin should wait before stopping the preload and either ending the trial
* (if continue_after_error is true) or stopping the experiment with an error message (if continue_after_error is false).
* If null, the plugin will wait indefintely for the files to load.
*/
max_load_time: {
type: ParameterType.INT,
default: null,
},
/** Function to be called after a file fails to load. The function takes the file name as its only argument. */
on_error: {
type: ParameterType.FUNCTION,
default: null,
},
/** Function to be called after a file loads successfully. The function takes the file name as its only argument. */
on_success: {
type: ParameterType.FUNCTION,
default: null,
},
},
data: {
/** If `true`, then all files loaded successfully within the `max_load_time`. If `false`, then one or
* more file requests returned a failure and/or the file loading did not complete within the `max_load_time` duration. */
success: {
type: ParameterType.BOOL,
},
/** If `true`, then the files did not finish loading within the `max_load_time` duration.
* If `false`, then the file loading did not timeout. Note that when the preload trial does not timeout
* (`timeout: false`), it is still possible for loading to fail (`success: false`). This happens if
* one or more files fails to load and all file requests trigger either a success or failure event before
* the `max_load_time` duration. */
timeout: {
type: ParameterType.BOOL,
},
/** One or more image file paths that produced a loading failure before the trial ended. */
failed_images: {
type: ParameterType.STRING,
array: true,
},
/** One or more audio file paths that produced a loading failure before the trial ended. */
failed_audio: {
type: ParameterType.STRING,
array: true,
},
/** One or more video file paths that produced a loading failure before the trial ended. */
failed_video: {
type: ParameterType.STRING,
array: true,
},
},
};
type Info = typeof info;
/**
* This plugin loads images, audio, and video files. It is used for loading files into the browser's memory before they are
* needed in the experiment, in order to improve stimulus and response timing, and avoid disruption to the experiment flow.
* We recommend using this plugin anytime you are loading media files, and especially when your experiment requires large
* and/or many media files. See the [Media Preloading page](../overview/media-preloading.md) for more information.
*
* The preload trial will end as soon as all files have loaded successfully. The trial will end or stop with an error
* message when one of these two scenarios occurs (whichever comes first): (a) all files have not finished loading
* when the `max_load_time` duration is reached, or (b) all file requests have responded with either a load or fail
* event, and one or more files has failed to load. The `continue_after_error` parameter determines whether the trial
* will stop with an error message or end (allowing the experiment to continue) when preloading is not successful.
*
* @author Becky Gilbert
* @see {@link https://www.jspsych.org/latest/plugins/preload/ preload plugin documentation on jspsych.org}
*/
class PreloadPlugin implements JsPsychPlugin<Info> {
static info = info;
constructor(private jsPsych: JsPsych) {}
trial(display_element: HTMLElement, trial: TrialType<Info>) {
var success = null;
var timeout = false;
var failed_images = [];
var failed_audio = [];
var failed_video = [];
var detailed_errors = [];
var in_safe_mode = this.jsPsych.getSafeModeStatus();
// create list of media to preload //
var images = [];
var audio = [];
var video = [];
if (trial.auto_preload) {
var experiment_timeline = this.jsPsych.getTimeline();
var auto_preload = this.jsPsych.pluginAPI.getAutoPreloadList(experiment_timeline);
images = images.concat(auto_preload.images);
audio = audio.concat(auto_preload.audio);
video = video.concat(auto_preload.video);
}
if (trial.trials.length > 0) {
var trial_preloads = this.jsPsych.pluginAPI.getAutoPreloadList(trial.trials);
images = images.concat(trial_preloads.images);
audio = audio.concat(trial_preloads.audio);
video = video.concat(trial_preloads.video);
}
images = images.concat(trial.images);
audio = audio.concat(trial.audio);
video = video.concat(trial.video);
images = this.jsPsych.utils.unique(images.flat());
audio = this.jsPsych.utils.unique(audio.flat());
video = this.jsPsych.utils.unique(video.flat());
if (in_safe_mode) {
// don't preload video if in safe mode (experiment is running via file protocol)
video = [];
}
// render display of message and progress bar
var html = "";
if (trial.message !== null) {
html += trial.message;
}
if (trial.show_progress_bar) {
html += `
<div id='jspsych-loading-progress-bar-container' style='height: 10px; width: 300px; background-color: #ddd; margin: auto;'>
<div id='jspsych-loading-progress-bar' style='height: 10px; width: 0%; background-color: #777;'></div>
</div>`;
}
display_element.innerHTML = html;
const update_loading_progress_bar = () => {
loaded++;
if (trial.show_progress_bar) {
var percent_loaded = (loaded / total_n) * 100;
var preload_progress_bar = display_element.querySelector<HTMLElement>(
"#jspsych-loading-progress-bar"
);
if (preload_progress_bar !== null) {
preload_progress_bar.style.width = percent_loaded + "%";
}
}
};
// called if all files load successfully
const on_success = () => {
if (typeof timeout !== "undefined" && timeout === false) {
// clear timeout immediately after finishing, to handle race condition with max_load_time
this.jsPsych.pluginAPI.clearAllTimeouts();
// need to call cancel preload function to clear global jsPsych preload_request list, even when they've all succeeded
this.jsPsych.pluginAPI.cancelPreloads();
success = true;
end_trial();
}
};
// called if all_files haven't finished loading when max_load_time is reached
const on_timeout = () => {
this.jsPsych.pluginAPI.cancelPreloads();
if (typeof success !== "undefined" && (success === false || success === null)) {
timeout = true;
if (loaded_success < total_n) {
success = false;
}
after_error("timeout"); // call trial's on_error event handler here, in case loading timed out with no file errors
detailed_errors.push(
"<p><strong>Loading timed out.</strong><br>" +
"Consider compressing your stimuli files, loading your files in smaller batches,<br>" +
"and/or increasing the <i>max_load_time</i> parameter.</p>"
);
if (trial.continue_after_error) {
end_trial();
} else {
stop_with_error_message();
}
}
};
const stop_with_error_message = () => {
this.jsPsych.pluginAPI.clearAllTimeouts();
this.jsPsych.pluginAPI.cancelPreloads();
// show error message
display_element.innerHTML = trial.error_message;
// show detailed errors, if necessary
if (trial.show_detailed_errors) {
display_element.innerHTML += "<p><strong>Error details:</strong></p>";
detailed_errors.forEach((e) => {
display_element.innerHTML += e;
});
}
};
const end_trial = () => {
// clear timeout again when end_trial is called, to handle race condition with max_load_time
this.jsPsych.pluginAPI.clearAllTimeouts();
var trial_data = {
success: success,
timeout: timeout,
failed_images: failed_images,
failed_audio: failed_audio,
failed_video: failed_video,
};
// clear the display
display_element.innerHTML = "";
this.jsPsych.finishTrial(trial_data);
};
// do preloading
if (trial.max_load_time !== null) {
this.jsPsych.pluginAPI.setTimeout(on_timeout, trial.max_load_time);
}
var total_n = images.length + audio.length + video.length;
var loaded = 0; // success or error count
var loaded_success = 0; // success count
if (total_n == 0) {
on_success();
} else {
const load_video = (cb) => {
this.jsPsych.pluginAPI.preloadVideo(video, cb, file_loading_success, file_loading_error);
};
const load_audio = (cb) => {
this.jsPsych.pluginAPI.preloadAudio(audio, cb, file_loading_success, file_loading_error);
};
const load_images = (cb) => {
this.jsPsych.pluginAPI.preloadImages(images, cb, file_loading_success, file_loading_error);
};
if (video.length > 0) {
load_video(() => {});
}
if (audio.length > 0) {
load_audio(() => {});
}
if (images.length > 0) {
load_images(() => {});
}
}
// helper functions and callbacks
// called when a single file loading fails
function file_loading_error(e) {
// update progress bar even if there's an error
update_loading_progress_bar();
// change success flag after first file loading error
if (success == null) {
success = false;
}
// add file to failed media list
var source = "unknown file";
if (e.source) {
source = e.source;
}
if (e.error && e.error.path && e.error.path.length > 0) {
if (e.error.path[0].localName == "img") {
failed_images.push(source);
} else if (e.error.path[0].localName == "audio") {
failed_audio.push(source);
} else if (e.error.path[0].localName == "video") {
failed_video.push(source);
}
}
// construct detailed error message
var err_msg = "<p><strong>Error loading file: " + source + "</strong><br>";
if (e.error.statusText) {
err_msg += "File request response status: " + e.error.statusText + "<br>";
}
if (e.error == "404") {
err_msg += "404 - file not found.<br>";
}
if (
typeof e.error.loaded !== "undefined" &&
e.error.loaded !== null &&
e.error.loaded !== 0
) {
err_msg += e.error.loaded + " bytes transferred.";
} else {
err_msg +=
"File did not begin loading. Check that file path is correct and reachable by the browser,<br>" +
"and that loading is not blocked by cross-origin resource sharing (CORS) errors.";
}
err_msg += "</p>";
detailed_errors.push(err_msg);
// call trial's on_error function
after_error(source);
// if this is the last file
if (loaded == total_n) {
if (trial.continue_after_error) {
// if continue_after_error is false, then stop with an error
end_trial();
} else {
// otherwise end the trial and continue
stop_with_error_message();
}
}
}
// called when a single file loads successfully
function file_loading_success(source: string) {
update_loading_progress_bar();
// call trial's on_success function
after_success(source);
loaded_success++;
if (loaded_success == total_n) {
// if this is the last file and all loaded successfully, call success function
on_success();
} else if (loaded == total_n) {
// if this is the last file and there was at least one error
if (trial.continue_after_error) {
// end the trial and continue with experiment
end_trial();
} else {
// if continue_after_error is false, then stop with an error
stop_with_error_message();
}
}
}
function after_error(source: string) {
// call on_error function and pass file name
if (trial.on_error !== null) {
trial.on_error(source);
}
}
function after_success(source: string) {
// call on_success function and pass file name
if (trial.on_success !== null) {
trial.on_success(source);
}
}
}
simulate(
trial: TrialType<Info>,
simulation_mode,
simulation_options: any,
load_callback: () => void
) {
if (simulation_mode == "data-only") {
load_callback();
this.simulate_data_only(trial, simulation_options);
}
if (simulation_mode == "visual") {
this.simulate_visual(trial, simulation_options, load_callback);
}
}
private create_simulation_data(trial: TrialType<Info>, simulation_options) {
const default_data = {
success: true,
timeout: false,
failed_images: [],
failed_audio: [],
failed_video: [],
};
const data = this.jsPsych.pluginAPI.mergeSimulationData(default_data, simulation_options);
return data;
}
private simulate_data_only(trial: TrialType<Info>, simulation_options) {
const data = this.create_simulation_data(trial, simulation_options);
this.jsPsych.finishTrial(data);
}
private simulate_visual(trial: TrialType<Info>, simulation_options, load_callback: () => void) {
const display_element = this.jsPsych.getDisplayElement();
this.trial(display_element, trial);
load_callback();
}
}
export default PreloadPlugin;

18
backend/server.js Normal file
View File

@ -0,0 +1,18 @@
const path = require("path");
const express = require("express");
const cors = require("cors"); // Import the cors package
const app = express();
app.use(cors()); // Use the cors middleware
app.get("/plugin/:pluginName/index.ts", (req, res) => {
const pluginName = req.params.pluginName;
const filePath = path.join(__dirname, ".", "plugins", pluginName, "index.ts");
res.sendFile(filePath);
});
const port = 3000;
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});

View File

@ -56,9 +56,7 @@
var jsPsych = initJsPsych({
on_finish: async function() {
// console.log(jsPsych.data.get().json());
// console.log(jsPsych.data.get().csv());
await metadata.generate(jsPsych.data.get().json(), metadata_options);
await metadata.generate(jsPsych.data.get().json());
// await metadata.generate(jsPsych.data.get().csv(), metadata_options, true);
// metadata.saveAsJsonFile();
@ -81,8 +79,8 @@
var timeline_variables = [
{ v1: 'img/happy_face_1.jpg', v2: 'Ann' },
//{ v1: 'img/happy_face_2.jpg', v2: 'Jackson' },
//{ v1: 'img/happy_face_3.jpg', v2: 'Riley' }
{ v1: 'img/happy_face_2.jpg', v2: 'Jackson' },
{ v1: 'img/happy_face_3.jpg', v2: 'Riley' }
];
var node = {

3428
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,12 @@
import { SetRequired } from "type-fest";
import { SimulationMode, SimulationOptions, TrialDescription, TrialResult } from "../timeline";
import {
Parameter,
SimulationMode,
SimulationOptions,
TrialDescription,
TrialResult,
} from "../timeline";
/**
* Parameter types for plugins
@ -138,7 +144,9 @@ export type UniversalPluginParameters = InferredParameters<typeof universalPlugi
export interface PluginInfo {
name: string;
version?: string;
parameters: ParameterInfos;
data?: ParameterInfos;
}
export interface JsPsychPlugin<I extends PluginInfo> {

View File

@ -40,6 +40,7 @@
"@jspsych/config": "^2.0.0",
"@jspsych/test-utils": "^1.1.2",
"@types/jest": "^29.5.12",
"husky": "^9.0.11",
"ts-jest": "^29.1.4"
}
}

View File

@ -39,7 +39,7 @@ export default class JsPsychMetadata {
* @private
* @type {{}}
*/
private cache: {};
private variables_cache: {};
private requests_cache: {}; // temporary requests cache before implementing faster method
/**
@ -66,7 +66,7 @@ export default class JsPsychMetadata {
this.setMetadataField("description", "Dataset generated using JsPsych");
this.authors = new AuthorsMap();
this.variables = new VariablesMap();
this.cache = {};
this.variables_cache = {};
this.requests_cache = {};
}
@ -319,6 +319,7 @@ export default class JsPsychMetadata {
for (const variable in observation) {
const value = observation[variable];
// console.log("pluginType:", pluginType, "variable:", variable, "value:", value);
if (value === null) continue;
@ -429,55 +430,78 @@ export default class JsPsychMetadata {
* @throws Will throw an error if the fetch operation fails.
*/
private async getPluginInfo(pluginType: string, variableName: string) {
const cache_request = this.checkCache(pluginType, variableName);
if (cache_request) return cache_request;
const description = await this.fetchAPI(pluginType, variableName);
return description;
}
private checkCache(pluginType: string, variableName: string) {
// Check if the cache for the pluginType exists, if not initialize it
if (!this.cache[pluginType]) this.cache[pluginType] = {};
else if (variableName in this.cache[pluginType]) {
if (!this.variables_cache[pluginType]) this.variables_cache[pluginType] = {};
else if (variableName in this.variables_cache[pluginType]) {
// If the variable already exists in the cache for the plugin, return the cached value
return this.cache[pluginType][variableName];
}
// If not, we proceed to fetch script:
// Construct the URL for the unpkg service
const unpkgUrl = `https://unpkg.com/@jspsych/plugin-${pluginType}/src/index.ts`;
try {
let description = "unknown";
// check requests cache
if (pluginType in this.requests_cache) {
const scriptContent = this.requests_cache[pluginType];
description = this.getJsdocsDescription(scriptContent, variableName);
this.cache[pluginType][variableName] = description;
} else {
// Fetch the script content from the unpkg URL
const response = await fetch(unpkgUrl);
const scriptContent = await response.text();
this.requests_cache[pluginType] = scriptContent;
console.log(scriptContent);
// Extract the JSDoc description for the variable from the script content
description = this.getJsdocsDescription(scriptContent, variableName);
// Check again if the cache for the pluginType exists, if not initialize it
if (!this.cache[pluginType]) this.cache[pluginType] = {}; // don't think this ever returns true, might be able delete
// Cache the description for the variable in the pluginType cache
this.cache[pluginType][variableName] = description;
// Return the description
}
return this.variables_cache[pluginType][variableName];
} else if (pluginType in this.requests_cache) {
// requests_cache hit
const scriptContent = this.requests_cache[pluginType];
const description = this.getJsdocsDescription(scriptContent, variableName);
this.variables_cache[pluginType][variableName] = description;
return description;
} else return undefined;
}
private async fetchAPI(pluginType: string, variableName: string) {
const unpkgUrl = `http://localhost:3000/plugin/${pluginType}/index.ts`;
let description = undefined;
try {
// Fetch the script content from the unpkg URL
const response = await fetch(unpkgUrl);
// Check if the response is not ok
if (!response.ok) {
throw new Error(`Network response was not ok, status: ${response.status}`);
}
const scriptContent = await response.text();
if (!scriptContent) {
console.error("fetched script was null...");
return description;
}
this.requests_cache[pluginType] = scriptContent;
// // Extract the JSDoc description for the variable from the script content
description = this.getJsdocsDescription(scriptContent, variableName);
// if (description) {
// this.variables_cache[pluginType][variableName] = description;
// } else {
// throw new Error(`No JSDoc description found for variable: ${variableName}`);
// }
} catch (error) {
console.error(`Failed to fetch info from ${unpkgUrl}:`, error); // DISABLING to test other features
// Error is likely due to 1)a fetch failure, or 2)no JSDoc comments in the script content matched.
//HANDLE FETCH FAILURE CASES
// In case of the latter, we cache the null value to prevent repeated fetch attempts.
// Handle specific fetch errors
if (error instanceof TypeError) {
console.error(`Failed to fetch info from ${unpkgUrl}: Network or CORS error`, error);
} else if (error.message.includes("Network response was not ok")) {
console.error(`Failed to fetch info from ${unpkgUrl}: ${error.message}`);
} else if (error.message.includes("No JSDoc description found")) {
console.error(error.message);
} else {
console.error(`Unexpected error occurred:`, error);
}
if (!this.cache[pluginType]) this.cache[pluginType] = {};
// Ensure the cache is updated to prevent repeated fetch attempts
if (!this.variables_cache[pluginType]) {
this.variables_cache[pluginType] = {};
}
this.cache[pluginType][variableName] = null;
return "failed with error";
this.variables_cache[pluginType][variableName] = undefined;
}
return description;
}
/**
@ -491,6 +515,7 @@ export default class JsPsychMetadata {
* @returns {string} The extracted JSDoc description, cleaned and trimmed.
*/
private getJsdocsDescription(scriptContent: string, variableName: string) {
// console.log("getJsDocDesc, varName:", variableName);
// Regex to match part of the content that starts with 'parameters:' and ends with '};', which
// is parameters info. THIS MUST BE CHANGED TO data FOR NEW PLUGIN LAYOUT
const paramRegex = scriptContent.match(/parameters:\s*{([\s\S]*?)};\s*/).join();