mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-09-25 06:01:40 +08:00
Merge branch 'develop' of https://github.com/TriliumNext/Notes into develop
This commit is contained in:
commit
7514e2348c
@ -1,12 +1,17 @@
|
||||
[Desktop Entry]
|
||||
<% if (productName) { %>Name=<%= productName %>
|
||||
<% } %><% if (description) { %>Comment=<%= description %>
|
||||
<% } %><% if (genericName) { %>GenericName=<%= genericName %>
|
||||
<% } %><% if (name) { %>Exec=<%= name %> %U
|
||||
Icon=<%= name %>
|
||||
<% } %>Type=Application
|
||||
StartupNotify=true
|
||||
<% if (productName) { %>StartupWMClass=<%= productName %>
|
||||
<% } if (categories && categories.length) { %>Categories=<%= categories.join(';') %>;
|
||||
<% } %><% if (mimeType && mimeType.length) { %>MimeType=<%= mimeType.join(';') %>;
|
||||
<% } %>
|
||||
<%=
|
||||
Object.entries({
|
||||
"Name": productName,
|
||||
"Comment": description,
|
||||
"GenericName": genericName,
|
||||
"Exec": name ? `${name} %U` : undefined,
|
||||
"Icon": name,
|
||||
"Type": "Application",
|
||||
"StartupNotify": "true",
|
||||
"StartupWMClass": productName,
|
||||
"Categories": categories?.length ? `${categories.join(";")};` : undefined,
|
||||
"MimeType": mimeType?.length ? `${mimeType.join(";")};` : undefined
|
||||
})
|
||||
.map(line => line[1] ? line.join("=") : undefined)
|
||||
.filter(line => !!line)
|
||||
.join("\n")%>
|
147
package-lock.json
generated
147
package-lock.json
generated
@ -86,7 +86,6 @@
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sanitize-html": "2.14.0",
|
||||
"sax": "1.4.1",
|
||||
"semver": "7.7.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"session-file-store": "1.5.0",
|
||||
"source-map-support": "0.5.21",
|
||||
@ -113,7 +112,7 @@
|
||||
"@electron-forge/maker-zip": "7.6.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.6.1",
|
||||
"@electron/rebuild": "3.7.1",
|
||||
"@playwright/test": "1.50.0",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@types/archiver": "6.0.3",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
@ -136,12 +135,11 @@
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/multer": "1.4.12",
|
||||
"@types/node": "22.12.0",
|
||||
"@types/node": "22.13.1",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/safe-compare": "1.1.2",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/sax": "1.2.7",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/serve-favicon": "2.5.7",
|
||||
"@types/session-file-store": "1.2.5",
|
||||
"@types/source-map-support": "0.5.10",
|
||||
@ -151,7 +149,7 @@
|
||||
"@types/ws": "8.5.14",
|
||||
"@types/xml2js": "0.4.14",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@vitest/coverage-v8": "3.0.4",
|
||||
"@vitest/coverage-v8": "3.0.5",
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "34.0.2",
|
||||
"esm": "3.2.25",
|
||||
@ -166,7 +164,7 @@
|
||||
"tsx": "4.19.2",
|
||||
"typedoc": "0.27.6",
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "3.0.4",
|
||||
"vitest": "3.0.5",
|
||||
"webpack": "5.97.1",
|
||||
"webpack-cli": "6.0.1",
|
||||
"webpack-dev-middleware": "7.4.2"
|
||||
@ -2880,13 +2878,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.50.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0.tgz",
|
||||
"integrity": "sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==",
|
||||
"version": "1.50.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz",
|
||||
"integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.50.0"
|
||||
"playwright": "1.50.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@ -3909,9 +3907,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz",
|
||||
"integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==",
|
||||
"version": "22.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz",
|
||||
"integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
@ -4002,13 +4000,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/semver": {
|
||||
"version": "7.5.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
|
||||
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/send": {
|
||||
"version": "0.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
|
||||
@ -4163,9 +4154,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.4.tgz",
|
||||
"integrity": "sha512-f0twgRCHgbs24Dp8cLWagzcObXMcuKtAwgxjJV/nnysPAJJk1JiKu/W0gIehZLmkljhJXU/E0/dmuQzsA/4jhA==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.5.tgz",
|
||||
"integrity": "sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -4186,8 +4177,8 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "3.0.4",
|
||||
"vitest": "3.0.4"
|
||||
"@vitest/browser": "3.0.5",
|
||||
"vitest": "3.0.5"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
@ -4196,14 +4187,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.4.tgz",
|
||||
"integrity": "sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz",
|
||||
"integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.0.4",
|
||||
"@vitest/utils": "3.0.4",
|
||||
"@vitest/spy": "3.0.5",
|
||||
"@vitest/utils": "3.0.5",
|
||||
"chai": "^5.1.2",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
@ -4212,13 +4203,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.4.tgz",
|
||||
"integrity": "sha512-gEef35vKafJlfQbnyOXZ0Gcr9IBUsMTyTLXsEQwuyYAerpHqvXhzdBnDFuHLpFqth3F7b6BaFr4qV/Cs1ULx5A==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz",
|
||||
"integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "3.0.4",
|
||||
"@vitest/spy": "3.0.5",
|
||||
"estree-walker": "^3.0.3",
|
||||
"magic-string": "^0.30.17"
|
||||
},
|
||||
@ -4239,9 +4230,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/pretty-format": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz",
|
||||
"integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz",
|
||||
"integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -4252,13 +4243,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.4.tgz",
|
||||
"integrity": "sha512-dKHzTQ7n9sExAcWH/0sh1elVgwc7OJ2lMOBrAm73J7AH6Pf9T12Zh3lNE1TETZaqrWFXtLlx3NVrLRb5hCK+iw==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz",
|
||||
"integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "3.0.4",
|
||||
"@vitest/utils": "3.0.5",
|
||||
"pathe": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
@ -4273,13 +4264,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.4.tgz",
|
||||
"integrity": "sha512-+p5knMLwIk7lTQkM3NonZ9zBewzVp9EVkVpvNta0/PlFWpiqLaRcF4+33L1it3uRUCh0BGLOaXPPGEjNKfWb4w==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz",
|
||||
"integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.0.4",
|
||||
"@vitest/pretty-format": "3.0.5",
|
||||
"magic-string": "^0.30.17",
|
||||
"pathe": "^2.0.2"
|
||||
},
|
||||
@ -4295,9 +4286,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.4.tgz",
|
||||
"integrity": "sha512-sXIMF0oauYyUy2hN49VFTYodzEAu744MmGcPR3ZBsPM20G+1/cSW/n1U+3Yu/zHxX2bIDe1oJASOkml+osTU6Q==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz",
|
||||
"integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -4308,13 +4299,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.4.tgz",
|
||||
"integrity": "sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz",
|
||||
"integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "3.0.4",
|
||||
"@vitest/pretty-format": "3.0.5",
|
||||
"loupe": "^3.1.2",
|
||||
"tinyrainbow": "^2.0.0"
|
||||
},
|
||||
@ -13320,13 +13311,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.50.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0.tgz",
|
||||
"integrity": "sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==",
|
||||
"version": "1.50.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz",
|
||||
"integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.50.0"
|
||||
"playwright-core": "1.50.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@ -13339,9 +13330,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.50.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0.tgz",
|
||||
"integrity": "sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==",
|
||||
"version": "1.50.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz",
|
||||
"integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@ -14721,9 +14712,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz",
|
||||
"integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==",
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@ -16856,9 +16847,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.4.tgz",
|
||||
"integrity": "sha512-7JZKEzcYV2Nx3u6rlvN8qdo3QV7Fxyt6hx+CCKz9fbWxdX5IvUOmTWEAxMrWxaiSf7CKGLJQ5rFu8prb/jBjOA==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz",
|
||||
"integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -17350,19 +17341,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.4.tgz",
|
||||
"integrity": "sha512-6XG8oTKy2gnJIFTHP6LD7ExFeNLxiTkK3CfMvT7IfR8IN+BYICCf0lXUQmX7i7JoxUP8QmeP4mTnWXgflu4yjw==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz",
|
||||
"integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/expect": "3.0.4",
|
||||
"@vitest/mocker": "3.0.4",
|
||||
"@vitest/pretty-format": "^3.0.4",
|
||||
"@vitest/runner": "3.0.4",
|
||||
"@vitest/snapshot": "3.0.4",
|
||||
"@vitest/spy": "3.0.4",
|
||||
"@vitest/utils": "3.0.4",
|
||||
"@vitest/expect": "3.0.5",
|
||||
"@vitest/mocker": "3.0.5",
|
||||
"@vitest/pretty-format": "^3.0.5",
|
||||
"@vitest/runner": "3.0.5",
|
||||
"@vitest/snapshot": "3.0.5",
|
||||
"@vitest/spy": "3.0.5",
|
||||
"@vitest/utils": "3.0.5",
|
||||
"chai": "^5.1.2",
|
||||
"debug": "^4.4.0",
|
||||
"expect-type": "^1.1.0",
|
||||
@ -17374,7 +17365,7 @@
|
||||
"tinypool": "^1.0.2",
|
||||
"tinyrainbow": "^2.0.0",
|
||||
"vite": "^5.0.0 || ^6.0.0",
|
||||
"vite-node": "3.0.4",
|
||||
"vite-node": "3.0.5",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
@ -17390,8 +17381,8 @@
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||
"@vitest/browser": "3.0.4",
|
||||
"@vitest/ui": "3.0.4",
|
||||
"@vitest/browser": "3.0.5",
|
||||
"@vitest/ui": "3.0.5",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
|
10
package.json
10
package.json
@ -131,7 +131,6 @@
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sanitize-html": "2.14.0",
|
||||
"sax": "1.4.1",
|
||||
"semver": "7.7.0",
|
||||
"serve-favicon": "2.5.0",
|
||||
"session-file-store": "1.5.0",
|
||||
"source-map-support": "0.5.21",
|
||||
@ -155,7 +154,7 @@
|
||||
"@electron-forge/maker-zip": "7.6.1",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.6.1",
|
||||
"@electron/rebuild": "3.7.1",
|
||||
"@playwright/test": "1.50.0",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@types/archiver": "6.0.3",
|
||||
"@types/better-sqlite3": "7.6.12",
|
||||
"@types/bootstrap": "5.2.10",
|
||||
@ -178,12 +177,11 @@
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/multer": "1.4.12",
|
||||
"@types/node": "22.12.0",
|
||||
"@types/node": "22.13.1",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/safe-compare": "1.1.2",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/sax": "1.2.7",
|
||||
"@types/semver": "7.5.8",
|
||||
"@types/serve-favicon": "2.5.7",
|
||||
"@types/session-file-store": "1.2.5",
|
||||
"@types/source-map-support": "0.5.10",
|
||||
@ -193,7 +191,7 @@
|
||||
"@types/ws": "8.5.14",
|
||||
"@types/xml2js": "0.4.14",
|
||||
"@types/yargs": "17.0.33",
|
||||
"@vitest/coverage-v8": "3.0.4",
|
||||
"@vitest/coverage-v8": "3.0.5",
|
||||
"cross-env": "7.0.3",
|
||||
"electron": "34.0.2",
|
||||
"esm": "3.2.25",
|
||||
@ -208,7 +206,7 @@
|
||||
"tsx": "4.19.2",
|
||||
"typedoc": "0.27.6",
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "3.0.4",
|
||||
"vitest": "3.0.5",
|
||||
"webpack": "5.97.1",
|
||||
"webpack-cli": "6.0.1",
|
||||
"webpack-dev-middleware": "7.4.2"
|
||||
|
@ -216,6 +216,7 @@ function deleteBranch(req: Request) {
|
||||
|
||||
function setPrefix(req: Request) {
|
||||
const branchId = req.params.branchId;
|
||||
//TriliumNextTODO: req.body arrives as string, so req.body.prefix will be undefined – did the code below ever even work?
|
||||
const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix;
|
||||
|
||||
const branch = becca.getBranchOrThrow(branchId);
|
||||
|
@ -6,7 +6,7 @@ import eventService from "./events.js";
|
||||
import cls from "../services/cls.js";
|
||||
import protectedSessionService from "../services/protected_session.js";
|
||||
import log from "../services/log.js";
|
||||
import { newEntityId, isString, unescapeHtml, quoteRegex, toMap } from "../services/utils.js";
|
||||
import { newEntityId, unescapeHtml, quoteRegex, toMap } from "../services/utils.js";
|
||||
import revisionService from "./revisions.js";
|
||||
import request from "./request.js";
|
||||
import path from "path";
|
||||
@ -731,13 +731,13 @@ function updateNoteData(noteId: string, content: string, attachments: Attachment
|
||||
note.setContent(newContent, { forceFrontendReload });
|
||||
|
||||
if (attachments?.length > 0) {
|
||||
const existingAttachmentsByTitle = toMap(note.getAttachments({ includeContentLength: false }), "title");
|
||||
const existingAttachmentsByTitle = toMap(note.getAttachments({ includeContentLength: false }), "title");
|
||||
|
||||
for (const { attachmentId, role, mime, title, position, content } of attachments) {
|
||||
if (attachmentId || !(title in existingAttachmentsByTitle)) {
|
||||
const existingAttachment = existingAttachmentsByTitle.get(title);
|
||||
if (attachmentId || !existingAttachment) {
|
||||
note.saveAttachment({ attachmentId, role, mime, title, content, position });
|
||||
} else {
|
||||
const existingAttachment = existingAttachmentsByTitle[title];
|
||||
existingAttachment.role = role;
|
||||
existingAttachment.mime = mime;
|
||||
existingAttachment.position = position;
|
||||
@ -884,7 +884,7 @@ async function asyncPostProcessContent(note: BNote, content: string | Buffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.hasStringContent() && !isString(content)) {
|
||||
if (note.hasStringContent() && typeof content !== "string") {
|
||||
content = content.toString();
|
||||
}
|
||||
|
||||
|
@ -1,61 +0,0 @@
|
||||
import { expect, describe, it } from "vitest";
|
||||
import { formatDownloadTitle } from "./utils.js";
|
||||
|
||||
const testCases: [fnValue: Parameters<typeof formatDownloadTitle>, expectedValue: ReturnType<typeof formatDownloadTitle>][] = [
|
||||
// empty fileName tests
|
||||
[["", "text", ""], "untitled.html"],
|
||||
|
||||
[["", "canvas", ""], "untitled.json"],
|
||||
|
||||
[["", null, ""], "untitled"],
|
||||
|
||||
// json extension from type tests
|
||||
[["test_file", "canvas", ""], "test_file.json"],
|
||||
|
||||
[["test_file", "relationMap", ""], "test_file.json"],
|
||||
|
||||
[["test_file", "search", ""], "test_file.json"],
|
||||
|
||||
// extension based on mime type
|
||||
[["test_file", null, "text/csv"], "test_file.csv"],
|
||||
|
||||
[["test_file_wo_ext", "image", "image/svg+xml"], "test_file_wo_ext.svg"],
|
||||
|
||||
[["test_file_wo_ext", "file", "application/json"], "test_file_wo_ext.json"],
|
||||
|
||||
[["test_file_w_fake_ext.ext", "image", "image/svg+xml"], "test_file_w_fake_ext.ext.svg"],
|
||||
|
||||
[["test_file_w_correct_ext.svg", "image", "image/svg+xml"], "test_file_w_correct_ext.svg"],
|
||||
|
||||
[["test_file_w_correct_ext.svgz", "image", "image/svg+xml"], "test_file_w_correct_ext.svgz"],
|
||||
|
||||
[["test_file.zip", "file", "application/zip"], "test_file.zip"],
|
||||
|
||||
[["test_file", "file", "application/zip"], "test_file.zip"],
|
||||
|
||||
// application/octet-stream tests
|
||||
[["test_file", "file", "application/octet-stream"], "test_file"],
|
||||
|
||||
[["test_file.zip", "file", "application/octet-stream"], "test_file.zip"],
|
||||
|
||||
[["test_file.unknown", null, "application/octet-stream"], "test_file.unknown"],
|
||||
|
||||
// sanitized filename tests
|
||||
[["test/file", null, "application/octet-stream"], "testfile"],
|
||||
|
||||
[["test:file.zip", "file", "application/zip"], "testfile.zip"],
|
||||
|
||||
[[":::", "file", "application/zip"], ".zip"],
|
||||
|
||||
[[":::a", "file", "application/zip"], "a.zip"]
|
||||
];
|
||||
|
||||
describe("utils/formatDownloadTitle unit tests", () => {
|
||||
testCases.forEach((testCase) => {
|
||||
return it(`With args '${JSON.stringify(testCase[0])}' it should return '${testCase[1]}'`, () => {
|
||||
const [value, expected] = testCase;
|
||||
const actual = formatDownloadTitle(...value);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
630
src/services/utils.spec.ts
Normal file
630
src/services/utils.spec.ts
Normal file
@ -0,0 +1,630 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import utils from "./utils.js";
|
||||
|
||||
type TestCase<T extends (...args: any) => any> = [desc: string, fnParams: Parameters<T>, expected: ReturnType<T>];
|
||||
|
||||
describe("#newEntityId", () => {
|
||||
|
||||
it("should return a string with a length of 12", () => {
|
||||
const result = utils.newEntityId();
|
||||
expect(result).toBeTypeOf("string");
|
||||
expect(result).toHaveLength(12);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("#randomString", () => {
|
||||
|
||||
it("should return a string with a length as per argument", () => {
|
||||
const stringLength = 5;
|
||||
const result = utils.randomString(stringLength);
|
||||
expect(result).toBeTypeOf("string");
|
||||
expect(result).toHaveLength(stringLength);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// TriliumNextTODO: should use mocks and assert that functions get called
|
||||
describe("#randomSecureToken", () => {
|
||||
// base64 -> 4 * (bytes/3) length -> if padding and rounding up is ignored for simplicity
|
||||
// https://stackoverflow.com/a/13378842
|
||||
const byteToBase64Length = (bytes: number) => 4 * (bytes / 3);
|
||||
|
||||
it("should return a string and use 32 bytes by default", () => {
|
||||
const result = utils.randomSecureToken();
|
||||
expect(result).toBeTypeOf("string");
|
||||
expect(result.length).toBeGreaterThanOrEqual(byteToBase64Length(32));
|
||||
});
|
||||
|
||||
it("should return a string and use passed byte length", () => {
|
||||
const bytes = 16;
|
||||
const result = utils.randomSecureToken(bytes);
|
||||
expect(result).toBeTypeOf("string");
|
||||
expect(result.length).toBeGreaterThanOrEqual(byteToBase64Length(bytes));
|
||||
expect(result.length).toBeLessThan(44); // default argument uses 32 bytes -> which translates to 44 base64 legal chars
|
||||
});
|
||||
});
|
||||
|
||||
// TriliumNextTODO: should use mocks and assert that functions get called
|
||||
describe.todo("#md5", () => {});
|
||||
|
||||
// TriliumNextTODO: should use mocks and assert that functions get called
|
||||
describe.todo("#hashedBlobId", () => {});
|
||||
|
||||
// TriliumNextTODO: should use mocks and assert that functions get called
|
||||
describe.todo("#toBase64", () => {});
|
||||
|
||||
// TriliumNextTODO: should use mocks and assert that functions get called
|
||||
describe.todo("#fromBase64", () => {});
|
||||
|
||||
// TriliumNextTODO: should use mocks and assert that functions get called
|
||||
describe.todo("#hmac", () => {});
|
||||
|
||||
// TriliumNextTODO: should use mocks and assert that functions get called
|
||||
describe.todo("#hash", () => {});
|
||||
|
||||
describe("#isEmptyOrWhitespace", () => {
|
||||
|
||||
const testCases: TestCase<typeof utils.isEmptyOrWhitespace>[] = [
|
||||
["w/ 'null' it should return true", [null], true],
|
||||
["w/ 'null' it should return true", [null], true],
|
||||
["w/ undefined it should return true", [undefined], true],
|
||||
["w/ empty string '' it should return true", [""], true],
|
||||
["w/ single whitespace string ' ' it should return true", [" "], true],
|
||||
["w/ multiple whitespace string ' ' it should return true", [" "], true],
|
||||
["w/ non-empty string ' t ' it should return false", [" t "], false],
|
||||
];
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [desc, fnParams, expected] = testCase;
|
||||
it(desc, () => {
|
||||
const result = utils.isEmptyOrWhitespace(...fnParams);
|
||||
expect(result).toStrictEqual(expected);
|
||||
})
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
describe("#sanitizeSqlIdentifier", () => {
|
||||
|
||||
const testCases: TestCase<typeof utils.sanitizeSqlIdentifier>[] = [
|
||||
["w/ 'test' it should not strip anything", ["test"], "test"],
|
||||
["w/ 'test123' it should not strip anything", ["test123"], "test123"],
|
||||
["w/ 'tEst_TeSt' it should not strip anything", ["tEst_TeSt"], "tEst_TeSt"],
|
||||
["w/ 'test_test' it should not strip '_'", ["test_test"], "test_test"],
|
||||
["w/ 'test-' it should strip the '-'", ["test-"], "test"],
|
||||
["w/ 'test-test' it should strip the '-'", ["test-test"], "testtest"],
|
||||
["w/ 'test; --test' it should strip the '; --'", ["test; --test"], "testtest"],
|
||||
["w/ 'test test' it should strip the ' '", ["test test"], "testtest"],
|
||||
];
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [desc, fnParams, expected] = testCase;
|
||||
it(desc, () => {
|
||||
const result = utils.sanitizeSqlIdentifier(...fnParams);
|
||||
expect(result).toStrictEqual(expected);
|
||||
})
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("#escapeHtml", () => {
|
||||
it("should re-export 'escape-html' npm module as escapeHtml", () => {
|
||||
expect(utils.escapeHtml).toBeTypeOf("function");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#unescapeHtml", () => {
|
||||
it("should re-export 'unescape' npm module as unescapeHtml", () => {
|
||||
expect(utils.unescapeHtml).toBeTypeOf("function");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#toObject", () => {
|
||||
it("should return an object with keys and value being set from the supplied Function", () => {
|
||||
type TestListEntry = { testPropA: string, testPropB: string };
|
||||
type TestListFn = (testListEntry: TestListEntry) => [string, string];
|
||||
const testList: [TestListEntry, TestListEntry] = [{ testPropA: "keyA", testPropB: "valueA" }, { testPropA: "keyB", testPropB: "valueB" }];
|
||||
const fn: TestListFn = (testListEntry: TestListEntry) => [testListEntry.testPropA + "_fn", testListEntry.testPropB + "_fn"];
|
||||
|
||||
const result = utils.toObject(testList, fn);
|
||||
expect(result).toStrictEqual({
|
||||
"keyA_fn": "valueA_fn",
|
||||
"keyB_fn": "valueB_fn"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#stripTags", () => {
|
||||
|
||||
//prettier-ignore
|
||||
const htmlWithNewlines =
|
||||
`<p>abc
|
||||
def</p>
|
||||
<p>ghi</p>`;
|
||||
|
||||
const testCases: TestCase<typeof utils.stripTags>[] = [
|
||||
["should strip all tags and only return the content, leaving new lines and spaces in tact", [htmlWithNewlines], "abc\ndef\nghi"],
|
||||
//TriliumNextTODO: should this actually insert a space between content to prevent concatenated text?
|
||||
["should strip all tags and only return the content", ["<h1>abc</h1><p>def</p>"], "abcdef"],
|
||||
];
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [desc, fnParams, expected] = testCase;
|
||||
it(desc, () => {
|
||||
const result = utils.stripTags(...fnParams);
|
||||
expect(result).toStrictEqual(expected);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe.todo("#escapeRegExp", () => {});
|
||||
|
||||
describe.todo("#crash", () => {});
|
||||
|
||||
describe("#getContentDisposition", () => {
|
||||
|
||||
const defaultFallBackDisposition = `file; filename="file"; filename*=UTF-8''file`;
|
||||
const testCases: TestCase<typeof utils.getContentDisposition>[] = [
|
||||
[
|
||||
"when passed filename is empty, it should fallback to default value 'file'",
|
||||
[" "],
|
||||
defaultFallBackDisposition
|
||||
],
|
||||
[
|
||||
"when passed filename '..' would cause sanitized filename to be empty, it should fallback to default value 'file'",
|
||||
[".."],
|
||||
defaultFallBackDisposition
|
||||
],
|
||||
// COM1 is a Windows specific "illegal filename" that sanitize filename strips away
|
||||
[
|
||||
"when passed filename 'COM1' would cause sanitized filename to be empty, it should fallback to default value 'file'",
|
||||
["COM1"],
|
||||
defaultFallBackDisposition
|
||||
],
|
||||
[
|
||||
"sanitized passed filename should be returned URIEncoded",
|
||||
["test file.csv"],
|
||||
`file; filename="test%20file.csv"; filename*=UTF-8''test%20file.csv`
|
||||
]
|
||||
]
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [desc, fnParams, expected] = testCase;
|
||||
it(desc, () => {
|
||||
const result = utils.getContentDisposition(...fnParams);
|
||||
expect(result).toStrictEqual(expected);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe("#isStringNote", () => {
|
||||
|
||||
const testCases: TestCase<typeof utils.isStringNote>[] = [
|
||||
[
|
||||
"w/ 'undefined' note type, but a string mime type, it should return true",
|
||||
[undefined, "application/javascript"],
|
||||
true
|
||||
],
|
||||
[
|
||||
"w/ non-string note type, it should return false",
|
||||
["image", "image/jpeg"],
|
||||
false
|
||||
],
|
||||
[
|
||||
"w/ string note type (text), it should return true",
|
||||
["text", "text/html"],
|
||||
true
|
||||
],
|
||||
[
|
||||
"w/ string note type (code), it should return true",
|
||||
["code", "application/json"],
|
||||
true
|
||||
],
|
||||
[
|
||||
"w/ non-string note type (file), but string mime type, it should return true",
|
||||
["file", "application/json"],
|
||||
true
|
||||
],
|
||||
[
|
||||
"w/ non-string note type (file), but mime type starting with 'text/', it should return true",
|
||||
["file", "text/html"],
|
||||
true
|
||||
],
|
||||
];
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [desc, fnParams, expected] = testCase;
|
||||
it(desc, () => {
|
||||
const result = utils.isStringNote(...fnParams);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.todo("#quoteRegex", () => {});
|
||||
|
||||
describe.todo("#replaceAll", () => {});
|
||||
|
||||
describe("#removeTextFileExtension", () => {
|
||||
const testCases: TestCase<typeof utils.removeTextFileExtension>[] = [
|
||||
["w/ 'test.md' it should strip '.md'", ["test.md"], "test"],
|
||||
["w/ 'test.markdown' it should strip '.markdown'", ["test.markdown"], "test"],
|
||||
["w/ 'test.html' it should strip '.html'", ["test.html"], "test"],
|
||||
["w/ 'test.htm' it should strip '.htm'", ["test.htm"], "test"],
|
||||
["w/ 'test.zip' it should NOT strip '.zip'", ["test.zip"], "test.zip"],
|
||||
];
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [desc, fnParams, expected] = testCase;
|
||||
it(desc, () => {
|
||||
const result = utils.removeTextFileExtension(...fnParams);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("#getNoteTitle", () => {
|
||||
const testCases: TestCase<typeof utils.getNoteTitle>[] = [
|
||||
[
|
||||
"when file has no spaces, and no special file extension, it should return the filename unaltered",
|
||||
["test.json", true, undefined],
|
||||
"test.json"
|
||||
],
|
||||
[
|
||||
"when replaceUnderscoresWithSpaces is false, it should keep the underscores in the title",
|
||||
["test_file.json", false, undefined],
|
||||
"test_file.json"
|
||||
],
|
||||
[
|
||||
"when replaceUnderscoresWithSpaces is true, it should replace the underscores in the title",
|
||||
["test_file.json", true, undefined],
|
||||
"test file.json"
|
||||
],
|
||||
[
|
||||
"when filePath ends with one of the extra handled endings (.md), it should strip the file extension from the title",
|
||||
["test_file.md", false, undefined],
|
||||
"test_file"
|
||||
],
|
||||
[
|
||||
"when filePath ends with one of the extra handled endings (.md) and replaceUnderscoresWithSpaces is true, it should strip the file extension from the title and replace underscores",
|
||||
["test_file.md", true, undefined],
|
||||
"test file"
|
||||
],
|
||||
[
|
||||
"when filepath contains a full path, it should only return the basename of the file",
|
||||
["Trilium Demo/Scripting examples/Statistics/Most cloned notes/template.zip", true, undefined],
|
||||
"template.zip"
|
||||
],
|
||||
[
|
||||
"when filepath contains a full path and has extra handled ending (.html), it should only return the basename of the file and strip the file extension",
|
||||
["Trilium Demo/Scripting examples/Statistics/Most cloned notes/template.html", true, undefined],
|
||||
"template"
|
||||
],
|
||||
[
|
||||
"when a noteMeta object is passed, it should use the title from the noteMeta, if present",
|
||||
//@ts-expect-error - passing in incomplete noteMeta - but we only care about the title prop here
|
||||
["test_file.md", true, { title: "some other title"}],
|
||||
"some other title"
|
||||
],
|
||||
[
|
||||
"when a noteMeta object is passed, but the title prop is empty, it should try to handle the filename as if no noteMeta was passed",
|
||||
//@ts-expect-error - passing in incomplete noteMeta - but we only care about the title prop here
|
||||
["test_file.md", true, { title: ""}],
|
||||
"test file"
|
||||
],
|
||||
[
|
||||
"when a noteMeta object is passed, but the title prop is empty, it should try to handle the filename as if no noteMeta was passed",
|
||||
//@ts-expect-error - passing in incomplete noteMeta - but we only care about the title prop here
|
||||
["test_file.json", false, { title: " "}],
|
||||
"test_file.json"
|
||||
]
|
||||
];
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [desc, fnParams, expected] = testCase;
|
||||
it(desc, () => {
|
||||
const result = utils.getNoteTitle(...fnParams);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("#timeLimit", () => {
|
||||
|
||||
it("when promise execution does NOT exceed timeout, it should resolve with promises' value", async () => {
|
||||
const resolvedValue = `resolved: ${new Date().toISOString()}`;
|
||||
const testPromise = new Promise((res, rej) => {
|
||||
setTimeout(() => {
|
||||
return res(resolvedValue);
|
||||
}, 200);
|
||||
//rej("rejected!");
|
||||
});
|
||||
await expect(utils.timeLimit(testPromise, 1_000)).resolves.toBe(resolvedValue);
|
||||
});
|
||||
|
||||
it("when promise execution rejects within timeout, it should return the original promises' rejected value, not the custom set one", async () => {
|
||||
const rejectedValue = `rejected: ${new Date().toISOString()}`;
|
||||
const testPromise = new Promise((res, rej) => {
|
||||
setTimeout(() => {
|
||||
//return res("resolved");
|
||||
rej(rejectedValue);
|
||||
}, 100);
|
||||
});
|
||||
await expect(utils.timeLimit(testPromise, 200, "Custom Error")).rejects.toThrow(rejectedValue)
|
||||
});
|
||||
|
||||
it("when promise execution exceeds the set timeout, and 'errorMessage' is NOT set, it should reject the promise and display default error message", async () => {
|
||||
const testPromise = new Promise((res, rej) => {
|
||||
setTimeout(() => {
|
||||
return res("resolved");
|
||||
}, 500);
|
||||
//rej("rejected!");
|
||||
});
|
||||
await expect(utils.timeLimit(testPromise, 200)).rejects.toThrow(`Process exceeded time limit 200`)
|
||||
});
|
||||
|
||||
it("when promise execution exceeds the set timeout, and 'errorMessage' is set, it should reject the promise and display set error message", async () => {
|
||||
const customErrorMsg = "Custom Error";
|
||||
const testPromise = new Promise((res, rej) => {
|
||||
setTimeout(() => {
|
||||
return res("resolved");
|
||||
}, 500);
|
||||
//rej("rejected!");
|
||||
});
|
||||
await expect(utils.timeLimit(testPromise, 200, customErrorMsg)).rejects.toThrow(customErrorMsg)
|
||||
});
|
||||
|
||||
// TriliumNextTODO: since TS avoids this from ever happening – do we need this check?
|
||||
it("when the passed promise is not a promise but 'undefined', it should return 'undefined'", async () => {
|
||||
//@ts-expect-error - passing in illegal type 'undefined'
|
||||
expect(utils.timeLimit(undefined, 200)).toBe(undefined)
|
||||
});
|
||||
|
||||
// TriliumNextTODO: since TS avoids this from ever happening – do we need this check?
|
||||
it("when the passed promise is not a promise, it should return the passed value", async () => {
|
||||
//@ts-expect-error - passing in illegal type 'object'
|
||||
expect(utils.timeLimit({test: 1}, 200)).toStrictEqual({test: 1})
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("#deferred", () => {
|
||||
it("should return a promise", () => {
|
||||
const result = utils.deferred();
|
||||
expect(result).toBeInstanceOf(Promise)
|
||||
})
|
||||
// TriliumNextTODO: Add further tests!
|
||||
});
|
||||
|
||||
describe("#removeDiacritic", () => {
|
||||
|
||||
const testCases: TestCase<typeof utils.removeDiacritic>[] = [
|
||||
["w/ 'Äpfel' it should replace the 'Ä'", ["Äpfel"], "Apfel"],
|
||||
["w/ 'Été' it should replace the 'É' and 'é'", ["Été"], "Ete"],
|
||||
["w/ 'Fête' it should replace the 'ê'", ["Fête"], "Fete"],
|
||||
["w/ 'Αλφαβήτα' it should replace the 'ή'", ["Αλφαβήτα"], "Αλφαβητα"],
|
||||
["w/ '' (empty string) it should return empty string", [""], ""],
|
||||
];
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [desc, fnParams, expected] = testCase;
|
||||
it(desc, () => {
|
||||
const result = utils.removeDiacritic(...fnParams);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("#normalize", () => {
|
||||
|
||||
const testCases: TestCase<typeof utils.normalize>[] = [
|
||||
["w/ 'Äpfel' it should replace the 'Ä' and return lowercased", ["Äpfel"], "apfel"],
|
||||
["w/ 'Été' it should replace the 'É' and 'é' and return lowercased", ["Été"], "ete"],
|
||||
["w/ 'FêTe' it should replace the 'ê' and return lowercased", ["FêTe"], "fete"],
|
||||
["w/ 'ΑλΦαβήΤα' it should replace the 'ή' and return lowercased", ["ΑλΦαβήΤα"], "αλφαβητα"],
|
||||
["w/ '' (empty string) it should return empty string", [""], ""],
|
||||
];
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [desc, fnParams, expected] = testCase;
|
||||
it(desc, () => {
|
||||
const result = utils.normalize(...fnParams);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("#toMap", () => {
|
||||
it("should return an instace of Map, with the correct size and keys, when supplied with a list and existing keys", () => {
|
||||
const testList = [{title: "test", propA: "text", propB: 123 }, {title: "test2", propA: "prop2", propB: 456 }];
|
||||
const result = utils.toMap(testList, "title");
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
expect(result.size).toBe(2);
|
||||
expect(Array.from(result.keys())).toStrictEqual(["test", "test2"]);
|
||||
});
|
||||
it("should return an instace of Map, with an empty size, when the supplied list does not contain the supplied key", () => {
|
||||
const testList = [{title: "test", propA: "text", propB: 123 }, {title: "test2", propA: "prop2", propB: 456 }];
|
||||
//@ts-expect-error - key is non-existing on supplied list type
|
||||
const result = utils.toMap(testList, "nonExistingKey");
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
it.fails("should correctly handle duplicate keys? (currently it will overwrite the entry, so returned size will be 1 instead of 2)", () => {
|
||||
const testList = [{title: "testDupeTitle", propA: "text", propB: 123 }, {title: "testDupeTitle", propA: "prop2", propB: 456 }];
|
||||
const result = utils.toMap(testList, "title");
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
expect(result.size).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#envToBoolean", () => {
|
||||
const testCases: TestCase<typeof utils.envToBoolean>[] = [
|
||||
["w/ 'true' it should return boolean 'true'", ["true"], true],
|
||||
["w/ 'True' it should return boolean 'true'", ["True"], true],
|
||||
["w/ 'TRUE' it should return boolean 'true'", ["TRUE"], true],
|
||||
["w/ 'true ' it should return boolean 'true'", ["true "], true],
|
||||
["w/ 'false' it should return boolean 'false'", ["false"], false],
|
||||
["w/ 'False' it should return boolean 'false'", ["False"], false],
|
||||
["w/ 'FALSE' it should return boolean 'false'", ["FALSE"], false],
|
||||
["w/ 'false ' it should return boolean 'false'", ["false "], false],
|
||||
["w/ 'whatever' (non-boolean string) it should return undefined", ["whatever"], undefined],
|
||||
["w/ '-' (non-boolean string) it should return undefined", ["-"], undefined],
|
||||
["w/ '' (empty string) it should return undefined", [""], undefined],
|
||||
["w/ ' ' (white space string) it should return undefined", [" "], undefined],
|
||||
["w/ undefined it should return undefined", [undefined], undefined],
|
||||
//@ts-expect-error - pass wrong type as param
|
||||
["w/ number 1 it should return undefined", [1], undefined],
|
||||
];
|
||||
|
||||
testCases.forEach(testCase => {
|
||||
const [desc, fnParams, expected] = testCase;
|
||||
it(desc, () => {
|
||||
const result = utils.envToBoolean(...fnParams);
|
||||
expect(result).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.todo("#getResourceDir", () => {});
|
||||
|
||||
describe("#isElectron", () => {
|
||||
it("should export a boolean", () => {
|
||||
expect(utils.isElectron).toBeTypeOf("boolean");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#isMac", () => {
|
||||
it("should export a boolean", () => {
|
||||
expect(utils.isMac).toBeTypeOf("boolean");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#isWindows", () => {
|
||||
it("should export a boolean", () => {
|
||||
expect(utils.isWindows).toBeTypeOf("boolean");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#isDev", () => {
|
||||
it("should export a boolean", () => {
|
||||
expect(utils.isDev).toBeTypeOf("boolean");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#formatDownloadTitle", () => {
|
||||
|
||||
//prettier-ignore
|
||||
const testCases: [fnValue: Parameters<typeof utils.formatDownloadTitle>, expectedValue: ReturnType<typeof utils.formatDownloadTitle>][] = [
|
||||
|
||||
// empty fileName tests
|
||||
[
|
||||
["", "text", ""],
|
||||
"untitled.html"
|
||||
],
|
||||
[
|
||||
["", "canvas", ""],
|
||||
"untitled.json"
|
||||
],
|
||||
[
|
||||
["", null, ""],
|
||||
"untitled"
|
||||
],
|
||||
|
||||
|
||||
// json extension from type tests
|
||||
[
|
||||
["test_file", "canvas", ""],
|
||||
"test_file.json"
|
||||
],
|
||||
[
|
||||
["test_file", "relationMap", ""],
|
||||
"test_file.json"
|
||||
],
|
||||
[
|
||||
["test_file", "search", ""],
|
||||
"test_file.json"
|
||||
],
|
||||
|
||||
|
||||
// extension based on mime type
|
||||
[
|
||||
["test_file", null, "text/csv"],
|
||||
"test_file.csv"
|
||||
],
|
||||
[
|
||||
["test_file_wo_ext", "image", "image/svg+xml"],
|
||||
"test_file_wo_ext.svg"
|
||||
],
|
||||
[
|
||||
["test_file_wo_ext", "file", "application/json"],
|
||||
"test_file_wo_ext.json"
|
||||
],
|
||||
[
|
||||
["test_file_w_fake_ext.ext", "image", "image/svg+xml"],
|
||||
"test_file_w_fake_ext.ext.svg"
|
||||
],
|
||||
[
|
||||
["test_file_w_correct_ext.svg", "image", "image/svg+xml"],
|
||||
"test_file_w_correct_ext.svg"
|
||||
],
|
||||
[
|
||||
["test_file_w_correct_ext.svgz", "image", "image/svg+xml"],
|
||||
"test_file_w_correct_ext.svgz"
|
||||
],
|
||||
[
|
||||
["test_file.zip", "file", "application/zip"],
|
||||
"test_file.zip"
|
||||
],
|
||||
[
|
||||
["test_file", "file", "application/zip"],
|
||||
"test_file.zip"
|
||||
],
|
||||
|
||||
|
||||
// application/octet-stream tests
|
||||
[
|
||||
["test_file", "file", "application/octet-stream"],
|
||||
"test_file"
|
||||
],
|
||||
[
|
||||
["test_file.zip", "file", "application/octet-stream"],
|
||||
"test_file.zip"
|
||||
],
|
||||
[
|
||||
["test_file.unknown", null, "application/octet-stream"],
|
||||
"test_file.unknown"
|
||||
],
|
||||
|
||||
|
||||
// sanitized filename tests
|
||||
[
|
||||
["test/file", null, "application/octet-stream"],
|
||||
"testfile"
|
||||
],
|
||||
[
|
||||
["test:file.zip", "file", "application/zip"],
|
||||
"testfile.zip"
|
||||
],
|
||||
[
|
||||
[":::", "file", "application/zip"],
|
||||
".zip"
|
||||
],
|
||||
[
|
||||
[":::a", "file", "application/zip"],
|
||||
"a.zip"
|
||||
]
|
||||
];
|
||||
|
||||
testCases.forEach((testCase) => {
|
||||
const [fnParams, expected] = testCase;
|
||||
return it(`With args '${JSON.stringify(fnParams)}', it should return '${expected}'`, () => {
|
||||
const actual = utils.formatDownloadTitle(...fnParams);
|
||||
expect(actual).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
@ -9,6 +9,7 @@ import mimeTypes from "mime-types";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
import type NoteMeta from "./meta/note_meta.js";
|
||||
|
||||
const randtoken = generator({ source: "crypto" });
|
||||
|
||||
@ -71,21 +72,18 @@ export function hash(text: string) {
|
||||
return crypto.createHash("sha1").update(text).digest("base64");
|
||||
}
|
||||
|
||||
export function isEmptyOrWhitespace(str: string) {
|
||||
return str === null || str.match(/^ *$/) !== null;
|
||||
export function isEmptyOrWhitespace(str: string | null | undefined) {
|
||||
if (!str) return true;
|
||||
return str.match(/^ *$/) !== null;
|
||||
}
|
||||
|
||||
export function sanitizeSqlIdentifier(str: string) {
|
||||
return str.replace(/[^A-Za-z0-9_]/g, "");
|
||||
}
|
||||
|
||||
export function escapeHtml(str: string) {
|
||||
return escape(str);
|
||||
}
|
||||
export const escapeHtml = escape;
|
||||
|
||||
export function unescapeHtml(str: string) {
|
||||
return unescape(str);
|
||||
}
|
||||
export const unescapeHtml = unescape;
|
||||
|
||||
export function toObject<T, K extends string | number | symbol, V>(array: T[], fn: (item: T) => [K, V]): Record<K, V> {
|
||||
const obj: Record<K, V> = {} as Record<K, V>; // TODO: unsafe?
|
||||
@ -103,29 +101,6 @@ export function stripTags(text: string) {
|
||||
return text.replace(/<(?:.|\n)*?>/gm, "");
|
||||
}
|
||||
|
||||
export function union<T extends string | number | symbol>(a: T[], b: T[]): T[] {
|
||||
const obj: Record<T, T> = {} as Record<T, T>; // TODO: unsafe?
|
||||
|
||||
for (let i = a.length - 1; i >= 0; i--) {
|
||||
obj[a[i]] = a[i];
|
||||
}
|
||||
|
||||
for (let i = b.length - 1; i >= 0; i--) {
|
||||
obj[b[i]] = b[i];
|
||||
}
|
||||
|
||||
const res: T[] = [];
|
||||
|
||||
for (const k in obj) {
|
||||
if (obj.hasOwnProperty(k)) {
|
||||
// <-- optional
|
||||
res.push(obj[k]);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export function escapeRegExp(str: string) {
|
||||
return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
|
||||
}
|
||||
@ -138,27 +113,18 @@ export async function crash() {
|
||||
}
|
||||
}
|
||||
|
||||
export function sanitizeFilenameForHeader(filename: string) {
|
||||
let sanitizedFilename = sanitize(filename);
|
||||
|
||||
if (sanitizedFilename.trim().length === 0) {
|
||||
sanitizedFilename = "file";
|
||||
}
|
||||
|
||||
return encodeURIComponent(sanitizedFilename);
|
||||
}
|
||||
|
||||
export function getContentDisposition(filename: string) {
|
||||
const sanitizedFilename = sanitizeFilenameForHeader(filename);
|
||||
|
||||
return `file; filename="${sanitizedFilename}"; filename*=UTF-8''${sanitizedFilename}`;
|
||||
const sanitizedFilename = sanitize(filename).trim() || "file";
|
||||
const uriEncodedFilename = encodeURIComponent(sanitizedFilename);
|
||||
return `file; filename="${uriEncodedFilename}"; filename*=UTF-8''${uriEncodedFilename}`;
|
||||
}
|
||||
|
||||
// render and book are string note in the sense that they are expected to contain empty string
|
||||
const STRING_NOTE_TYPES = new Set(["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"]);
|
||||
const STRING_MIME_TYPES = new Set(["application/javascript", "application/x-javascript", "application/json", "application/x-sql", "image/svg+xml"]);
|
||||
|
||||
export function isStringNote(type: string | undefined, mime: string) {
|
||||
// render and book are string note in the sense that they are expected to contain empty string
|
||||
return (type && ["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"].includes(type)) || mime.startsWith("text/") || STRING_MIME_TYPES.has(mime);
|
||||
return (type && STRING_NOTE_TYPES.has(type)) || mime.startsWith("text/") || STRING_MIME_TYPES.has(mime);
|
||||
}
|
||||
|
||||
export function quoteRegex(url: string) {
|
||||
@ -211,26 +177,23 @@ export function removeTextFileExtension(filePath: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, noteMeta?: { title?: string }) {
|
||||
if (noteMeta?.title) {
|
||||
return noteMeta.title;
|
||||
} else {
|
||||
const basename = path.basename(removeTextFileExtension(filePath));
|
||||
if (replaceUnderscoresWithSpaces) {
|
||||
return basename.replace(/_/g, " ").trim();
|
||||
}
|
||||
return basename;
|
||||
}
|
||||
export function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, noteMeta?: NoteMeta) {
|
||||
const trimmedNoteMeta = noteMeta?.title?.trim();
|
||||
if (trimmedNoteMeta) return trimmedNoteMeta;
|
||||
|
||||
const basename = path.basename(removeTextFileExtension(filePath));
|
||||
return replaceUnderscoresWithSpaces ? basename.replace(/_/g, " ").trim() : basename;
|
||||
}
|
||||
|
||||
export function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?: string): Promise<T> {
|
||||
// TriliumNextTODO: since TS avoids this from ever happening – do we need this check?
|
||||
if (!promise || !promise.then) {
|
||||
// it's not actually a promise
|
||||
return promise;
|
||||
}
|
||||
|
||||
// better stack trace if created outside of promise
|
||||
const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
|
||||
const errorTimeLimit = new Error(errorMessage || `Process exceeded time limit ${limitMs}`);
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
let resolved = false;
|
||||
@ -245,7 +208,7 @@ export function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?
|
||||
|
||||
setTimeout(() => {
|
||||
if (!resolved) {
|
||||
rej(error);
|
||||
rej(errorTimeLimit);
|
||||
}
|
||||
}, limitMs);
|
||||
});
|
||||
@ -284,20 +247,18 @@ export function normalize(str: string) {
|
||||
return removeDiacritic(str).toLowerCase();
|
||||
}
|
||||
|
||||
export function toMap<T extends Record<string, any>>(list: T[], key: keyof T): Record<string, T> {
|
||||
const map: Record<string, T> = {};
|
||||
|
||||
export function toMap<T extends Record<string, any>>(list: T[], key: keyof T) {
|
||||
const map = new Map<string, T>();
|
||||
for (const el of list) {
|
||||
map[el[key]] = el;
|
||||
const keyForMap = el[key];
|
||||
if (!keyForMap) continue;
|
||||
// TriliumNextTODO: do we need to handle the case when the same key is used?
|
||||
// currently this will overwrite the existing entry in the map
|
||||
map.set(keyForMap, el);
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
export function isString(x: any) {
|
||||
return Object.prototype.toString.call(x) === "[object String]";
|
||||
}
|
||||
|
||||
// try to turn 'true' and 'false' strings from process.env variables into boolean values or undefined
|
||||
export function envToBoolean(val: string | undefined) {
|
||||
if (val === undefined || typeof val !== "string") return undefined;
|
||||
@ -317,48 +278,87 @@ export function envToBoolean(val: string | undefined) {
|
||||
* @returns the resource dir.
|
||||
*/
|
||||
export function getResourceDir() {
|
||||
if (isElectron && !isDev) {
|
||||
return process.resourcesPath;
|
||||
} else {
|
||||
return join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
if (isElectron && !isDev) return process.resourcesPath;
|
||||
return join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with src/public/app/services/utils.ts
|
||||
/**
|
||||
* Compares two semantic version strings.
|
||||
* Returns:
|
||||
* 1 if v1 is greater than v2
|
||||
* 0 if v1 is equal to v2
|
||||
* -1 if v1 is less than v2
|
||||
*
|
||||
* @param v1 First version string
|
||||
* @param v2 Second version string
|
||||
* @returns
|
||||
*/
|
||||
function compareVersions(v1: string, v2: string): number {
|
||||
// Remove 'v' prefix and everything after dash if present
|
||||
v1 = v1.replace(/^v/, "").split("-")[0];
|
||||
v2 = v2.replace(/^v/, "").split("-")[0];
|
||||
|
||||
const v1parts = v1.split(".").map(Number);
|
||||
const v2parts = v2.split(".").map(Number);
|
||||
|
||||
// Pad shorter version with zeros
|
||||
while (v1parts.length < 3) v1parts.push(0);
|
||||
while (v2parts.length < 3) v2parts.push(0);
|
||||
|
||||
// Compare major version
|
||||
if (v1parts[0] !== v2parts[0]) {
|
||||
return v1parts[0] > v2parts[0] ? 1 : -1;
|
||||
}
|
||||
|
||||
// Compare minor version
|
||||
if (v1parts[1] !== v2parts[1]) {
|
||||
return v1parts[1] > v2parts[1] ? 1 : -1;
|
||||
}
|
||||
|
||||
// Compare patch version
|
||||
if (v1parts[2] !== v2parts[2]) {
|
||||
return v1parts[2] > v2parts[2] ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export default {
|
||||
randomSecureToken,
|
||||
randomString,
|
||||
compareVersions,
|
||||
crash,
|
||||
deferred,
|
||||
envToBoolean,
|
||||
escapeHtml,
|
||||
escapeRegExp,
|
||||
formatDownloadTitle,
|
||||
fromBase64,
|
||||
getContentDisposition,
|
||||
getNoteTitle,
|
||||
getResourceDir,
|
||||
hash,
|
||||
hashedBlobId,
|
||||
hmac,
|
||||
isDev,
|
||||
isElectron,
|
||||
isEmptyOrWhitespace,
|
||||
isMac,
|
||||
isStringNote,
|
||||
isWindows,
|
||||
md5,
|
||||
newEntityId,
|
||||
toBase64,
|
||||
fromBase64,
|
||||
hmac,
|
||||
isElectron,
|
||||
hash,
|
||||
isEmptyOrWhitespace,
|
||||
sanitizeSqlIdentifier,
|
||||
escapeHtml,
|
||||
unescapeHtml,
|
||||
toObject,
|
||||
stripTags,
|
||||
union,
|
||||
escapeRegExp,
|
||||
crash,
|
||||
getContentDisposition,
|
||||
isStringNote,
|
||||
quoteRegex,
|
||||
replaceAll,
|
||||
getNoteTitle,
|
||||
removeTextFileExtension,
|
||||
formatDownloadTitle,
|
||||
timeLimit,
|
||||
deferred,
|
||||
removeDiacritic,
|
||||
normalize,
|
||||
hashedBlobId,
|
||||
quoteRegex,
|
||||
randomSecureToken,
|
||||
randomString,
|
||||
removeDiacritic,
|
||||
removeTextFileExtension,
|
||||
replaceAll,
|
||||
sanitizeSqlIdentifier,
|
||||
stripTags,
|
||||
timeLimit,
|
||||
toBase64,
|
||||
toMap,
|
||||
isString,
|
||||
getResourceDir,
|
||||
isMac,
|
||||
isWindows,
|
||||
envToBoolean
|
||||
toObject,
|
||||
unescapeHtml
|
||||
};
|
||||
|
@ -12,7 +12,8 @@ import ws from "./services/ws.js";
|
||||
import utils from "./services/utils.js";
|
||||
import port from "./services/port.js";
|
||||
import host from "./services/host.js";
|
||||
import semver from "semver";
|
||||
|
||||
const MINIMUM_NODE_VERSION = "22.0.0";
|
||||
|
||||
// setup basic error handling even before requiring dependencies, since those can produce errors as well
|
||||
|
||||
@ -32,8 +33,8 @@ function exit() {
|
||||
process.on("SIGINT", exit);
|
||||
process.on("SIGTERM", exit);
|
||||
|
||||
if (!semver.satisfies(process.version, ">=10.5.0")) {
|
||||
console.error("Trilium only supports node.js 10.5 and later");
|
||||
if (utils.compareVersions(process.version, MINIMUM_NODE_VERSION) < 0) {
|
||||
console.error(`\nTrilium requires Node.js ${MINIMUM_NODE_VERSION} and later.\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user