Compare commits

...

125 Commits

Author SHA1 Message Date
Elian Doran
d873accf3e Merge remote-tracking branch 'origin/main' into feature/cleanup_ck_modules 2026-04-06 12:41:45 +03:00
Elian Doran
94b448863c chore(deps): update dependency esbuild to v0.28.0 (#9302) 2026-04-06 12:34:15 +03:00
Elian Doran
32acc8555d fix(deps): update dependency @eslint/js to v10 (#9308) 2026-04-06 12:33:56 +03:00
Elian Doran
d68ad84155 Refactor/build warnings due to imports (#9309) 2026-04-06 12:32:32 +03:00
Elian Doran
45e82b7f33 test(ckeditor5-mermaid): fix type errors 2026-04-06 12:31:53 +03:00
Elian Doran
55ad0fe9f0 test(ckeditor5-mermaid): broken tests after change in rendering 2026-04-06 12:30:33 +03:00
Elian Doran
559815273e fix(ckeditor5-mermaid): protect against multiple init 2026-04-06 12:27:12 +03:00
Elian Doran
af76740fd9 fix(ckeditor5-mermaid): protect against stale renders 2026-04-06 12:26:17 +03:00
Elian Doran
7dadd50bfe chore(ckeditor5-mermaid): don't remove parent element on error 2026-04-06 12:25:50 +03:00
Elian Doran
dd4cab22c1 chore(client): address requested changes 2026-04-06 12:22:00 +03:00
Elian Doran
c4d3e776a1 refactor(client): the last circular dependency 2026-04-06 12:20:40 +03:00
Elian Doran
19bb7f5ddb refactor(server): remove unnecessary route 2026-04-06 12:18:36 +03:00
Elian Doran
d212120f9b refactor(client): read locales from common instead of going through the server 2026-04-06 12:16:32 +03:00
Elian Doran
42da1872e7 fix(client): crashing due to circular dependency 2026-04-06 12:10:33 +03:00
Elian Doran
a080b50c45 refactor(client): duplicate toast import 2026-04-06 12:06:27 +03:00
Elian Doran
6d31e9b028 feat(ckeditor5-mermaid): use more modern mechanism for rendering with less flicker 2026-04-06 12:03:29 +03:00
Elian Doran
b606afa858 refactor(ckeditor5-mermaid): get rid of any runtime dependencies 2026-04-06 11:59:24 +03:00
Elian Doran
f9446304b3 refactor(ckeditor5-mermaid): switch to es-toolkit 2026-04-06 11:57:31 +03:00
renovate[bot]
fbe312d580 chore(deps): update dependency esbuild to v0.28.0 2026-04-06 08:54:56 +00:00
Elian Doran
8d383caaff chore(deps): update dependency @electron/fuses to v2 (#9304) 2026-04-06 11:53:07 +03:00
Elian Doran
6caf4fa7ce chore(deps): update dependency copy-webpack-plugin to v14 (#9305) 2026-04-06 11:52:29 +03:00
Elian Doran
606d58b08c chore(ckeditor5-*): remove unnecessary publishing stack 2026-04-06 11:49:47 +03:00
Elian Doran
09258179f0 refactor(client): one more ineffective dynamic import due to appContext 2026-04-06 11:46:35 +03:00
Elian Doran
40e986b188 refactor(client): ineffective dynamic imports 2026-04-06 11:41:35 +03:00
Elian Doran
37e47041bf Merge branch 'main' of https://github.com/TriliumNext/Trilium 2026-04-06 11:41:09 +03:00
Elian Doran
543438bca0 chore(ai): mention instructions for adding new locale 2026-04-06 11:28:10 +03:00
Elian Doran
b31290c1fc fix(deps): update dependency fuse.js to v7.2.0 (#9303) 2026-04-06 11:22:33 +03:00
Elian Doran
d41111a209 docs(dev): remove unnecessary step for adding new locale 2026-04-06 11:21:17 +03:00
Elian Doran
828b523382 feat(i18n): enable Czech 2026-04-06 11:20:58 +03:00
Elian Doran
32409ecbee Translations update from Hosted Weblate (#9295) 2026-04-06 11:16:03 +03:00
renovate[bot]
3ca2cec63a chore(deps): update dependency copy-webpack-plugin to v14 2026-04-06 08:14:46 +00:00
Francis C.
1ed2db0c82 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (1842 of 1842 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hant/
2026-04-06 08:13:48 +00:00
Francis C.
2423b74dd0 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/zh_Hant/
2026-04-06 08:13:48 +00:00
Tomas Adamek
3f781ea298 Translated using Weblate (Czech)
Currently translated at 98.9% (1822 of 1842 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/cs/
2026-04-06 08:13:47 +00:00
Tomas Adamek
30c5c49aef Translated using Weblate (Czech)
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/cs/
2026-04-06 08:13:47 +00:00
Tomas Adamek
9421e39c34 Translated using Weblate (Czech)
Currently translated at 100.0% (116 of 116 strings)

Translation: Trilium Notes/README
Translate-URL: https://hosted.weblate.org/projects/trilium/readme/cs/
2026-04-06 08:13:46 +00:00
Tomas Adamek
c46805cf4f Translated using Weblate (Czech)
Currently translated at 100.0% (158 of 158 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/cs/
2026-04-06 08:13:46 +00:00
Elian Doran
f181343fca chore(deps): update dependency @redocly/cli to v2.25.4 (#9297) 2026-04-06 11:13:37 +03:00
Elian Doran
8a512e4f73 chore(deps): update dependency electron to v41 (#9306) 2026-04-06 11:13:01 +03:00
Elian Doran
06a3750168 chore(renovate): group AI SDK updates 2026-04-06 11:09:07 +03:00
renovate[bot]
35c1a5642d fix(deps): update dependency @eslint/js to v10 2026-04-06 01:05:51 +00:00
renovate[bot]
f29df2ad28 chore(deps): update dependency electron to v41 2026-04-06 01:03:55 +00:00
renovate[bot]
75a5714451 chore(deps): update dependency @electron/fuses to v2 2026-04-06 01:01:53 +00:00
renovate[bot]
2882863b5b fix(deps): update dependency fuse.js to v7.2.0 2026-04-06 01:00:57 +00:00
renovate[bot]
773b6cca14 chore(deps): update dependency @redocly/cli to v2.25.4 2026-04-06 00:54:32 +00:00
Elian Doran
f97370c8f7 Dependency cleanup (#9293) 2026-04-05 23:04:03 +03:00
Elian Doran
afad96a375 Merge remote-tracking branch 'origin/main' into feature/dependency_cleanup 2026-04-05 22:56:13 +03:00
Elian Doran
9e5ababfcb chore(deps): update dependency electron to v41.1.1 (#9277) 2026-04-05 22:51:05 +03:00
Elian Doran
dc1e0e8db4 fix(desktop): tesseract.js not copied 2026-04-05 22:22:58 +03:00
Elian Doran
1e861d1125 chore(ocr): externalize tesseract.js completely 2026-04-05 22:20:38 +03:00
Elian Doran
baa93cb371 chore(ocr): expose needed dependencies 2026-04-05 22:14:01 +03:00
Elian Doran
61dcc8db47 Revert "fix(ocr): not working in server prod"
This reverts commit f4f881e839.
2026-04-05 21:53:53 +03:00
Elian Doran
2c557eb015 Revert "fix(desktop): failing in prod due to tesseract"
This reverts commit 9e34fcb8a8.
2026-04-05 21:36:11 +03:00
Elian Doran
f5a80526ab fix(deps): update dependency mermaid to v11.14.0 (#9282) 2026-04-05 21:35:45 +03:00
Elian Doran
27e1455874 fix(mermaid): treeview clipped when padding 2026-04-05 21:27:39 +03:00
Elian Doran
278d8428de feat(mermaid): integrate two new note types 2026-04-05 21:26:42 +03:00
Elian Doran
164e667158 chore: remove empty dependencies list in JSON 2026-04-05 21:05:11 +03:00
Elian Doran
28b31791e7 fix(codemirror): broken dependency on electron-window-state 2026-04-05 21:03:24 +03:00
Elian Doran
9515768e62 fix(server): broken dependency on electron-window-state 2026-04-05 21:02:03 +03:00
Elian Doran
fbbad19cb7 chore(deps): update dependency electron to v40.8.5 [security] (#9291) 2026-04-05 20:59:58 +03:00
Elian Doran
eab353ca2e chore(deps): remove unnecessary depedencies 2026-04-05 20:58:02 +03:00
Elian Doran
cb9ee20763 chore(deps): remove hard-coded dependency to @smithy/middleware-retry 2026-04-05 20:55:43 +03:00
Elian Doran
dac12532bc Merge branch 'main' into renovate/electron-41.x 2026-04-05 20:49:33 +03:00
Elian Doran
1d99734ea0 chore(ci): try to bypass operation not permitted in Electron build
7
node_modules/fs-xattr install: gyp http 200 https://nodejs.org/download/release/v24.14.1/node-v24.14.1-headers.tar.gz
node_modules/wxt/node_modules/esbuild postinstall$ node install.js
node_modules/wxt/node_modules/esbuild postinstall: Done
node_modules/macos-alias install: gyp http GET https://nodejs.org/download/release/v24.14.1/SHASUMS256.txt
node_modules/macos-alias install: gyp http 200 https://nodejs.org/download/release/v24.14.1/SHASUMS256.txt
node_modules/fs-xattr install: gyp http GET https://nodejs.org/download/release/v24.14.1/SHASUMS256.txt
node_modules/fs-xattr install: gyp http 200 https://nodejs.org/download/release/v24.14.1/SHASUMS256.txt
node_modules/electron postinstall: Done
.../remote/node_modules/electron postinstall: Done
 ERR_PNPM_EPERM  EPERM: operation not permitted, link '/Users/runner/work/Trilium/Trilium/node_modules/@electron/remote/node_modules/electron/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Helpers' -> 'apps/desktop/node_modules/_tmp_3196_65c494775712c8b30c73644d84dc191e/dist/Electron.app/Contents/Frameworks/Electron Framework.framework/Helpers'
2026-04-05 20:49:06 +03:00
Elian Doran
3e764c762a chore(desktop): remove unnecessary dependencies 2026-04-05 20:43:37 +03:00
Elian Doran
7be51168d3 Merge branch 'main' into renovate/electron-41.x 2026-04-05 20:38:36 +03:00
Elian Doran
530d193734 fix(forge): build no longer working due to audit 2026-04-05 20:37:33 +03:00
Elian Doran
aba5ff75af fix(server): sync version not increased after breaking changes 2026-04-05 20:22:49 +03:00
Elian Doran
9e34fcb8a8 fix(desktop): failing in prod due to tesseract 2026-04-05 20:15:08 +03:00
Elian Doran
055dd9cd01 chore(toast): fix button alignment if no title & make buttons full-width 2026-04-05 20:14:54 +03:00
Elian Doran
1437fdc4e3 feat(ocr): warn if text wasn't retrieved on manual to due low confidence 2026-04-05 20:14:38 +03:00
Elian Doran
e5c67b16ac fix(flake): failing due to symlinks to /build 2026-04-05 20:12:59 +03:00
Elian Doran
94987314b8 feat(ocr): warn about OCR confidence too low 2026-04-05 20:03:12 +03:00
Elian Doran
f4f881e839 fix(ocr): not working in server prod 2026-04-05 19:58:48 +03:00
renovate[bot]
92f5901b95 chore(deps): update dependency electron to v41.1.1 2026-04-05 16:44:14 +00:00
renovate[bot]
1c0cb601cb chore(deps): update dependency electron to v40.8.5 [security] 2026-04-05 16:43:32 +00:00
Elian Doran
109f06f8bb Merge branch 'release/v0.102.2'
; Conflicts:
;	apps/desktop/package.json
;	apps/server/src/routes/api/image.ts
;	apps/server/src/share/routes.ts
;	pnpm-lock.yaml
2026-04-05 19:41:24 +03:00
Elian Doran
bf23439792 chore(release): prepare for v0.102.2 2026-04-05 19:30:04 +03:00
Elian Doran
b7a0bc08be Various bugfixes (#9274) 2026-04-05 19:28:59 +03:00
Elian Doran
9d6a26dda9 docs(security): add more details & change reporting mechanism 2026-04-05 19:28:30 +03:00
Elian Doran
a01ce2c3fc docs(release): release notes for v0.102.2 2026-04-05 19:28:03 +03:00
Elian Doran
ba6298af27 Translations update from Hosted Weblate (#9289) 2026-04-05 17:11:26 +03:00
green
3d17e0aa75 Translated using Weblate (Japanese)
Currently translated at 100.0% (1837 of 1837 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ja/
2026-04-05 14:10:34 +00:00
Elian Doran
7e18166160 chore(deps): update dependency esbuild to v0.27.5 (#9278) 2026-04-05 17:10:26 +03:00
Elian Doran
40d8571797 Translations update from Hosted Weblate (#9288) 2026-04-05 17:09:36 +03:00
Elian Doran
25e04e358a Apply suggestions from code review
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-05 17:08:56 +03:00
Aindriú Mac Giolla Eoin
e473e12c0e Translated using Weblate (Irish)
Currently translated at 100.0% (1837 of 1837 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/ga/
2026-04-05 14:09:52 +02:00
Aindriú Mac Giolla Eoin
dfb20df16f Translated using Weblate (Irish)
Currently translated at 100.0% (391 of 391 strings)

Translation: Trilium Notes/Server
Translate-URL: https://hosted.weblate.org/projects/trilium/server/ga/
2026-04-05 14:09:49 +02:00
Elian Doran
efcbf439ee chore(deps): update dependency http-proxy-agent to v9 (#9283) 2026-04-05 13:54:41 +03:00
renovate[bot]
514f7fedbc chore(deps): update dependency http-proxy-agent to v9 2026-04-05 10:35:14 +00:00
Elian Doran
ee88fedacd chore(deps): update dependency https-proxy-agent to v9 (#9284) 2026-04-05 13:32:40 +03:00
renovate[bot]
2933f9c49f chore(deps): update dependency esbuild to v0.27.5 2026-04-05 10:26:58 +00:00
Elian Doran
1cca5d989c chore(deps): update dependency @playwright/test to v1.59.1 (#9276) 2026-04-05 13:25:47 +03:00
Elian Doran
9981020728 chore(deps): update dependency dotenv to v17.4.0 (#9280) 2026-04-05 13:25:17 +03:00
Elian Doran
56843dcf8b chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v55.3.1 (#9275) 2026-04-05 13:24:29 +03:00
Elian Doran
e661118192 fix(deps): update dependency @codemirror/view to v6.41.0 (#9281) 2026-04-05 13:23:16 +03:00
Elian Doran
54a7de6cb0 fix(deps): update dependency mathlive to v0.109.1 (#9279) 2026-04-05 13:22:48 +03:00
Elian Doran
13b1e0afbb fix(desktop): make failing due to wrong version of fuses 2026-04-05 12:46:39 +03:00
Elian Doran
4a48796142 chore(ci): trigger dev on release branches as well 2026-04-05 12:37:33 +03:00
Elian Doran
9a4fef80b9 chore(deps): fix pnpm lock 2026-04-05 12:15:07 +03:00
Elian Doran
79dc4b39f1 chore(client): address requested changes 2026-04-05 12:11:05 +03:00
Elian Doran
9bc18b774e test(server): add unit tests for sanitizeSvg 2026-04-05 12:11:05 +03:00
Elian Doran
465c36407c Update apps/server/src/etapi/notes.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 12:10:52 +03:00
Elian Doran
b99486259e Update apps/server/src/etapi/notes.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 12:10:44 +03:00
Elian Doran
ecf5475966 Update apps/desktop/package.json
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-05 12:10:29 +03:00
Elian Doran
90822cc8a3 chore: address requested changes 2026-04-05 11:59:45 +03:00
Elian Doran
5c46209ddc feat(server): improve request handling for SVGs 2026-04-05 11:28:28 +03:00
Elian Doran
176de87b6b feat(desktop): add Electron fuses 2026-04-05 11:01:22 +03:00
Elian Doran
7f199c527b feat(share): improve request handling for SVGs 2026-04-05 10:52:36 +03:00
Elian Doran
2432e230c5 chore(etapi): enforce MIME for image upload 2026-04-05 10:44:47 +03:00
Elian Doran
fc1be0d23d fix(ckeditor5-mermaid): use textContent for diagram source rendering 2026-04-05 10:17:16 +03:00
renovate[bot]
d084b9e941 chore(deps): update dependency https-proxy-agent to v9 2026-04-05 01:33:43 +00:00
renovate[bot]
6678c0af49 fix(deps): update dependency mermaid to v11.14.0 2026-04-05 01:32:26 +00:00
renovate[bot]
37754ecf31 fix(deps): update dependency @codemirror/view to v6.41.0 2026-04-05 01:31:45 +00:00
renovate[bot]
709d9633a1 chore(deps): update dependency dotenv to v17.4.0 2026-04-05 01:31:06 +00:00
renovate[bot]
7ca57efaad fix(deps): update dependency mathlive to v0.109.1 2026-04-05 01:30:27 +00:00
renovate[bot]
342fedca1c chore(deps): update dependency @playwright/test to v1.59.1 2026-04-05 01:28:20 +00:00
renovate[bot]
b1262b0448 chore(deps): update dependency @ckeditor/ckeditor5-dev-build-tools to v55.3.1 2026-04-05 01:27:37 +00:00
Elian Doran
626aca5181 fix(client): toasts could render HTML content 2026-04-04 22:21:25 +03:00
Elian Doran
8204322b46 fix(openid): use more secure RNG 2026-04-04 22:02:33 +03:00
Elian Doran
70ce86cd53 fix(scripts): electron rebuild failing in flake 2026-04-04 22:01:43 +03:00
Elian Doran
ed3b86cd49 fix(import): no longer preserve named note IDs 2026-04-04 21:27:37 +03:00
Elian Doran
b371675494 chore(commons): mark docName as a dangerous attribute 2026-04-04 21:25:05 +03:00
Elian Doran
ff06c8e7bd fix(client): validate docName attribute in doc renderer 2026-04-04 21:21:50 +03:00
Elian Doran
8ff41d8fa9 fix(server): align attachment upload validation with note upload 2026-04-04 20:46:03 +03:00
105 changed files with 4942 additions and 2588 deletions

View File

@@ -320,6 +320,7 @@ Trilium provides powerful user scripting capabilities:
- Use translation system via `t()` function
- Automatic pluralization: Add `_other` suffix to translation keys (e.g., `item` and `item_other` for singular/plural)
- When a translated string contains **interpolated components** (e.g. links, note references) whose order may vary across languages, use `<Trans>` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `"<Note/> in <Parent/>"` vs `"in <Parent/>, <Note/>"`)
- When adding a new locale, follow the step-by-step guide in `docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md`
## Testing Conventions

View File

@@ -1,9 +1,13 @@
name: Dev
on:
push:
branches: [ main ]
branches:
- main
- "release/*"
pull_request:
branches: [ main ]
branches:
- main
- "release/*"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}

View File

@@ -69,6 +69,8 @@ jobs:
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
env:
npm_config_package_import_method: copy
- name: Update nightly version
run: pnpm run chore:ci-update-nightly-version
- name: Run the build

View File

@@ -121,6 +121,7 @@ Trilium provides powerful user scripting capabilities:
- **Only add new translation keys to `en/translation.json`** — translations for other languages are managed via Weblate and will be contributed by the community
- Third-party components (e.g., mind-map context menu) should use i18next `t()` for their labels, with the English strings added to `en/translation.json` under a dedicated namespace (e.g., `"mind-map"`)
- When a translated string contains **interpolated components** (e.g. links, note references) whose order may vary across languages, use `<Trans>` from `react-i18next` instead of `t()`. This lets translators reorder components freely (e.g. `"<Note/> in <Parent/>"` vs `"in <Parent/>, <Note/>"`)
- When adding a new locale, follow the step-by-step guide in `docs/Developer Guide/Developer Guide/Concepts/Internationalisation Translations/Adding a new locale.md`
### Security Considerations
- Per-note encryption with granular protected sessions

View File

@@ -2,13 +2,87 @@
## Supported Versions
In the (still active) 0.X phase of the project only the latest stable minor release is getting bugfixes (including security ones).
Only the latest stable minor release receives security fixes.
So e.g. if the latest stable version is 0.42.3 and the latest beta version is 0.43.0-beta, then 0.42 line will still get security fixes but older versions (like 0.41.X) won't get any fixes.
For example, if the latest stable version is 0.92.3 and the latest beta is 0.93.0-beta, then only the 0.92.x line will receive security patches. Older versions (like 0.91.x) will not receive fixes.
Description above is a general rule and may be altered on case by case basis.
This policy may be altered on a case-by-case basis for critical vulnerabilities.
## Reporting a Vulnerability
* For low severity vulnerabilities, they can be reported as GitHub issues.
* For severe vulnerabilities, please report it using [GitHub Security Advisories](https://github.com/TriliumNext/Trilium/security/advisories).
**Please report all security vulnerabilities through [GitHub Security Advisories](https://github.com/TriliumNext/Notes/security/advisories/new).**
We do not accept security reports via email, public issues, or other channels. GitHub Security Advisories allows us to:
- Discuss and triage vulnerabilities privately
- Coordinate fixes before public disclosure
- Credit reporters appropriately
- Publish advisories with CVE identifiers
### What to Include
When reporting, please provide:
- A clear description of the vulnerability
- Steps to reproduce or proof-of-concept
- Affected versions (if known)
- Potential impact assessment
- Any suggested mitigations or fixes
### Response Timeline
- **Initial response**: Within 7 days
- **Triage decision**: Within 14 days
- **Fix timeline**: Depends on severity and complexity
## Scope
### In Scope
- Remote code execution
- Authentication/authorization bypass
- Cross-site scripting (XSS) that affects other users
- SQL injection
- Path traversal
- Sensitive data exposure
- Privilege escalation
### Out of Scope (Won't Fix)
The following are considered out of scope or accepted risks:
#### Self-XSS / Self-Injection
Trilium is a personal knowledge base where users have full control over their own data. Users can intentionally create notes containing scripts, HTML, or other executable content. This is by design - Trilium's scripting system allows users to extend functionality with custom JavaScript.
Vulnerabilities that require a user to inject malicious content into their own notes and then view it themselves are not considered security issues.
#### Electron Architecture (nodeIntegration)
Trilium's desktop application runs with `nodeIntegration: true` to enable its powerful scripting features. This is an intentional design decision, similar to VS Code extensions having full system access. We mitigate risks by:
- Sanitizing content at input boundaries
- Fixing specific XSS vectors as they're discovered
- Using Electron fuses to prevent external abuse
#### Authenticated User Actions
Actions that require valid authentication and only affect the authenticated user's own data are generally not vulnerabilities.
#### Denial of Service via Resource Exhaustion
Creating extremely large notes or performing many operations is expected user behavior in a note-taking application.
#### Missing Security Headers on Non-Sensitive Endpoints
We implement security headers where they provide meaningful protection, but may omit them on endpoints where they provide no practical benefit.
## Coordinated Disclosure
We follow a coordinated disclosure process:
1. **Report received** - We acknowledge receipt and begin triage
2. **Fix developed** - We develop and test a fix privately
3. **Release prepared** - Security release is prepared with vague changelog
4. **Users notified** - Release is published, users encouraged to upgrade
5. **Advisory published** - After reasonable upgrade window (typically 2-4 weeks), full advisory is published
We appreciate reporters allowing us time to fix issues before public disclosure. We aim to credit all reporters in published advisories unless they prefer to remain anonymous.
## Security Updates
Security fixes are released as patch versions (e.g., 0.92.1 → 0.92.2) to minimize upgrade friction. We recommend all users keep their installations up to date.
Subscribe to GitHub releases or watch the repository to receive notifications of new releases.

View File

@@ -16,12 +16,10 @@
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.33.0",
"devDependencies": {
"@redocly/cli": "2.25.3",
"@redocly/cli": "2.25.4",
"archiver": "7.0.1",
"fs-extra": "11.3.4",
"js-yaml": "4.1.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"typedoc": "0.28.18",
"typedoc-plugin-missing-exports": "4.1.2"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/client",
"version": "0.102.1",
"version": "0.102.2",
"description": "JQuery-based client for TriliumNext, used for both web and desktop (via Electron)",
"private": true,
"license": "AGPL-3.0-only",
@@ -27,7 +27,6 @@
"@maplibre/maplibre-gl-leaflet": "0.1.3",
"@mermaid-js/layout-elk": "0.2.1",
"@mind-elixir/node-menu": "5.0.1",
"@popperjs/core": "2.11.8",
"@preact/signals": "2.9.0",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
@@ -53,7 +52,6 @@
"dompurify": "3.3.3",
"draggabilly": "3.0.0",
"force-graph": "1.51.2",
"globals": "17.4.0",
"i18next": "26.0.3",
"i18next-http-backend": "3.0.4",
"jquery": "4.0.0",
@@ -64,9 +62,8 @@
"leaflet-gpx": "2.2.0",
"mark.js": "8.11.1",
"marked": "17.0.5",
"mermaid": "11.13.0",
"mermaid": "11.14.0",
"mind-elixir": "5.10.0",
"normalize.css": "8.0.1",
"panzoom": "9.4.4",
"preact": "10.29.0",
"react-i18next": "17.0.2",

View File

@@ -1,10 +1,11 @@
import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
import { SqlExecuteResponse } from "@triliumnext/commons";
import { type LOCALE_IDS, SqlExecuteResponse } from "@triliumnext/commons";
import type { NativeImage, TouchBar } from "electron";
import { ColumnComponent } from "tabulator-tables";
import type { Attribute } from "../services/attribute_parser.js";
import bundleService from "../services/bundle.js";
import froca from "../services/froca.js";
import { initLocale, t } from "../services/i18n.js";
import keyboardActionsService from "../services/keyboard_actions.js";
@@ -563,7 +564,7 @@ export class AppContext extends Component {
*/
async earlyInit() {
await options.initializedPromise;
await initLocale();
await initLocale((options.get("locale") || "en") as LOCALE_IDS);
}
setLayout(layout: Layout) {
@@ -578,7 +579,6 @@ export class AppContext extends Component {
this.tabManager.loadTabs();
const bundleService = (await import("../services/bundle.js")).default;
setTimeout(() => bundleService.executeStartupBundles(), 2000);
}

View File

@@ -1,5 +1,6 @@
import { getNoteIcon } from "@triliumnext/commons";
import bundleService from "../services/bundle.js";
import cssClassManager from "../services/css_class_manager.js";
import type { Froca } from "../services/froca-interface.js";
import noteAttributeCache from "../services/note_attribute_cache.js";
@@ -1014,7 +1015,6 @@ export default class FNote {
const env = this.getScriptEnv();
if (env === "frontend") {
const bundleService = (await import("../services/bundle.js")).default;
return await bundleService.getAndExecuteBundle(this.noteId);
} else if (env === "backend") {
await server.post(`script/run/${this.noteId}`);

View File

@@ -1,12 +1,14 @@
import utils from "../services/utils.js";
import options from "../services/options.js";
import zoomService from "../components/zoom.js";
import contextMenu, { type MenuItem } from "./context_menu.js";
import { t } from "../services/i18n.js";
import server from "../services/server.js";
import * as clipboardExt from "../services/clipboard_ext.js";
import type { BrowserWindow } from "electron";
import type { CommandNames, AppContext } from "../components/app_context.js";
import type { CommandNames } from "../components/app_context.js";
import appContext from "../components/app_context.js";
import zoomService from "../components/zoom.js";
import * as clipboardExt from "../services/clipboard_ext.js";
import { t } from "../services/i18n.js";
import options from "../services/options.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
import contextMenu, { type MenuItem } from "./context_menu.js";
function setupContextMenu() {
const electron = utils.dynamicRequire("electron");
@@ -15,8 +17,6 @@ function setupContextMenu() {
// FIXME: Remove typecast once Electron is properly integrated.
const { webContents } = remote.getCurrentWindow() as BrowserWindow;
let appContext: AppContext;
webContents.on("context-menu", (event, params) => {
const { editFlags } = params;
const hasText = params.selectionText.trim().length > 0;
@@ -141,7 +141,7 @@ function setupContextMenu() {
}
// Replace the placeholder with the real search keyword.
let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
const searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
items.push({ kind: "separator" });
@@ -155,10 +155,6 @@ function setupContextMenu() {
title: t("electron_context_menu.search_in_trilium", { term: shortenedSelection }),
uiIcon: "bx bx-search",
handler: async () => {
if (!appContext) {
appContext = (await import("../components/app_context.js")).default;
}
await appContext.triggerCommand("searchNotes", {
searchString: params.selectionText
});

View File

@@ -4,6 +4,7 @@ import { useCallback, useLayoutEffect, useRef } from "preact/hooks";
import FNote from "./entities/fnote";
import content_renderer from "./services/content_renderer";
import { applyInlineMermaid } from "./services/content_renderer_text";
import froca from "./services/froca";
import { dynamicRequire, isElectron } from "./services/utils";
import { CustomNoteList, useNoteViewType } from "./widgets/collections/NoteList";
@@ -30,7 +31,6 @@ async function main() {
if (!noteId) return;
await import("./print.css");
const froca = (await import("./services/froca")).default;
const note = await froca.getNote(noteId);
const bodyWrapper = document.createElement("div");

View File

@@ -26,7 +26,7 @@ type WithNoteId<T> = T & {
};
export type Widget = WithNoteId<(LegacyWidget | WidgetDefinitionWithType)>;
async function getAndExecuteBundle(noteId: string, originEntity = null, script = null, params = null) {
async function getAndExecuteBundle(noteId: string, originEntity: Entity | null = null, script: string | null = null, params: string | null = null) {
const bundle = await server.post<Bundle>(`script/bundle/${noteId}`, {
script,
params

View File

@@ -1,3 +1,6 @@
import { t } from "./i18n.js";
import toast from "./toast.js";
export function copyText(text: string) {
if (!text) {
return;
@@ -6,29 +9,26 @@ export function copyText(text: string) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
return true;
} else {
// Fallback method: https://stackoverflow.com/a/72239825
const textArea = document.createElement("textarea");
textArea.value = text;
try {
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return document.execCommand('copy');
} finally {
document.body.removeChild(textArea);
}
}
// Fallback method: https://stackoverflow.com/a/72239825
const textArea = document.createElement("textarea");
textArea.value = text;
try {
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return document.execCommand('copy');
} finally {
document.body.removeChild(textArea);
}
} catch (e) {
console.warn(e);
return false;
}
}
export async function copyTextWithToast(text: string) {
const t = (await import("./i18n.js")).t;
const toast = (await import("./toast.js")).default;
export function copyTextWithToast(text: string) {
if (copyText(text)) {
toast.showMessage(t("clipboard.copy_success"));
} else {

View File

@@ -1,9 +1,11 @@
import { Modal } from "bootstrap";
import appContext from "../components/app_context.js";
import type { ConfirmDialogOptions, ConfirmDialogResult, ConfirmWithMessageOptions, MessageType } from "../widgets/dialogs/confirm.js";
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
import { focusSavedElement, saveFocusedElement } from "./focus.js";
import { InfoExtraProps } from "../widgets/dialogs/info.jsx";
import keyboardActionsService from "./keyboard_actions.js";
export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog = true, config?: Partial<Modal.Options>) {
if (closeActDialog) {
@@ -25,7 +27,6 @@ export async function openDialog($dialog: JQuery<HTMLElement>, closeActDialog =
}
});
const keyboardActionsService = (await import("./keyboard_actions.js")).default;
keyboardActionsService.updateDisplayedShortcuts($dialog);
return $dialog;

View File

@@ -0,0 +1,30 @@
import { describe, expect, it } from "vitest";
import { isValidDocName } from "./doc_renderer.js";
describe("isValidDocName", () => {
it("accepts valid docNames", () => {
expect(isValidDocName("launchbar_intro")).toBe(true);
expect(isValidDocName("User Guide/Quick Start")).toBe(true);
expect(isValidDocName("User Guide/User Guide/Quick Start")).toBe(true);
expect(isValidDocName("Quick Start Guide")).toBe(true);
expect(isValidDocName("quick_start_guide")).toBe(true);
expect(isValidDocName("quick-start-guide")).toBe(true);
});
it("rejects path traversal attacks", () => {
expect(isValidDocName("..")).toBe(false);
expect(isValidDocName("../etc/passwd")).toBe(false);
expect(isValidDocName("foo/../bar")).toBe(false);
expect(isValidDocName("../../../../api/notes/_malicious/open")).toBe(false);
expect(isValidDocName("..\\etc\\passwd")).toBe(false);
expect(isValidDocName("foo\\bar")).toBe(false);
});
it("rejects URL manipulation attacks", () => {
expect(isValidDocName("../../../../api/notes/_malicious/open?x=")).toBe(false);
expect(isValidDocName("foo#bar")).toBe(false);
expect(isValidDocName("%2e%2e")).toBe(false);
expect(isValidDocName("%2e%2e%2f%2e%2e%2fapi")).toBe(false);
});
});

View File

@@ -3,22 +3,39 @@ import { applyReferenceLinks } from "../widgets/type_widgets/text/read_only_help
import { getCurrentLanguage } from "./i18n.js";
import { formatCodeBlocks } from "./syntax_highlight.js";
/**
* Validates a docName to prevent path traversal attacks.
* Allows forward slashes for subdirectories (e.g., "User Guide/Quick Start")
* but blocks traversal sequences and URL manipulation characters.
*/
export function isValidDocName(docName: string): boolean {
// Allow alphanumeric characters, spaces, underscores, hyphens, and forward slashes.
const validDocNameRegex = /^[a-zA-Z0-9_/\- ]+$/;
return validDocNameRegex.test(docName);
}
export default function renderDoc(note: FNote) {
return new Promise<JQuery<HTMLElement>>((resolve) => {
let docName = note.getLabelValue("docName");
const docName = note.getLabelValue("docName");
const $content = $("<div>");
if (docName) {
// find doc based on language
const url = getUrl(docName, getCurrentLanguage());
// find doc based on language
const url = getUrl(docName, getCurrentLanguage());
if (url) {
$content.load(url, async (response, status) => {
// fallback to english doc if no translation available
if (status === "error") {
const fallbackUrl = getUrl(docName, "en");
$content.load(fallbackUrl, async () => {
await processContent(fallbackUrl, $content)
if (fallbackUrl) {
$content.load(fallbackUrl, async () => {
await processContent(fallbackUrl, $content);
resolve($content);
});
} else {
resolve($content);
});
}
return;
}
@@ -28,8 +45,6 @@ export default function renderDoc(note: FNote) {
} else {
resolve($content);
}
return $content;
});
}
@@ -39,7 +54,7 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
// Images are relative to the docnote but that will not work when rendered in the application since the path breaks.
$content.find("img").each((i, el) => {
const $img = $(el);
$img.attr("src", dir + "/" + $img.attr("src"));
$img.attr("src", `${dir}/${$img.attr("src")}`);
});
formatCodeBlocks($content);
@@ -48,10 +63,17 @@ async function processContent(url: string, $content: JQuery<HTMLElement>) {
await applyReferenceLinks($content[0]);
}
function getUrl(docNameValue: string, language: string) {
function getUrl(docNameValue: string | null, language: string) {
if (!docNameValue) return;
if (!isValidDocName(docNameValue)) {
console.error(`Invalid docName: ${docNameValue}`);
return null;
}
// Cannot have spaces in the URL due to how JQuery.load works.
docNameValue = docNameValue.replaceAll(" ", "%20");
const basePath = window.glob.isDev ? window.glob.assetPath + "/.." : window.glob.assetPath;
const basePath = window.glob.isDev ? `${window.glob.assetPath }/..` : window.glob.assetPath;
return `${basePath}/doc_notes/${language}/${docNameValue}.html`;
}

View File

@@ -1,14 +1,16 @@
import LoadResults from "./load_results.js";
import froca from "./froca.js";
import utils from "./utils.js";
import options from "./options.js";
import noteAttributeCache from "./note_attribute_cache.js";
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import type { OptionNames } from "@triliumnext/commons";
import appContext from "../components/app_context.js";
import FAttachment, { type FAttachmentRow } from "../entities/fattachment.js";
import FAttribute, { type FAttributeRow } from "../entities/fattribute.js";
import FBranch, { type FBranchRow } from "../entities/fbranch.js";
import type { default as FNote, FNoteRow } from "../entities/fnote.js";
import type { EntityChange } from "../server_types.js";
import type { OptionNames } from "@triliumnext/commons";
import froca from "./froca.js";
import LoadResults from "./load_results.js";
import noteAttributeCache from "./note_attribute_cache.js";
import options from "./options.js";
import utils from "./utils.js";
async function processEntityChanges(entityChanges: EntityChange[]) {
const loadResults = new LoadResults(entityChanges);
@@ -63,7 +65,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
if (entityName === "branches" && !((entity as FBranchRow).parentNoteId in froca.notes)) {
missingNoteIds.push((entity as FBranchRow).parentNoteId);
} else if (entityName === "attributes") {
let attributeEntity = entity as FAttributeRow;
const attributeEntity = entity as FAttributeRow;
if (attributeEntity.type === "relation" && (attributeEntity.name === "template" || attributeEntity.name === "inherit") && !(attributeEntity.value in froca.notes)) {
missingNoteIds.push(attributeEntity.value);
}
@@ -79,7 +81,6 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
noteAttributeCache.invalidate();
}
const appContext = (await import("../components/app_context.js")).default;
await appContext.triggerEvent("entitiesReloaded", { loadResults });
}
}

View File

@@ -1,21 +1,14 @@
import options from "./options.js";
import { LOCALE_IDS, LOCALES, setDayjsLocale } from "@triliumnext/commons";
import i18next from "i18next";
import i18nextHttpBackend from "i18next-http-backend";
import server from "./server.js";
import { LOCALE_IDS, setDayjsLocale, type Locale } from "@triliumnext/commons";
import { initReactI18next } from "react-i18next";
let locales: Locale[] | null;
/**
* A deferred promise that resolves when translations are initialized.
*/
export let translationsInitializedPromise = $.Deferred();
export const translationsInitializedPromise = $.Deferred();
export async function initLocale() {
const locale = ((options.get("locale") as string) || "en") as LOCALE_IDS;
locales = await server.get<Locale[]>("options/locales");
export async function initLocale(locale: LOCALE_IDS = "en") {
i18next.use(initReactI18next);
await i18next.use(i18nextHttpBackend).init({
@@ -32,11 +25,7 @@ export async function initLocale() {
}
export function getAvailableLocales() {
if (!locales) {
throw new Error("Tried to load list of locales, but localization is not yet initialized.")
}
return locales;
return LOCALES;
}
/**
@@ -47,7 +36,7 @@ export function getAvailableLocales() {
*/
export function getLocaleById(localeId: string | null | undefined) {
if (!localeId) return null;
return locales?.find((l) => l.id === localeId) ?? null;
return LOCALES.find((l) => l.id === localeId) ?? null;
}
export const t = i18next.t;

View File

@@ -1,3 +1,4 @@
import { t } from "./i18n.js";
import utils, { isShare } from "./utils.js";
import ValidationError from "./validation_error.js";
@@ -32,8 +33,7 @@ async function getHeaders(headers?: Headers) {
return {};
}
const appContext = (await import("../components/app_context.js")).default;
const activeNoteContext = appContext.tabManager ? appContext.tabManager.getActiveContext() : null;
const activeNoteContext = glob.appContext?.tabManager ? glob.appContext.tabManager.getActiveContext() : null;
// headers need to be lowercase because node.js automatically converts them to lower case
// also avoiding using underscores instead of dashes since nginx filters them out by default
@@ -344,6 +344,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
} catch (e) {}
}
// Dynamic import to avoid circular dependency (toast → app_context → options → server).
const toastService = (await import("./toast.js")).default;
const messageStr = (typeof message === "string" ? message : JSON.stringify(message)) || "-";
@@ -357,7 +358,6 @@ async function reportError(method: string, url: string, statusCode: number, resp
...response
});
} else {
const { t } = await import("./i18n.js");
if (statusCode === 400 && (url.includes("%23") || url.includes("%2F"))) {
toastService.showPersistent({
id: "trafik-blocked",
@@ -371,8 +371,7 @@ async function reportError(method: string, url: string, statusCode: number, resp
t("server.unknown_http_error_content", { statusCode, method, url, message: messageStr }),
15_000);
}
const { logError } = await import("./ws.js");
logError(`${statusCode} ${method} ${url} - ${message}`);
window.logError(`${statusCode} ${method} ${url} - ${message}`);
}
}

View File

@@ -455,9 +455,7 @@ export function openInAppHelpFromUrl(inAppHelpPage: string) {
export async function openInReusableSplit(targetNoteId: string, targetViewMode: ViewMode, openOpts: {
hoistedNoteId?: string;
} = {}) {
// Dynamic import to avoid import issues in tests.
const appContext = (await import("../components/app_context.js")).default;
const activeContext = appContext.tabManager.getActiveContext();
const activeContext = glob.appContext?.tabManager?.getActiveContext();
if (!activeContext) {
return;
}
@@ -467,7 +465,7 @@ export async function openInReusableSplit(targetNoteId: string, targetViewMode:
if (!existingSubcontext) {
// The target split is not already open, open a new split with it.
const { ntxId } = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", {
glob.appContext?.triggerCommand("openNewNoteSplit", {
ntxId,
notePath: targetNoteId,
hoistedNoteId: openOpts.hoistedNoteId,

View File

@@ -1,13 +1,15 @@
import utils from "./utils.js";
import toastService from "./toast.js";
import server from "./server.js";
import options from "./options.js";
import frocaUpdater from "./froca_updater.js";
import appContext from "../components/app_context.js";
import { t } from "./i18n.js";
import type { EntityChange } from "../server_types.js";
import { WebSocketMessage } from "@triliumnext/commons";
import appContext from "../components/app_context.js";
import type { EntityChange } from "../server_types.js";
import bundleService from "./bundle.js";
import froca from "./froca.js";
import frocaUpdater from "./froca_updater.js";
import { t } from "./i18n.js";
import options from "./options.js";
import server from "./server.js";
import toast from "./toast.js";
import utils from "./utils.js";
type MessageHandler = (message: WebSocketMessage) => void;
let messageHandlers: MessageHandler[] = [];
@@ -126,20 +128,14 @@ async function handleMessage(event: MessageEvent<any>) {
} else if (message.type === "frontend-update") {
await executeFrontendUpdate(message.data.entityChanges);
} else if (message.type === "sync-hash-check-failed") {
toastService.showError(t("ws.sync-check-failed"), 60000);
toast.showError(t("ws.sync-check-failed"), 60000);
} else if (message.type === "consistency-checks-failed") {
toastService.showError(t("ws.consistency-checks-failed"), 50 * 60000);
toast.showError(t("ws.consistency-checks-failed"), 50 * 60000);
} else if (message.type === "api-log-messages") {
appContext.triggerEvent("apiLogMessages", { noteId: message.noteId, messages: message.messages });
} else if (message.type === "toast") {
toastService.showMessage(message.message);
toast.showMessage(message.message);
} else if (message.type === "execute-script") {
// TODO: Remove after porting the file
// @ts-ignore
const bundleService = (await import("./bundle.js")).default as any;
// TODO: Remove after porting the file
// @ts-ignore
const froca = (await import("./froca.js")).default as any;
const originEntity = message.originEntityId ? await froca.getNote(message.originEntityId) : null;
bundleService.getAndExecuteBundle(message.currentNoteId, originEntity, message.script, message.params);
@@ -161,7 +157,7 @@ function waitForEntityChangeId(desiredEntityChangeId: number) {
return new Promise<void>((res, rej) => {
entityChangeIdReachedListeners.push({
desiredEntityChangeId: desiredEntityChangeId,
desiredEntityChangeId,
resolvePromise: res,
start: Date.now()
});
@@ -205,7 +201,7 @@ async function consumeFrontendUpdateData() {
} else {
console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges);
toastService.showError(t("ws.encountered-error", { message: e.message }));
toast.showError(t("ws.encountered-error", { message: e.message }));
}
}

View File

@@ -544,14 +544,11 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
vertical-align: middle;
}
#toast-container .toast .toast-header .btn-close {
#toast-container .toast .toast-header .btn-close,
#toast-container .toast .toast-close .btn-close {
margin: 0 0 0 12px;
}
#toast-container .toast.no-title {
flex-direction: row;
}
#toast-container .toast .toast-body {
flex-grow: 1;
overflow: hidden;

View File

@@ -26,7 +26,8 @@
.modal .modal-header .btn-close,
.modal .modal-header .help-button,
.modal .modal-header .custom-title-bar-button,
#toast-container .toast .toast-header .btn-close {
#toast-container .toast .toast-header .btn-close,
#toast-container .toast .toast-close .btn-close {
display: flex;
justify-content: center;
align-items: center;
@@ -46,12 +47,14 @@
}
.modal .modal-header .btn-close,
#toast-container .toast .toast-header .btn-close {
#toast-container .toast .toast-header .btn-close,
#toast-container .toast .toast-close .btn-close {
--modal-control-button-hover-background: var(--modal-close-button-hover-background);
}
.modal .modal-header .btn-close::after,
#toast-container .toast .toast-header .btn-close::after {
#toast-container .toast .toast-header .btn-close::after,
#toast-container .toast .toast-close .btn-close::after {
content: "\ec8d";
font-family: boxicons;
}
@@ -67,7 +70,8 @@
.modal .modal-header .btn-close:hover,
.modal .modal-header .help-button:hover,
.modal .modal-header .custom-title-bar-button:hover,
#toast-container .toast .toast-header .btn-close:hover {
#toast-container .toast .toast-header .btn-close:hover,
#toast-container .toast .toast-close .btn-close:hover {
background: var(--modal-control-button-hover-background);
color: var(--modal-control-button-hover-color);
}
@@ -75,19 +79,22 @@
.modal .modal-header .btn-close:active,
.modal .modal-header .help-button:active,
.modal .modal-header .custom-title-bar-button:active,
#toast-container .toast .toast-header .btn-close:active {
#toast-container .toast .toast-header .btn-close:active,
#toast-container .toast .toast-close .btn-close:active {
transform: scale(.85);
}
.modal .modal-header .btn-close:focus,
.modal .modal-header .help-button:focus,
#toast-container .toast .toast-header .btn-close:focus {
#toast-container .toast .toast-header .btn-close:focus,
#toast-container .toast .toast-close .btn-close:focus {
box-shadow: none !important;
}
.modal .modal-header .btn-close:focus-visible,
.modal .modal-header .help-button:focus-visible,
#toast-container .toast .toast-header .btn-close:focus-visible {
#toast-container .toast .toast-header .btn-close:focus-visible,
#toast-container .toast .toast-close .btn-close:focus-visible {
outline: 2px solid var(--input-focus-outline-color);
outline-offset: 2px;
}

File diff suppressed because it is too large Load Diff

View File

@@ -2093,7 +2093,10 @@
"process_now": "Process OCR",
"processing": "Processing...",
"processing_started": "OCR processing has been started. Please wait a moment and refresh.",
"processing_complete": "OCR processing complete.",
"processing_failed": "Failed to start OCR processing",
"text_filtered_low_confidence": "OCR detected text with {{confidence}}% confidence, but it was discarded because your minimum threshold is {{threshold}}%.",
"open_media_settings": "Open Settings",
"view_extracted_text": "View extracted text (OCR)"
},
"command_palette": {
@@ -2304,7 +2307,9 @@
"sample_user_journey": "User Journey",
"sample_xy": "XY",
"sample_venn": "Venn",
"sample_ishikawa": "Ishikawa"
"sample_ishikawa": "Ishikawa",
"sample_treeview": "TreeView",
"sample_wardley": "Wardley Map"
},
"mind-map": {
"addChild": "Add child",

View File

@@ -399,7 +399,7 @@
"calendar_root": "nóta marcáilte ar cheart a úsáid mar fhréamh do nótaí lae. Níor cheart ach ceann amháin a mharcáil mar sin.",
"archived": "Ní bheidh nótaí leis an lipéad seo le feiceáil de réir réamhshocraithe i dtorthaí cuardaigh (i ndialóga Léim Chuig, Cuir Nasc Leis srl. chomh maith).",
"exclude_from_export": "ní chuirfear nótaí (lena bhfo-chrann) san áireamh in aon onnmhairiú nótaí",
"run": "Sainmhíníonn sé seo cé na himeachtaí ar cheart don script rith orthu. Is iad seo a leanas na luachanna féideartha:\n<ul>\n<li>frontendStartup - nuair a thosaíonn (nó a athnuachan) tosaigh Trilium, ach ní ar fhóin phóca.</li>\n<li>mobileStartup - nuair a thosaíonn (nó a athnuachan) tosaigh Trilium, ar fhóin phóca.</li>\n<li>backendStartup - nuair a thosaíonn cúltaca Trilium</li>\n<li>uair an chloig - rith uair san uair. Is féidir leat lipéad breise <code>runAtHour</code> a úsáid chun a shonrú cén uair a ritheann sé.</li>\n<li>daily - rith uair sa lá</li>\n</ul>",
"run": "Sainmhíníonn sé seo cé na himeachtaí ar cheart don script rith orthu. Is iad seo a leanas na luachanna féideartha:\n<ul>\n<li>frontendStartup - nuair a thosaíonn (nó a athnuachan) tosaigh Trilium, ach ní ar fhóin phóca.</li>\n<li>mobileStartup - nuair a thosaíonn (nó a athnuachan) tosaigh Trilium, ar fhóin phóca.</li>\n<li>backendStartup - nuair a thosaíonn cúltaca Trilium.</li>\n<li>hourly - rith uair san uair. Is féidir leat lipéad breise <code>runAtHour</code> a úsáid chun a shonrú cén uair a ritheann sé.</li>\n<li>daily - rith uair sa lá.</li>\n</ul>",
"run_on_instance": "Sainmhínigh cén sampla de Trilium ba chóir é seo a rith air. Réamhshocrú do gach sampla.",
"run_at_hour": "Cén uair ar cheart é seo a rith? Ba cheart é a úsáid i dteannta <code>#run=hourly</code>. Is féidir é seo a shainiú arís agus arís eile le haghaidh níos mó ritheanna i rith an lae.",
"disable_inclusion": "Ní chuirfear scripteanna leis an lipéad seo san áireamh i bhforghníomhú an scripte tuismitheora.",
@@ -709,7 +709,8 @@
"export_as_image": "Easpórtáil mar íomhá",
"export_as_image_png": "PNG (rastar)",
"export_as_image_svg": "SVG (veicteoir)",
"note_map": "Léarscáil nótaí"
"note_map": "Léarscáil nótaí",
"view_ocr_text": "Féach ar théacs OCR"
},
"onclick_button": {
"no_click_handler": "Níl aon láimhseálaí cliceáil sainithe ag an ngiuirléid cnaipe '{{componentId}}'"
@@ -1224,12 +1225,28 @@
},
"images": {
"images_section_title": "Íomhánna",
"download_images_automatically": "Íoslódáil íomhánna go huathoibríoch le húsáid as líne.",
"download_images_description": "Is féidir tagairtí díomhánna ar líne a bheith i HTML greamaithe, aimseoidh Trilium na tagairtí sin agus íoslódálfaidh sé na híomhánna ionas go mbeidh siad ar fáil as líne.",
"enable_image_compression": "Cumasaigh comhbhrú íomhá",
"max_image_dimensions": "Uasleithead/airde íomhá (athrófar méid na híomhá má sháraíonn sí an socrú seo).",
"download_images_automatically": "Íomhánna a íoslódáil go huathoibríoch",
"download_images_description": "Íoslódáil íomhánna tagartha ar líne ó HTML greamaithe ionas go mbeidh siad ar fáil as líne.",
"enable_image_compression": "Comhbhrú íomhá",
"max_image_dimensions": "Uasmhéid toisí íomhá",
"max_image_dimensions_unit": "picteilíní",
"jpeg_quality_description": "Cáilíocht JPEG (10 - an caighdeán is measa, 100 - an caighdeán is fearr, moltar 50 - 85)"
"jpeg_quality_description": "Is é an raon molta ná 5085. Laghdaíonn luachanna níos ísle méid an chomhaid, agus coinníonn luachanna níos airde sonraí.",
"enable_image_compression_description": "Comhbhrúigh agus athraigh méid íomhánna nuair a uaslódálfar nó a ghreamaítear iad.",
"max_image_dimensions_description": "Athrófar méid íomhánna a sháraíonn an méid seo go huathoibríoch.",
"jpeg_quality": "Cáilíocht JPEG",
"ocr_section_title": "Eastóscadh Téacs (OCR)",
"ocr_related_content_languages": "Teangacha ábhair (a úsáidtear le haghaidh eastóscadh téacs)",
"ocr_auto_process": "Próiseáil comhaid nua go huathoibríoch",
"ocr_auto_process_description": "Bain téacs go huathoibríoch as comhaid atá uaslódáilte nó greamaithe le déanaí.",
"ocr_min_confidence": "Íosmhuinín",
"ocr_confidence_description": "Ná bain ach téacs os cionn an tairsí muiníne seo. Cuimsíonn luachanna níos ísle níos mó téacs ach d'fhéadfadh siad a bheith níos lú cruinn.",
"batch_ocr_title": "Próiseáil Comhaid atá ann cheana",
"batch_ocr_description": "Bain téacs as na híomhánna, na PDFanna agus na doiciméid Office go léir atá i do nótaí. Dfhéadfadh sé seo roinnt ama a thógáil ag brath ar líon na gcomhad.",
"batch_ocr_start": "Tosaigh Próiseáil Bhaisc",
"batch_ocr_starting": "Ag tosú próiseáil bhaisc...",
"batch_ocr_progress": "Ag próiseáil {{processed}} de {{total}} comhad...",
"batch_ocr_completed": "Próiseáil bhaisc críochnaithe! Próiseáladh {{processed}} comhad.",
"batch_ocr_error": "Earráid le linn próiseála baisce: {{error}}"
},
"attachment_erasure_timeout": {
"attachment_erasure_timeout": "Am Teorann Scriosadh Ceangaltáin",
@@ -1275,7 +1292,7 @@
"custom_name_label": "Ainm innill chuardaigh saincheaptha",
"custom_name_placeholder": "Saincheap ainm an innill chuardaigh",
"custom_url_label": "Ba chóir go mbeadh {keyword} san áireamh mar áitchoinneálaí don téarma cuardaigh i URL inneall cuardaigh saincheaptha.",
"custom_url_placeholder": "Saincheap url an innill chuardaigh",
"custom_url_placeholder": "Saincheap URL an innill chuardaigh",
"save_button": "Sábháil"
},
"tray": {
@@ -1903,7 +1920,7 @@
},
"content_language": {
"title": "Teangacha ábhair",
"description": "Roghnaigh teanga amháin nó níos mó ar cheart dóibh a bheith le feiceáil sa rogha teanga sa rannán Airíonna Bunúsacha de nóta téacs inléite amháin nó in-eagarthóireachta. Ceadóidh sé seo gnéithe ar nós seiceáil litrithe tacaíocht ó dheis go clé."
"description": "Roghnaigh teanga amháin nó níos mó ar cheart dóibh a bheith le feiceáil sa rogha teanga sa rannán Airíonna Bunúsacha de nóta téacs inléite amháin nó in-eagarthóireachta. Ceadaíonn sé seo gnéithe ar nós seiceáil litrithe, tacaíocht ó dheis go clé agus eastóscadh téacs (OCR)."
},
"switch_layout_button": {
"title_vertical": "Bog an painéal eagarthóireachta go dtí an bun",
@@ -2308,7 +2325,8 @@
"note_context_enabled": "Cliceáil chun comhthéacs nótaí a dhíchumasú: {{title}}",
"note_context_disabled": "Cliceáil chun an nóta reatha a chur san áireamh i gcomhthéacs",
"no_provider_message": "Níl aon soláthraí AI cumraithe. Cuir ceann leis chun comhrá a thosú.",
"add_provider": "Cuir Soláthraí AI leis"
"add_provider": "Cuir Soláthraí AI leis",
"sources_summary": "{{count}} foinsí ó {{sites}} suíomhanna"
},
"sidebar_chat": {
"title": "Comhrá AI",
@@ -2348,6 +2366,44 @@
"delete_provider_confirmation": "An bhfuil tú cinnte gur mian leat an soláthraí \"{{name}}\" a scriosadh?",
"api_key": "Eochair API",
"api_key_placeholder": "Cuir isteach d'eochair API",
"cancel": "Cealaigh"
"cancel": "Cealaigh",
"feature_not_enabled": "Cumasaigh an ghné turgnamhach LLM i Socruithe → Ardleibhéil → Gnéithe turgnamhacha chun comhtháthú AI a úsáid.",
"mcp_title": "MCP (Prótacal Comhthéacs Múnla)",
"mcp_enabled": "Freastalaí MCP",
"mcp_enabled_description": "Nochtaigh críochphointe Prótacal Comhthéacs Múnla (MCP) ionas gur féidir le cúntóirí códaithe AI (m.sh. Claude Code, GitHub Copilot) do nótaí a léamh agus a mhodhnú. Ní féidir rochtain a fháil ar an gcríochphointe ach ó localhost.",
"mcp_endpoint_title": "URL críochphointe",
"mcp_endpoint_description": "Cuir an URL seo le cumraíocht MCP do chúntóra AI",
"tools": {
"search_notes": "Cuardaigh nótaí",
"get_note": "Faigh nóta",
"get_note_content": "Faigh ábhar nótaí",
"update_note_content": "Nuashonraigh ábhar an nóta",
"append_to_note": "Cuir leis an nóta",
"create_note": "Cruthaigh nóta",
"get_attributes": "Faigh tréithe",
"get_attribute": "Faigh tréith",
"set_attribute": "Socraigh tréith",
"delete_attribute": "Scrios tréith",
"get_child_notes": "Faigh nótaí leanaí",
"get_subtree": "Faigh fo-chrann",
"load_skill": "Luchtaigh scileanna",
"web_search": "Cuardach gréasáin",
"note_in_parent": "<Note/> i <Parent/>",
"get_attachment": "Faigh ceangaltán",
"get_attachment_content": "Léigh ábhar an cheangail"
}
},
"ocr": {
"extracted_text": "Téacs Bainte (OCR)",
"extracted_text_title": "Téacs Bainte (OCR)",
"loading_text": "Ag lódáil téacs OCR...",
"no_text_available": "Níl aon téacs OCR ar fáil",
"no_text_explanation": "Níor próiseáladh an nóta seo le haghaidh eastóscadh téacs OCR nó níor aimsíodh aon téacs.",
"failed_to_load": "Theip ar lódáil téacs OCR",
"process_now": "Próiseas OCR",
"processing": "Ag próiseáil...",
"processing_started": "Tá próiseáil OCR tosaithe. Fan nóiméad agus athnuachan le do thoil.",
"processing_failed": "Theip ar phróiseáil OCR a thosú",
"view_extracted_text": "Féach ar théacs eastósctha (OCR)"
}
}

View File

@@ -2265,7 +2265,8 @@
"note_context_enabled": "クリックしてノートのコンテキストを無効にする: {{title}}",
"note_context_disabled": "クリックして現在のノートをコンテキストに含める",
"no_provider_message": "AI プロバイダーが設定されていません。チャットを開始するには、プロバイダーを追加してください。",
"add_provider": "AI プロバイダーを追加"
"add_provider": "AI プロバイダーを追加",
"sources_summary": "{{count}} 件のソースを {{sites}} サイトから取得"
},
"sidebar_chat": {
"title": "AI チャット",
@@ -2305,7 +2306,32 @@
"delete_provider_confirmation": "プロバイダー \"{{name}}\" を削除してもよろしいですか?",
"api_key": "API キー",
"api_key_placeholder": "API キーを入力してください",
"cancel": "キャンセル"
"cancel": "キャンセル",
"feature_not_enabled": "AI 連携機能を使用するには、「設定」→「高度」→「実験的機能」で LLM 実験的機能を有効にしてください。",
"mcp_title": "MCPモデル コンテキスト プロトコル)",
"mcp_enabled": "MCP サーバー",
"mcp_enabled_description": "AI コーディングアシスタントClaude Code、GitHub Copilotートを読み取って変更できるように、モデルコンテキストプロトコルMCPエンドポイントを公開します。このエンドポイントは localhost からのみアクセス可能です。",
"mcp_endpoint_title": "エンドポイント URL",
"mcp_endpoint_description": "この URL を AI アシスタントの MCP 設定に追加してください",
"tools": {
"search_notes": "ノートを検索",
"get_note": "ノートを取得",
"get_note_content": "ノートの内容を取得",
"update_note_content": "ノートの内容を更新",
"append_to_note": "ノートに追記",
"create_note": "ノートを作成",
"get_attributes": "複数の属性を取得",
"get_attribute": "属性を取得",
"set_attribute": "属性を設定",
"delete_attribute": "属性を削除",
"get_child_notes": "子ノートを取得",
"get_subtree": "サブツリーを取得",
"load_skill": "スキルを読み込む",
"web_search": "Web 検索",
"note_in_parent": "<Note/> を <Parent/>",
"get_attachment": "添付ファイルを取得",
"get_attachment_content": "添付ファイルの内容を読み取る"
}
},
"ocr": {
"extracted_text": "抽出されたテキストOCR",

View File

@@ -368,7 +368,7 @@
"calendar_root": "標記應用作為每日筆記的根。只應標記一個筆記。",
"archived": "含有此標籤的筆記預設在搜尋結果中不可見(也適用於跳轉至、新增連結對話方塊等)。",
"exclude_from_export": "筆記(及其子階層)不會包含在任何匯出的筆記中",
"run": "定義腳本應運行的事件。可能的值包括:\n<ul>\n<li>frontendStartup - Trilium前端啟動時或重新整理時但不會在移動端執行。</li>\n<li>mobileStartup - Trilium前端啟動時或重新整理時 在行動端會執行。</li>\n<li>backendStartup - Trilium後端啟動時</li>\n<li>hourly - 每小時運行一次。您可以使用附加標籤<code>runAtHour</code>指定小時。</li>\n<li>daily - 每天運行一次</li>\n</ul>",
"run": "定義腳本應運行的事件。可能的值包括:\n<ul>\n<li>frontendStartup - Trilium前端啟動時或重新整理時但不會在移動端執行。</li>\n<li>mobileStartup - Trilium前端啟動時或重新整理時 在行動端會執行。</li>\n<li>backendStartup - Trilium後端啟動時</li>\n<li>hourly - 每小時運行一次。您可以使用附加標籤<code>runAtHour</code>指定小時。</li>\n<li>daily - 每天運行一次</li>\n</ul>",
"run_on_instance": "定義應在哪個 Trilium 實例上運行。預設為所有實例。",
"run_at_hour": "應在哪個小時運行。應與<code>#run=hourly</code>一起使用。可以多次定義,以便一天內運行多次。",
"disable_inclusion": "含有此標籤的腳本不會包含在父腳本執行中。",
@@ -706,7 +706,8 @@
"export_as_image": "匯出為圖片",
"export_as_image_png": "PNG (點陣)",
"export_as_image_svg": "SVG (向量)",
"note_map": "筆記地圖"
"note_map": "筆記地圖",
"view_ocr_text": "顯示 OCR 文字"
},
"onclick_button": {
"no_click_handler": "按鈕元件'{{componentId}}'沒有定義點擊時的處理方式"
@@ -1196,12 +1197,28 @@
},
"images": {
"images_section_title": "圖片",
"download_images_automatically": "自動下載圖片以供離線使用。",
"download_images_description": "貼上的 HTML 可能包含線上圖片的引用Trilium 會找到這些引用並下載圖片,以便它們可以離線使用。",
"enable_image_compression": "啟用圖片壓縮",
"max_image_dimensions": "圖片的最大寬度 / 高度(超過此限制的圖片將會被縮放)。",
"jpeg_quality_description": "JPEG 質量10 - 最差質量100 最佳質量,建議為 50 - 85",
"max_image_dimensions_unit": "像素"
"download_images_automatically": "自動下載圖片",
"download_images_description": "貼上的 HTML 下載引用的線上圖片以便離線使用。",
"enable_image_compression": "圖片壓縮",
"max_image_dimensions": "最大圖片尺寸",
"jpeg_quality_description": "建議範圍為 5085。較低的數值可縮小檔案大小較高的數值則能保留更多細節。",
"max_image_dimensions_unit": "像素",
"enable_image_compression_description": "在上傳或貼上圖片時壓縮並調整圖片大小。",
"max_image_dimensions_description": "超過此尺寸的圖片將會自動調整大小。",
"jpeg_quality": "JPEG 品質",
"ocr_section_title": "文字擷取OCR",
"ocr_related_content_languages": "內容語言(用於文字擷取)",
"ocr_auto_process": "自動處理新檔案",
"ocr_auto_process_description": "自動從新上傳或貼上的檔案中擷取文字。",
"ocr_min_confidence": "最低信賴度",
"ocr_confidence_description": "僅提取高於此信賴度閾值的文字。較低的閾值雖能包含更多文字,但準確度可能較低。",
"batch_ocr_title": "處理現有檔案",
"batch_ocr_description": "從筆記中的所有現有圖片、PDF 檔案及 Office 文件中擷取文字。根據檔案數量多寡,此過程可能需要一些時間。",
"batch_ocr_start": "開始批次處理",
"batch_ocr_starting": "開始批次處理…",
"batch_ocr_progress": "正在處理 {{processed}} 個檔案,共 {{total}} 個檔案…",
"batch_ocr_completed": "批次處理完成!已處理 {{processed}} 個檔案。",
"batch_ocr_error": "批次處理期間發生錯誤:{{error}}"
},
"attachment_erasure_timeout": {
"attachment_erasure_timeout": "附件清理超時",
@@ -1497,7 +1514,8 @@
"new-feature": "新增",
"collections": "集合",
"ai-chat": "AI 聊天",
"spreadsheet": "試算表"
"spreadsheet": "試算表",
"llm-chat": "AI 對話"
},
"protect_note": {
"toggle-on": "保護筆記",
@@ -1866,7 +1884,7 @@
},
"content_language": {
"title": "內文語言",
"description": "選擇一種或多種語言作為唯讀或可編輯文字筆記的可選基本屬性,這將支援拼寫檢查從右向左之類的功能。"
"description": "選擇一種或多種語言作為唯讀或可編輯文字筆記的可選基本屬性,這將支援拼寫檢查從右向左及文字擷取 (OCR) 等功能。"
},
"switch_layout_button": {
"title_vertical": "將編輯面板移至底部",
@@ -2046,7 +2064,9 @@
"title": "實驗性選項",
"disclaimer": "這些選項屬實驗性質,可能導致系統不穩定。請謹慎使用。",
"new_layout_name": "新版面配置",
"new_layout_description": "體驗全新版面配置,呈現更現代的外觀與更佳的使用體驗。在未來版本將進行大幅調整。"
"new_layout_description": "體驗全新版面配置,呈現更現代的外觀與更佳的使用體驗。在未來版本將進行大幅調整。",
"llm_name": "AI / LLM 對話",
"llm_description": "啟用由大語言模型驅動的 AI 聊天側邊欄及 LLM 聊天筆記。"
},
"server": {
"unknown_http_error_title": "與伺服器通訊錯誤",
@@ -2229,6 +2249,121 @@
"sample_user_journey": "使用者旅程",
"sample_xy": "XY 圖表",
"sample_venn": "韋恩圖",
"sample_ishikawa": "魚骨圖"
"sample_ishikawa": "魚骨圖",
"sample_treeview": "樹狀視圖",
"sample_wardley": "沃德利地圖"
},
"llm_chat": {
"placeholder": "輸入訊息…",
"send": "送出",
"sending": "正在送出…",
"empty_state": "請在下方輸入訊息,開啟對話。",
"searching_web": "正在搜尋網頁…",
"web_search": "網頁搜尋",
"note_tools": "筆記存取",
"sources": "來源",
"sources_summary": "來自 {{sites}} 個網站的 {{count}} 個來源",
"extended_thinking": "延伸思考",
"legacy_models": "傳統模型",
"thinking": "正在思考…",
"thought_process": "思考過程",
"tool_calls": "{{count}} 次工具調用",
"input": "輸入",
"result": "結果",
"error": "錯誤",
"tool_error": "失敗",
"total_tokens": "{{total}} 個詞元",
"tokens_detail": "{{prompt}} 提示詞 + {{completion}} 補全",
"tokens_used": "{{prompt}} 提示詞 + {{completion}} 補全 = {{total}} 個詞元",
"tokens_used_with_cost": "{{prompt}} 提示詞 + {{completion}} 補全 = {{total}} 個詞元(約 ${{cost}}",
"tokens_used_with_model": "{{model}}{{prompt}} 提示詞 + {{completion}} 補全 = {{total}} 個詞元",
"tokens_used_with_model_and_cost": "{{model}}{{prompt}} 提示詞 + {{completion}} 補全 = {{total}} 個詞元(約 ${{cost}}",
"tokens": "詞元",
"context_used": "已使用 {{percentage}}%",
"note_context_enabled": "點擊以禁用筆記上下文:{{title}}",
"note_context_disabled": "點擊將當前筆記納入上下文",
"no_provider_message": "尚未設定任何 AI 服務提供者。請新增一個以開始聊天。",
"add_provider": "新增 AI 提供者"
},
"ocr": {
"processing_complete": "OCR 處理已完成。",
"processing_failed": "無法啟動 OCR 處理",
"text_filtered_low_confidence": "OCR 偵測到的信賴度為 {{confidence}}%,但因您的最低閾值設定為 {{threshold}}%,故該結果已被捨棄。",
"open_media_settings": "開啟設定",
"view_extracted_text": "檢視擷取的文字 (OCR)",
"extracted_text": "已擷取的文字 (OCR)",
"extracted_text_title": "已擷取的文字 (OCR)",
"loading_text": "正在載入 OCR 文字…",
"no_text_available": "無 OCR 文字可用",
"no_text_explanation": "此筆記尚未經過 OCR 文字擷取處理,或未找到任何文字。",
"failed_to_load": "載入 OCR 文字失敗",
"process_now": "處理 OCR",
"processing": "正在處理…",
"processing_started": "OCR 處理已開始。請稍候片刻並重新整理頁面。"
},
"mind-map": {
"addChild": "新增子節點",
"addParent": "新增父節點",
"addSibling": "新增同級節點",
"removeNode": "刪除節點",
"focus": "專注模式",
"cancelFocus": "退出專注模式",
"moveUp": "上移",
"moveDown": "下移",
"link": "連結",
"linkBidirectional": "雙向連結",
"clickTips": "請點擊目標節點",
"summary": "摘要"
},
"llm": {
"settings_title": "AI / LLM",
"settings_description": "設定 AI 及大型語言模型整合。",
"feature_not_enabled": "請前往「設定」→「進階」→「實驗性功能」啟用 LLM 實驗性功能,即可使用 AI 整合。",
"add_provider": "新增提供者",
"add_provider_title": "新增 AI 提供者",
"configured_providers": "已設定的提供者",
"no_providers_configured": "尚未設定任何提供者。",
"provider_name": "名稱",
"provider_type": "提供者",
"actions": "動作",
"delete_provider": "刪除",
"delete_provider_confirmation": "您確定要刪除提供者 \"{{name}}\" 嗎?",
"api_key": "API 金鑰",
"api_key_placeholder": "請輸入您的 API 金鑰",
"cancel": "取消",
"mcp_title": "MCP模型上下文協定",
"mcp_enabled": "MCP 伺服器",
"mcp_enabled_description": "公開一個模型上下文協定 (MCP) 端點,以便人工智慧編程助手(例如 Claude Code、GitHub Copilot能夠讀取並修改您的筆記。此端點僅限從 localhost 存取。",
"mcp_endpoint_title": "端點網址",
"mcp_endpoint_description": "將此網址新增至您的 AI 助理的 MCP 設定中",
"tools": {
"search_notes": "搜尋筆記",
"get_note": "取得筆記",
"get_note_content": "取得筆記內容",
"update_note_content": "更新筆記內容",
"append_to_note": "追加至筆記",
"create_note": "建立筆記",
"get_attributes": "取得屬性",
"get_attribute": "取得屬性",
"set_attribute": "設定屬性",
"delete_attribute": "移除屬性",
"get_child_notes": "取得子筆記",
"get_subtree": "取得子階層",
"load_skill": "載入技能",
"web_search": "網頁搜尋",
"note_in_parent": "<Note/> 於 <Parent/>",
"get_attachment": "取得附件",
"get_attachment_content": "讀取附件內容"
}
},
"sidebar_chat": {
"title": "AI 對話",
"launcher_title": "打開 AI 對話",
"new_chat": "開始新對話",
"save_chat": "將對話保存至筆記",
"empty_state": "開始會話",
"history": "對話歷史",
"recent_chats": "最近的對話",
"no_chats": "無先前的對話記錄"
}
}

View File

@@ -28,9 +28,10 @@
overflow: hidden;
}
.toast.no-title {
.toast.no-title .toast-main-row {
display: flex;
flex-direction: row;
align-items: center;
}
.toast.no-title .toast-icon {
@@ -40,22 +41,26 @@
}
.toast.no-title .toast-body {
padding-inline-start: 0;
padding-inline-end: 0;
flex: 1;
padding-block: var(--bs-toast-padding-y);
padding-inline: 0;
}
.toast.no-title .toast-header {
background-color: unset !important;
.toast.no-title .toast-close {
display: flex;
align-items: center;
padding: var(--bs-toast-padding-y) var(--bs-toast-padding-x);
}
.toast {
.toast-buttons {
padding: 0 1em 1em 1em;
padding: 0 var(--bs-toast-padding-x) var(--bs-toast-padding-y) var(--bs-toast-padding-x);
display: flex;
gap: 1em;
justify-content: space-between;
flex-direction: column;
gap: 0.5em;
.btn {
width: 100%;
color: var(--bs-toast-color);
background: var(--modal-control-button-background);

View File

@@ -5,7 +5,6 @@ import { useEffect } from "preact/hooks";
import { removeToastFromStore, ToastOptionsWithRequiredId, toasts } from "../services/toast";
import Icon from "./react/Icon";
import { RawHtmlBlock } from "./react/RawHtml";
import Button from "./react/Button";
export default function ToastContainer() {
@@ -43,21 +42,24 @@ function Toast({ id, title, timeout, progress, message, icon, buttons }: ToastOp
id={`toast-${id}`}
>
{title ? (
<div class="toast-header">
<strong class="me-auto">
{toastIcon}
<span class="toast-title">{title}</span>
</strong>
{closeButton}
</div>
<>
<div class="toast-header">
<strong class="me-auto">
{toastIcon}
<span class="toast-title">{title}</span>
</strong>
{closeButton}
</div>
<div className="toast-body">{message}</div>
</>
) : (
<div class="toast-icon">{toastIcon}</div>
<div class="toast-main-row">
<div class="toast-icon">{toastIcon}</div>
<div className="toast-body">{message}</div>
<div class="toast-close">{closeButton}</div>
</div>
)}
<RawHtmlBlock className="toast-body" html={message} />
{!title && <div class="toast-header">{closeButton}</div>}
{buttons && (
<div class="toast-buttons">
{buttons.map(({ text, onClick }) => (

View File

@@ -82,6 +82,7 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
hi: () => import("@fullcalendar/core/locales/hi"),
ga: null,
cn: () => import("@fullcalendar/core/locales/zh-cn"),
cs: () => import("@fullcalendar/core/locales/cs"),
tw: () => import("@fullcalendar/core/locales/zh-tw"),
ro: () => import("@fullcalendar/core/locales/ro"),
ru: () => import("@fullcalendar/core/locales/ru"),

View File

@@ -1,11 +1,13 @@
import "./ReadOnlyTextRepresentation.css";
import type { TextRepresentationResponse } from "@triliumnext/commons";
import type { OCRProcessResponse, TextRepresentationResponse } from "@triliumnext/commons";
import { useEffect, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import { t } from "../../services/i18n";
import server from "../../services/server";
import toast from "../../services/toast";
import { randomString } from "../../services/utils";
import { TypeWidgetProps } from "./type_widget";
type State =
@@ -62,10 +64,35 @@ export function TextRepresentation({ textUrl, processUrl }: TextRepresentationPr
async function processOCR() {
setProcessing(true);
try {
const response = await server.post<{ success: boolean; message?: string }>(processUrl, { forceReprocess: true });
const response = await server.post<OCRProcessResponse>(processUrl, { forceReprocess: true });
if (response.success) {
toast.showMessage(t("ocr.processing_started"));
setTimeout(fetchText, 2000);
const result = response.result;
const minConfidence = response.minConfidence ?? 0;
// Check if text was filtered due to low confidence
if (result && !result.text && result.confidence > 0 && minConfidence > 0) {
const confidencePercent = Math.round(result.confidence * 100);
const thresholdPercent = Math.round(minConfidence * 100);
toast.showPersistent({
id: `ocr-low-confidence-${randomString(8)}`,
icon: "bx bx-info-circle",
message: t("ocr.text_filtered_low_confidence", {
confidence: confidencePercent,
threshold: thresholdPercent
}),
timeout: 15000,
buttons: [{
text: t("ocr.open_media_settings"),
onClick: ({ dismissToast }) => {
appContext.tabManager.openInNewTab("_optionsMedia", null, true);
dismissToast();
}
}]
});
} else {
toast.showMessage(t("ocr.processing_complete"));
}
setTimeout(fetchText, 500);
} else {
toast.showError(response.message || t("ocr.processing_failed"));
}

View File

@@ -4,6 +4,7 @@ import type { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Language["code"] | null> = {
ar: "ar-SA",
cn: "zh-CN",
cs: "cs-CZ",
de: "de-DE",
en: "en",
"en-GB": "en",

View File

@@ -104,6 +104,6 @@ body.desktop .note-detail-split .note-detail-code-editor {
.note-detail-split.svg-editor .render-container svg {
width: 100%;
height: 100%;
max-width: 100%;
max-width: 100% !important;
}
/* #endregion */

View File

@@ -505,6 +505,47 @@ ishikawa-beta
Environment
Subject moved too quickly
Too dark
`
},
{
name: t("mermaid.sample_treeview"),
content: `\
treeView-beta
"src"
"components"
"Button.tsx"
"Modal.tsx"
"services"
"api.ts"
"utils.ts"
"index.ts"
"package.json"
"README.md"
`
},
{
name: t("mermaid.sample_wardley"),
content: `\
wardley-beta
title Tea Shop
anchor Customers [0.95, 0.63]
anchor Business [0.95, 0.27]
component Cup of Tea [0.79, 0.61]
component Tea [0.63, 0.81]
component Cup [0.57, 0.46]
component Water [0.52, 0.89]
component Kettle [0.47, 0.53]
component Power [0.36, 0.72]
Customers -> Cup of Tea
Business -> Cup of Tea
Cup of Tea -> Tea
Cup of Tea -> Cup
Cup of Tea -> Water
Water -> Kettle
Kettle -> Power
`
}
];

View File

@@ -1,4 +1,5 @@
import type { ForgeConfig } from "@electron-forge/shared-types";
import { FuseV1Options, FuseVersion } from "@electron/fuses";
import { LOCALES } from "@triliumnext/commons";
import { existsSync } from "fs";
import fs from "fs-extra";
@@ -166,6 +167,17 @@ const config: ForgeConfig = {
{
name: "@electron-forge/plugin-auto-unpack-natives",
config: {}
},
{
name: "@electron-forge/plugin-fuses",
config: {
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true
}
}
],
hooks: {

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/desktop",
"version": "0.102.1",
"version": "0.102.2",
"description": "Build your personal knowledge base with Trilium Notes",
"private": true,
"main": "src/main.ts",
@@ -27,16 +27,9 @@
"better-sqlite3": "12.8.0",
"electron-debug": "4.1.0",
"electron-dl": "4.0.0",
"electron-squirrel-startup": "1.0.1",
"jquery.fancytree": "2.38.5",
"jquery-hotkeys": "0.2.2"
"electron-squirrel-startup": "1.0.1"
},
"devDependencies": {
"@types/electron-squirrel-startup": "1.0.2",
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "14.0.0",
"electron": "41.1.0",
"@electron-forge/cli": "7.11.1",
"@electron-forge/maker-deb": "7.11.1",
"@electron-forge/maker-dmg": "7.11.1",
@@ -45,6 +38,13 @@
"@electron-forge/maker-squirrel": "7.11.1",
"@electron-forge/maker-zip": "7.11.1",
"@electron-forge/plugin-auto-unpack-natives": "7.11.1",
"@electron-forge/plugin-fuses": "7.11.1",
"@electron/fuses": "2.1.1",
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"@types/electron-squirrel-startup": "1.0.2",
"copy-webpack-plugin": "14.0.0",
"electron": "41.1.1",
"prebuild-install": "7.1.3"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/edit-docs",
"version": "0.102.1",
"version": "0.102.2",
"private": true,
"description": "Desktop version of Trilium which imports the demo database (presented to new users at start-up) or the user guide and other documentation and saves the modifications for committing.",
"dependencies": {
@@ -12,7 +12,7 @@
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "14.0.0",
"electron": "41.1.0",
"electron": "41.1.1",
"fs-extra": "11.3.4"
},
"scripts": {

View File

@@ -6,6 +6,6 @@
"e2e": "playwright test"
},
"devDependencies": {
"dotenv": "17.3.1"
"dotenv": "17.4.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/server",
"version": "0.102.1",
"version": "0.102.2",
"description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.",
"private": true,
"main": "./src/main.ts",
@@ -79,8 +79,6 @@
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"axios": "1.14.0",
"bindings": "1.5.0",
"bootstrap": "5.3.8",
"chardet": "2.1.1",
"cheerio": "1.2.0",
"chokidar": "5.0.0",
@@ -91,8 +89,7 @@
"debounce": "3.0.0",
"debug": "4.4.3",
"ejs": "5.0.1",
"electron": "41.1.0",
"electron-debug": "4.1.0",
"electron": "41.1.1",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
"express": "5.2.1",
@@ -105,8 +102,8 @@
"helmet": "8.1.0",
"html": "1.0.0",
"html2plaintext": "2.1.4",
"http-proxy-agent": "8.0.0",
"https-proxy-agent": "8.0.0",
"http-proxy-agent": "9.0.0",
"https-proxy-agent": "9.0.0",
"i18next": "26.0.3",
"i18next-fs-backend": "2.6.3",
"image-type": "6.1.0",
@@ -114,7 +111,6 @@
"is-animated": "2.0.2",
"is-svg": "6.1.0",
"jimp": "1.6.0",
"lorem-ipsum": "2.0.8",
"marked": "17.0.5",
"mime-types": "3.0.2",
"multer": "2.1.1",
@@ -130,7 +126,6 @@
"strip-bom": "5.0.0",
"striptags": "3.2.0",
"supertest": "7.2.2",
"swagger-jsdoc": "6.2.8",
"tesseract.js": "7.0.0",
"time2fa": "1.4.2",
"tmp": "0.2.5",

View File

@@ -86,7 +86,24 @@
"copy-without-formatting": "Kopírovat vybraný text bez formátování",
"force-save-revision": "Vynutit vytvoření / uložení nové revize aktivní poznámky",
"export-as-pdf": "Exportovat současnou poznámku jako PDF",
"toggle-zen-mode": "Zapnout/vypnout režim zen (minimalistické uživatelské rozhraní pro soustředěnější úpravy)"
"toggle-zen-mode": "Zapnout/vypnout režim zen (minimalistické uživatelské rozhraní pro soustředěnější úpravy)",
"toggle-basic-properties": "Přepnout základní vlastnosti",
"toggle-file-properties": "Přepnout vlastnosti souboru",
"toggle-image-properties": "Přepnout vlastnosti obrázku",
"toggle-owned-attributes": "Přepnout vlastní atributy",
"toggle-inherited-attributes": "Přepnout zděděné atributy",
"toggle-promoted-attributes": "Přepnout propagované atributy",
"toggle-link-map": "Přepnout mapu odkazů",
"toggle-note-info": "Přepnout informace o poznámce",
"toggle-note-paths": "Přepnout cesty k poznámce",
"toggle-similar-notes": "Přepnout podobné poznámky",
"toggle-right-pane": "Přepnout zobrazení pravého panelu, který obsahuje obsah a zvýraznění",
"toggle-note-hoisting": "Přepnout zúžení zobrazení aktivní poznámky",
"find-in-text": "Přepnout panel hledání",
"toggle-left-note-tree-panel": "Přepnout levý panel (strom poznámek)",
"toggle-full-screen": "Přepnout režim celého obrazovky",
"toggle-book-properties": "Přepnout vlastnosti kolekce",
"toggle-classic-editor-toolbar": "Přepnout záložku formátování pro editor s fixní páskou"
},
"keyboard_action_names": {
"jump-to-note": "Přejít na...",
@@ -107,6 +124,322 @@
"expand-subtree": "Otevřít podstrom",
"collapse-tree": "Zavřít strom",
"collapse-subtree": "Zavřít podstrom",
"sort-child-notes": "Seřadit dceřiné poznámky"
"sort-child-notes": "Seřadit dceřiné poznámky",
"create-note-after": "Vytvořit poznámku po",
"create-note-into": "Vytvořit poznámku do",
"create-note-into-inbox": "Vytvořit poznámku v doručené poště",
"delete-notes": "Smazat poznámky",
"edit-branch-prefix": "Upravit předponu větve",
"paste-notes-from-clipboard": "Vložit poznámky ze schránky",
"cut-notes-to-clipboard": "Vyříznout poznámky do schránky",
"select-all-notes-in-parent": "Vybrat všechny poznámky v nadřazené položce",
"add-note-above-to-selection": "Přidat poznámku nad výběr",
"add-note-below-to-selection": "Přidat poznámku pod výběr",
"duplicate-subtree": "Duplikovat podstrom",
"open-new-tab": "Otevřít novou záložku",
"close-active-tab": "Zavřít aktivní záložku",
"reopen-last-tab": "Znovu otevřít poslední záložku",
"activate-next-tab": "Aktivovat další záložku",
"activate-previous-tab": "Aktivovat předchozí záložku",
"open-new-window": "Otevřít nové okno",
"toggle-system-tray-icon": "Přepínat ikonu v systémové oblasti",
"toggle-zen-mode": "Přepínat režim Zen",
"switch-to-first-tab": "Přepnout na první záložku",
"switch-to-second-tab": "Přepnout na druhou záložku",
"switch-to-third-tab": "Přepnout na třetí záložku",
"switch-to-fourth-tab": "Přepnout na čtvrtou záložku",
"switch-to-fifth-tab": "Přepnout na pátou záložku",
"switch-to-sixth-tab": "Přepnout na šestou záložku",
"switch-to-seventh-tab": "Přepnout na sedmou záložku",
"switch-to-eighth-tab": "Přepnout na osmou záložku",
"switch-to-ninth-tab": "Přepnout na devátou záložku",
"switch-to-last-tab": "Přepnout na poslední záložku",
"show-note-source": "Zobrazit zdroj poznámky",
"show-options": "Zobrazit nastavení",
"show-revisions": "Zobrazit revize",
"show-recent-changes": "Zobrazit nedávné změny",
"show-sql-console": "Zobrazit SQL konzoli",
"show-backend-log": "Zobrazit log backendu",
"show-help": "Zobrazit nápovědu",
"show-cheatsheet": "Zobrazit kısestupku",
"add-link-to-text": "Přidat odkaz do textu",
"follow-link-under-cursor": "Otevřít odkaz pod kurzorem",
"insert-date-and-time-to-text": "Vložit datum a čas do textu",
"paste-markdown-into-text": "Vložit Markdown do textu",
"cut-into-note": "Vyříznout do poznámky",
"add-include-note-to-text": "Přidat zahrnutí poznámky do textu",
"edit-read-only-note": "Upravit poznámku pouze pro čtení",
"add-new-label": "Přidat nový štítek",
"add-new-relation": "Přidat novou vazbu",
"toggle-ribbon-tab-classic-editor": "Přepínat záložku pásu karet Klasický editor",
"toggle-ribbon-tab-basic-properties": "Přepínat záložku pásu karet Základní vlastnosti",
"toggle-ribbon-tab-book-properties": "Přepínat záložku pásu karet Vlastnosti knihy",
"toggle-ribbon-tab-file-properties": "Přepínat záložku pásu karet Vlastnosti souboru",
"toggle-ribbon-tab-image-properties": "Přepínat záložku pásu karet Vlastnosti obrázku",
"toggle-ribbon-tab-owned-attributes": "Přepínat záložku pásu karet Vlastní atributy",
"toggle-ribbon-tab-inherited-attributes": "Přepínat záložku pásu karrét Zděděné atributy",
"toggle-ribbon-tab-promoted-attributes": "Přepínat záložku pásu karet Propagované atributy",
"toggle-ribbon-tab-note-map": "Přepínat záložku pásu karet Mapa poznámky",
"toggle-ribbon-tab-note-info": "Přepínat záložku pásu karet Informace o poznámce",
"toggle-ribbon-tab-note-paths": "Přepínat záložku pásu karet Cesty k poznámce",
"toggle-ribbon-tab-similar-notes": "Přepínat záložku pásu karet Podobné poznámky",
"toggle-right-pane": "Přepnout pravý panel",
"print-active-note": "Tisknout aktivní poznámku",
"export-active-note-as-pdf": "Exportovat aktivní poznámku jako PDF",
"open-note-externally": "Otevřít poznámku externě",
"render-active-note": "Zobrazit aktivní poznámku",
"run-active-note": "Spustit aktivní poznámku",
"toggle-note-hoisting": "Přepnout zúžení zobrazení poznámky",
"unhoist-note": "Zrušit zúžení zobrazení poznámky",
"reload-frontend-app": "Znovu načíst frontend aplikaci",
"open-developer-tools": "Otevřít vývojářské nástroje",
"find-in-text": "Najít v textu",
"toggle-left-pane": "Přepnout levý panel",
"toggle-full-screen": "Přepnout režim celého obrazovky",
"zoom-out": "Zoom out",
"zoom-in": "Zoom in",
"reset-zoom-level": "Resetovat úroveň zvětšení",
"copy-without-formatting": "Kopírovat bez formátování",
"force-save-revision": "Vynutit uložení revize"
},
"login": {
"title": "Přihlášení",
"heading": "Přihlášení do Trilium",
"incorrect-totp": "TOTP je nesprávné. Zkuste to prosím znovu.",
"incorrect-password": "Heslo je nesprávné. Zkuste to prosím znovu.",
"password": "Heslo",
"remember-me": "Zapamatovat si mě",
"button": "Přihlásit se",
"sign_in_with_sso": "Přihlásit se pomocí {{ ssoIssuerName }}"
},
"set_password": {
"title": "Nastavit heslo",
"heading": "Nastavit heslo",
"description": "Než budete moci začít Trilium používat z webu, musíte nejprve nastavit heslo. Toto heslo pak budete používat k přihlášení.",
"password": "Heslo",
"password-confirmation": "Potvrzení hesla",
"button": "Nastavit heslo"
},
"setup": {
"heading": "Nastavení Trilium Notes",
"new-document": "Jsem nový uživatel a chci vytvořit nový dokument Trilium pro své poznámky",
"sync-from-desktop": "Již mám instanci na počítači a chci s ní nastavit synchronizaci",
"sync-from-server": "Již mám instanci na serveru a chci s ní nastavit synchronizaci",
"next": "Další",
"init-in-progress": "Inicializace dokumentu probíhá",
"redirecting": "Budete brzy přesměrováni do aplikace.",
"title": "Nastavení"
},
"setup_sync-from-desktop": {
"heading": "Synchronizace z počítače",
"description": "Toto nastavení musí být zahájeno z instance na počítači:",
"step1": "Otevřete svou instanci Trilium Notes na počítači.",
"step2": "V menu Trilium klikněte na Nastavení.",
"step3": "Klikněte na kategorii Synchronizace.",
"step4": "Změňte adresu instance serveru na: {{- host}} a klikněte na Uložit.",
"step5": "Klikněte na tlačítko „Testovat synchronizaci“ pro ověření úspěšného připojení.",
"step6": "Jakmile tyto kroky dokončíte, klikněte na {{- link}}.",
"step6-here": "zde"
},
"setup_sync-from-server": {
"heading": "Synchronizace ze serveru",
"instructions": "Níže prosím zadejte adresu serveru Trilium a přihlašovací údaje. To stáhne celý dokument Trilium ze serveru a nastaví jeho synchronizaci. V závislosti na velikosti dokumentu a rychlosti vašeho připojení to může trvat nějakou dobu.",
"server-host": "Adresa serveru Trilium",
"server-host-placeholder": "https://<hostname>:<port>",
"proxy-server": "Proxy server (volitelné)",
"proxy-server-placeholder": "https://<hostname>:<port>",
"note": "Poznámka:",
"proxy-instruction": "Pokud ponecháte nastavení proxy prázdné, bude použita systémová proxy (platí pouze pro počítačovou aplikaci)",
"password": "Heslo",
"password-placeholder": "Heslo",
"back": "Zpět",
"finish-setup": "Dokončit nastavení"
},
"setup_sync-in-progress": {
"heading": "Synchronizace probíhá",
"successful": "Synchronizace byla správně nastavena. Prvotní synchronizace bude trvat nějaký čas. Jakmile bude hotovo, budete přesměrováni na přihlašovací stránku.",
"outstanding-items": "Neodeslané položky synchronizace:",
"outstanding-items-default": "N/A"
},
"share_404": {
"title": "Nenalezeno",
"heading": "Nenalezeno"
},
"share_page": {
"parent": "nadřazená:",
"clipped-from": "Tato poznámka byla původně uložena ze zdroje {{- url}}",
"child-notes": "Dceřiné poznámky:",
"no-content": "Tato poznámka neobsahuje žádný obsah."
},
"weekdays": {
"monday": "Pondělí",
"tuesday": "Úterý",
"wednesday": "Středa",
"thursday": "Čtvrtek",
"friday": "Pátek",
"saturday": "Sobota",
"sunday": "Neděle"
},
"weekdayNumber": "Týden {{weekNumber}}",
"months": {
"january": "Leden",
"february": "Únor",
"march": "Březen",
"april": "Duben",
"may": "Květen",
"june": "Červen",
"july": "Červenec",
"august": "Srpen",
"september": "Září",
"october": "Říjen",
"november": "Listopad",
"december": "Prosinec"
},
"quarterNumber": "Čtvrtletí {quarterNumber}",
"special_notes": {
"search_prefix": "Hledání:",
"llm_chat_prefix": "Chat:"
},
"test_sync": {
"not-configured": "Hostitel synchronizačního serveru není nakonfigurován. Nejprve prosím nakonfigurujte synchronizaci.",
"successful": "Protokol synchronizačního serveru byl úspěšný, synchronizace byla zahájena."
},
"hidden-subtree": {
"root-title": "Skryté poznámky",
"search-history-title": "Historie hledání",
"note-map-title": "Mapa poznámek",
"sql-console-history-title": "Historie SQL konzole",
"llm-chat-history-title": "Historie AI Chat",
"shared-notes-title": "Sdílené poznámky",
"bulk-action-title": "Hromadná akce",
"backend-log-title": "Log Backend",
"user-hidden-title": "Uživatel skryt",
"launch-bar-templates-title": "Šablony panelu spouštěče",
"base-abstract-launcher-title": "Základní abstraktní spouštěč",
"command-launcher-title": "Spouštěč příkazů",
"note-launcher-title": "Spouštěč poznámky",
"script-launcher-title": "Spouštěč skriptu",
"built-in-widget-title": "Vestavěný widget",
"spacer-title": "Mezera",
"custom-widget-title": "Vlastní widget",
"launch-bar-title": "Panel spouštěče",
"available-launchers-title": "Dostupné spouštěče",
"go-to-previous-note-title": "Přejít na předchozí poznámku",
"go-to-next-note-title": "Přejít na další poznámku",
"new-note-title": "Nová poznámka",
"search-notes-title": "Hledat poznámky",
"jump-to-note-title": "Skočit na...",
"calendar-title": "Kalendář",
"recent-changes-title": "Nedávné změny",
"bookmarks-title": "Záložky",
"command-palette": "Otevřít paletu příkazů",
"zen-mode": "Režim Zen",
"open-today-journal-note-title": "Otevřít dnešní deník",
"quick-search-title": "Rychlé hledání",
"protected-session-title": "Chráněná relace",
"sync-status-title": "Stav synchronizace",
"settings-title": "Nastavení",
"options-title": "Možnosti",
"appearance-title": "Vzhled",
"shortcuts-title": "Zkratky",
"text-notes": "Textové poznámky",
"code-notes-title": "Poznámky s kódem",
"images-title": "Média",
"spellcheck-title": "Kontrola pravopisu",
"password-title": "Heslo",
"multi-factor-authentication-title": "MFA",
"etapi-title": "ETAPI",
"backup-title": "Záloha",
"sync-title": "Synchronizace",
"other": "Ostatní",
"advanced-title": "Pokročilé",
"llm-title": "AI / LLM",
"visible-launchers-title": "Viditelné spouštěče",
"user-guide": "Uživatelská příručka",
"localization": "Jazyk a region",
"inbox-title": "Schránka příchozí",
"tab-switcher-title": "Přepínač záložek",
"sidebar-chat-title": "AI Chat"
},
"notes": {
"new-note": "Nová poznámka",
"duplicate-note-suffix": "(dup)",
"duplicate-note-title": "{{- noteTitle }} {{ duplicateNoteSuffix }}"
},
"backend_log": {
"log-does-not-exist": "Logovací soubor Backend '{{ fileName }}' neexistuje (zatím).",
"reading-log-failed": "Čtení logovacího souboru Backend '{{ fileName }}' se nepodařilo."
},
"content_renderer": {
"note-cannot-be-displayed": "Tento typ poznámky nelze zobrazit."
},
"pdf": {
"export_filter": "PDF dokument (*.pdf)",
"unable-to-export-message": "Aktuální poznámku nebylo možné exportovat jako PDF.",
"unable-to-export-title": "Nelze exportovat jako PDF",
"unable-to-save-message": "Do vybraného souboru nebylo možné zapisovat. Zkuste to znovu nebo vyberte jiné cílové místo.",
"unable-to-print": "Nelze vytisknout poznámku"
},
"tray": {
"tooltip": "Trilium Notes",
"close": "Ukončit Trilium",
"recents": "Nedávné poznámky",
"bookmarks": "Záložky (oblíbené)",
"today": "Otevřít dnešní deníkovou poznámku",
"new-note": "Nová poznámka",
"show-windows": "Zobrazit okna",
"open_new_window": "Otevřít nové okno"
},
"migration": {
"old_version": "Přímá migrace z vaší aktuální verze není podporována. Nejprve prosím upgradujte na nejnovější v0.60.4 a až poté na tuto verzi.",
"error_message": "Chyba během migrace na verzi {{version}}: {{stack}}",
"wrong_db_version": "Verze databáze ({{version}}) je novější, než jakou aplikace očekává ({{targetVersion}}), což znamená, že byla vytvořena novější a nekompatibilní verzí Trilium. Pro vyřešení tohoto problému upgradujte na nejnovější verzi Trilium."
},
"modals": {
"error_title": "Chyba"
},
"share_theme": {
"site-theme": "Motiv webu",
"search_placeholder": "Hledat...",
"image_alt": "Obrázek článku",
"last-updated": "Poslední aktualizace dne {{ - date}}",
"subpages": "Podstránky:",
"on-this-page": "Na této stránce",
"expand": "Rozbalit"
},
"hidden_subtree_templates": {
"text-snippet": "Textový úryvek",
"description": "Popis",
"list-view": "Seznamový pohled",
"grid-view": "Mřížkový pohled",
"calendar": "Kalendář",
"table": "Tabulka",
"geo-map": "Geomapa",
"start-date": "Počáteční datum",
"end-date": "Koncové datum",
"start-time": "Počáteční čas",
"end-time": "Koncowy čas",
"geolocation": "Geolokalizace",
"built-in-templates": "Vestavěné šablony",
"board": "Kanbanová tabule",
"status": "Stav",
"board_note_first": "První poznámka",
"board_note_second": "Druhá poznámka",
"board_note_third": "Třetí poznámka",
"board_status_todo": "K dokončení",
"board_status_progress": "Probíhá",
"board_status_done": "Hotovo",
"presentation": "Prezentace",
"presentation_slide": "Prezentace snímku",
"presentation_slide_first": "První snímek",
"presentation_slide_second": "Druhý snímek",
"background": "Pozadí"
},
"sql_init": {
"db_not_initialized_desktop": "DB není inicializována, postupujte podle pokynů na obrazovce.",
"db_not_initialized_server": "DB není inicializována, navštivte prosím stránku pro nastavení - http://[your-server-host]:{{port}}, kde najdete pokyny, jak inicializovat Trilium."
},
"desktop": {
"instance_already_running": "Instance již běží, místo vytváření nové se zaměříme na tu stávající."
}
}

View File

@@ -343,7 +343,7 @@
"shortcuts-title": "Aicearraí",
"text-notes": "Nótaí Téacs",
"code-notes-title": "Nótaí Cód",
"images-title": "Íomhánna",
"images-title": "Meáin",
"spellcheck-title": "Seiceáil litrithe",
"password-title": "Pasfhocal",
"multi-factor-authentication-title": "MFA",

View File

@@ -198,7 +198,8 @@
"december": "十二月"
},
"special_notes": {
"search_prefix": "搜尋:"
"search_prefix": "搜尋:",
"llm_chat_prefix": "對話:"
},
"test_sync": {
"not-configured": "尚未設定同步伺服器主機,請先設定同步。",
@@ -340,7 +341,7 @@
"shortcuts-title": "快捷鍵",
"text-notes": "文字筆記",
"code-notes-title": "程式碼筆記",
"images-title": "圖片",
"images-title": "媒體",
"spellcheck-title": "拼寫檢查",
"password-title": "密碼",
"multi-factor-authentication-title": "多重身份驗證",
@@ -355,7 +356,10 @@
"inbox-title": "收件匣",
"command-palette": "打開命令面板",
"zen-mode": "禪模式",
"tab-switcher-title": "切換分頁"
"tab-switcher-title": "切換分頁",
"llm-chat-history-title": "AI 對話歷史",
"llm-title": "AI / LLM",
"sidebar-chat-title": "AI 對話"
},
"notes": {
"new-note": "新增筆記",

View File

@@ -67,6 +67,11 @@ function register(router: Router) {
eu.validateAndPatch(_params, req.body, ALLOWED_PROPERTIES_FOR_CREATE_NOTE);
const params = _params as NoteParams;
// Validate MIME type for image notes
if (params.type === "image" && params.mime && !params.mime.toLowerCase().startsWith("image/")) {
throw new eu.EtapiError(400, "INVALID_MIME_FOR_IMAGE", `MIME type '${params.mime}' is not allowed for image notes. MIME must start with 'image/'.`);
}
try {
const resp = noteService.createNewNote(params);
@@ -94,6 +99,14 @@ function register(router: Router) {
throw new eu.EtapiError(400, "NOTE_IS_PROTECTED", `Note '${req.params.noteId}' is protected and cannot be modified through ETAPI.`);
}
// Validate MIME type for image notes (check both current and new type/mime)
const effectiveType = req.body.type ?? note.type;
const effectiveMime = req.body.mime ?? note.mime;
const normalizedEffectiveMime = typeof effectiveMime === "string" ? effectiveMime.toLowerCase() : effectiveMime;
if (effectiveType === "image" && normalizedEffectiveMime && !normalizedEffectiveMime.startsWith("image/")) {
throw new eu.EtapiError(400, "INVALID_MIME_FOR_IMAGE", `MIME type '${effectiveMime}' is not allowed for image notes. MIME must start with 'image/'.`);
}
noteService.saveRevisionIfNeeded(note);
eu.validateAndPatch(note, req.body, ALLOWED_PROPERTIES_FOR_PATCH);
note.save();

View File

@@ -230,6 +230,10 @@ function uploadModifiedFileToAttachment(req: Request<{ attachmentId: string }>)
const { attachmentId } = req.params;
const { filePath } = req.body;
if (!createdTemporaryFiles.has(filePath)) {
throw new ValidationError(`File '${filePath}' is not a temporary file.`);
}
const attachment = becca.getAttachmentOrThrow(attachmentId);
log.info(`Updating attachment '${attachmentId}' with content from '${filePath}'`);

View File

@@ -6,6 +6,7 @@ import type BNote from "../../becca/entities/bnote.js";
import type BRevision from "../../becca/entities/brevision.js";
import imageService from "../../services/image.js";
import { RESOURCE_DIR } from "../../services/resource_dir.js";
import { sanitizeSvg } from "../../services/utils.js";
function returnImageFromNote(req: Request<{ noteId: string }>, res: Response) {
const image = becca.getNote(req.params.noteId);
@@ -38,28 +39,33 @@ function returnImageInt(image: BNote | BRevision | null, res: Response) {
} else {
res.set("Content-Type", image.mime);
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(image.getContent());
if (image.mime === "image/svg+xml") {
sendSanitizedSvg(res, image.getContent());
} else {
res.send(image.getContent());
}
}
}
export function renderSvgAttachment(image: BNote | BRevision, res: Response, attachmentName: string) {
let svg: string | Buffer = `<svg xmlns="http://www.w3.org/2000/svg"></svg>`;
let svgContent: string | Buffer = `<svg xmlns="http://www.w3.org/2000/svg"></svg>`;
const attachment = image.getAttachmentByTitle(attachmentName);
if (attachment) {
svg = attachment.getContent();
svgContent = attachment.getContent();
} else {
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
const contentSvg = image.getJsonContentSafely()?.svg;
if (contentSvg) {
svg = contentSvg;
svgContent = contentSvg;
}
}
res.set("Content-Type", "image/svg+xml");
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(svg);
sendSanitizedSvg(res, svgContent);
}
export function renderPngAttachment(image: BNote | BRevision, res: Response, attachmentName: string) {
@@ -88,7 +94,12 @@ function returnAttachedImage(req: Request<{ attachmentId: string }>, res: Respon
res.set("Content-Type", attachment.mime);
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.send(attachment.getContent());
if (attachment.mime === "image/svg+xml") {
sendSanitizedSvg(res, attachment.getContent());
} else {
res.send(attachment.getContent());
}
}
function updateImage(req: Request<{ noteId: string }>) {
@@ -129,3 +140,9 @@ export default {
returnAttachedImage,
updateImage
};
function sendSanitizedSvg(res: Response, content: string | Buffer) {
const svgString = typeof content === "string" ? content : content.toString("utf-8");
res.set("Content-Security-Policy", "script-src 'none'");
res.send(sanitizeSvg(svgString));
}

View File

@@ -1,10 +1,16 @@
import { TextRepresentationResponse } from "@triliumnext/commons";
import type { OCRProcessResponse, TextRepresentationResponse } from "@triliumnext/commons";
import type { Request } from "express";
import becca from "../../becca/becca.js";
import ocrService from "../../services/ocr/ocr_service.js";
import options from "../../services/options.js";
import sql from "../../services/sql.js";
function getMinConfidenceThreshold(): number {
const minConfidence = options.getOption('ocrMinConfidence') ?? 0;
return parseFloat(minConfidence);
}
/**
* @swagger
* /api/ocr/process-note/{noteId}:
@@ -48,7 +54,7 @@ import sql from "../../services/sql.js";
* - session: []
* tags: ["ocr"]
*/
async function processNoteOCR(req: Request<{ noteId: string }>) {
async function processNoteOCR(req: Request<{ noteId: string }>): Promise<OCRProcessResponse | [number, OCRProcessResponse]> {
const { noteId } = req.params;
const { language, forceReprocess = false } = req.body || {};
@@ -62,7 +68,11 @@ async function processNoteOCR(req: Request<{ noteId: string }>) {
return [400, { success: false, message: 'Note is not an image or has unsupported format' }];
}
return { success: true, result };
return {
success: true,
result,
minConfidence: getMinConfidenceThreshold()
};
}
/**
@@ -108,7 +118,7 @@ async function processNoteOCR(req: Request<{ noteId: string }>) {
* - session: []
* tags: ["ocr"]
*/
async function processAttachmentOCR(req: Request<{ attachmentId: string }>) {
async function processAttachmentOCR(req: Request<{ attachmentId: string }>): Promise<OCRProcessResponse | [number, OCRProcessResponse]> {
const { attachmentId } = req.params;
const { language, forceReprocess = false } = req.body || {};
@@ -122,7 +132,11 @@ async function processAttachmentOCR(req: Request<{ attachmentId: string }>) {
return [400, { success: false, message: 'Attachment is not an image or has unsupported format' }];
}
return { success: true, result };
return {
success: true,
result,
minConfidence: getMinConfidenceThreshold()
};
}
/**

View File

@@ -5,7 +5,7 @@ import type { Request } from "express";
import ValidationError from "../../errors/validation_error.js";
import config from "../../services/config.js";
import { changeLanguage, getLocales } from "../../services/i18n.js";
import { changeLanguage } from "../../services/i18n.js";
import log from "../../services/log.js";
import optionService from "../../services/options.js";
import searchService from "../../services/search/services/search.js";
@@ -192,10 +192,6 @@ function getUserThemes() {
return ret;
}
function getSupportedLocales() {
return getLocales();
}
function isAllowed(name: string) {
return (ALLOWED_OPTIONS as Set<string>).has(name)
|| name.startsWith("keyboardShortcuts")
@@ -207,6 +203,5 @@ export default {
getOptions,
updateOption,
updateOptions,
getUserThemes,
getSupportedLocales
getUserThemes
};

View File

@@ -215,7 +215,6 @@ function register(app: express.Application) {
apiRoute(PUT, "/api/options/:name/:value", optionsApiRoute.updateOption);
apiRoute(PUT, "/api/options", optionsApiRoute.updateOptions);
apiRoute(GET, "/api/options/user-themes", optionsApiRoute.getUserThemes);
apiRoute(GET, "/api/options/locales", optionsApiRoute.getSupportedLocales);
apiRoute(PST, "/api/password/change", passwordApiRoute.changePassword);
apiRoute(PST, "/api/password/reset", passwordApiRoute.resetPassword);

View File

@@ -6,7 +6,7 @@ import build from "./build.js";
import dataDir from "./data_dir.js";
const APP_DB_VERSION = 236;
const SYNC_VERSION = 37;
const SYNC_VERSION = 38;
const CLIPPER_PROTOCOL_VERSION = "1.0";
export default {

View File

@@ -4,7 +4,7 @@ import sql_init from "./sql_init.js";
import { join } from "path";
import { getResourceDir } from "./utils.js";
import hidden_subtree from "./hidden_subtree.js";
import { dayjs, LOCALES, setDayjsLocale, type Dayjs, type Locale, type LOCALE_IDS } from "@triliumnext/commons";
import { dayjs, LOCALES, setDayjsLocale, type Dayjs, type LOCALE_IDS } from "@triliumnext/commons";
export async function initializeTranslations() {
const resourceDir = getResourceDir();
@@ -30,10 +30,6 @@ export function ordinal(date: Dayjs) {
.format("Do");
}
export function getLocales(): Locale[] {
return LOCALES;
}
function getCurrentLanguage(): LOCALE_IDS {
let language: string | null = null;
if (sql_init.isDbInitialized()) {

View File

@@ -51,8 +51,7 @@ async function importZip(taskContext: TaskContext<"importNotes">, fileBuffer: Bu
return "empty_note_id";
}
if (origNoteId === "root" || origNoteId.startsWith("_") || opts?.preserveIds) {
// these "named" noteIds don't differ between Trilium instances
if (origNoteId === "root" || opts?.preserveIds) {
return origNoteId;
}

View File

@@ -1,14 +1,14 @@
import type { NextFunction, Request, Response } from "express";
import openIDEncryption from "./encryption/open_id_encryption.js";
import sqlInit from "./sql_init.js";
import options from "./options.js";
import type { Session } from "express-openid-connect";
import sql from "./sql.js";
import config from "./config.js";
import config from "./config.js";
import openIDEncryption from "./encryption/open_id_encryption.js";
import options from "./options.js";
import sql from "./sql.js";
import sqlInit from "./sql_init.js";
function checkOpenIDConfig() {
const missingVars: string[] = []
const missingVars: string[] = [];
if (config.MultiFactorAuthentication.oauthBaseUrl === "") {
missingVars.push("oauthBaseUrl");
}
@@ -27,7 +27,7 @@ function isOpenIDEnabled() {
function isUserSaved() {
const data = sql.getValue<string>("SELECT isSetup FROM user_data;");
return data === "true" ? true : false;
return data === "true";
}
function getUsername() {
@@ -59,34 +59,31 @@ function getOAuthStatus() {
};
}
function isTokenValid(req: Request, res: Response, next: NextFunction) {
async function isTokenValid(req: Request, res: Response, next: NextFunction) {
const userStatus = openIDEncryption.isSubjectIdentifierSaved();
if (req.oidc !== undefined) {
const result = req.oidc
.fetchUserInfo()
.then((result) => {
return {
success: true,
message: "Token is valid",
user: userStatus,
};
})
.catch((result) => {
return {
success: false,
message: "Token is not valid",
user: userStatus,
};
});
return result;
} else {
return {
success: false,
message: "Token not set up",
user: userStatus,
};
try {
await req.oidc.fetchUserInfo();
return {
success: true,
message: "Token is valid",
user: userStatus,
};
} catch {
return {
success: false,
message: "Token is not valid",
user: userStatus,
};
}
}
return {
success: false,
message: "Token not set up",
user: userStatus,
};
}
function getSSOIssuerName() {
@@ -121,11 +118,10 @@ function generateOAuthConfig() {
scope: "openid profile email",
access_type: "offline",
prompt: "consent",
state: "random_state_" + Math.random().toString(36).substring(2)
},
routes: authRoutes,
idpLogout: true,
logoutParams: logoutParams,
logoutParams,
afterCallback: async (req: Request, res: Response, session: Session) => {
if (!sqlInit.isDbInitialized()) return session;

View File

@@ -705,3 +705,110 @@ describe("#slugify", () => {
expect(result).toBe(expectedSlug);
});
});
describe("#sanitizeSvg", () => {
it("should remove script elements", () => {
const maliciousSvg = '<svg><script>alert("XSS")</script><rect width="100" height="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100" height="100"/></svg>');
});
it("should remove script elements with attributes", () => {
const maliciousSvg = '<svg><script type="text/javascript">alert("XSS")</script></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg></svg>');
});
it("should remove multiline script elements", () => {
const maliciousSvg = `<svg><script>
var x = 1;
alert(x);
</script></svg>`;
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg></svg>');
});
it("should remove onclick event handlers with double quotes", () => {
const maliciousSvg = '<svg><rect onclick="doEvil()" width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should remove onclick event handlers with single quotes", () => {
const maliciousSvg = "<svg><rect onclick='doEvil()' width=\"100\"/></svg>";
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should remove onload event handlers", () => {
const maliciousSvg = '<svg onload="doEvil()"><rect width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should remove onerror event handlers", () => {
const maliciousSvg = '<svg><image onerror="alert(1)" href="invalid.jpg"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><image href="invalid.jpg"/></svg>');
});
it("should remove onmouseover event handlers", () => {
const maliciousSvg = '<svg><rect onmouseover="alert(1)" width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should remove event handlers without quotes", () => {
const maliciousSvg = '<svg><rect onclick=alert(1) width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
it("should replace javascript: URLs in href with #", () => {
const maliciousSvg = '<svg><a href="javascript:alert(1)"><text>Click me</text></a></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><a href="#"><text>Click me</text></a></svg>');
});
it("should replace javascript: URLs in xlink:href with #", () => {
const maliciousSvg = '<svg><a xlink:href="javascript:alert(1)"><text>Click me</text></a></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><a xlink:href="#"><text>Click me</text></a></svg>');
});
it("should preserve valid SVG content", () => {
const validSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect x="10" y="10" width="80" height="80" fill="blue"/><circle cx="50" cy="50" r="30" fill="red"/></svg>';
const result = utils.sanitizeSvg(validSvg);
expect(result).toBe(validSvg);
});
it("should preserve valid href URLs", () => {
const validSvg = '<svg><a href="https://example.com"><text>Link</text></a></svg>';
const result = utils.sanitizeSvg(validSvg);
expect(result).toBe(validSvg);
});
it("should handle multiple malicious elements", () => {
const maliciousSvg = '<svg onload="evil()"><script>evil()</script><rect onclick="bad()" width="100"/><a href="javascript:attack()">link</a></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/><a href="#">link</a></svg>');
});
it("should handle empty SVG", () => {
const emptySvg = '<svg></svg>';
const result = utils.sanitizeSvg(emptySvg);
expect(result).toBe('<svg></svg>');
});
it("should be case insensitive for script tags", () => {
const maliciousSvg = '<svg><SCRIPT>alert(1)</SCRIPT><Script>alert(2)</Script></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg></svg>');
});
it("should be case insensitive for event handlers", () => {
const maliciousSvg = '<svg><rect ONCLICK="alert(1)" width="100"/></svg>';
const result = utils.sanitizeSvg(maliciousSvg);
expect(result).toBe('<svg><rect width="100"/></svg>');
});
});

View File

@@ -119,6 +119,22 @@ export function sanitizeSqlIdentifier(str: string) {
return str.replace(/[^A-Za-z0-9_]/g, "");
}
/**
* Sanitize SVG to remove potentially dangerous elements and attributes.
* This prevents XSS via script injection in SVG content.
*/
export function sanitizeSvg(svg: string): string {
return svg
// Remove script elements
.replace(/<script[\s\S]*?<\/script>/gi, '')
// Remove on* event handlers (onclick, onload, onerror, etc.)
.replace(/\s+on\w+\s*=\s*["'][^"']*["']/gi, '')
.replace(/\s+on\w+\s*=\s*[^\s>]+/gi, '')
// Remove javascript: URLs
.replace(/href\s*=\s*["']javascript:[^"']*["']/gi, 'href="#"')
.replace(/xlink:href\s*=\s*["']javascript:[^"']*["']/gi, 'xlink:href="#"');
}
export const escapeHtml = escape;
export const unescapeHtml = unescape;
@@ -560,6 +576,7 @@ export default {
replaceAll,
safeExtractMessageAndStackFromError,
sanitizeSqlIdentifier,
sanitizeSvg,
stripTags,
slugify,
timeLimit,

View File

@@ -1,15 +1,14 @@
import type { NextFunction, Request, Response, Router } from "express";
import safeCompare from "safe-compare";
import type { NextFunction, Request, Response, Router } from "express";
import SearchContext from "../services/search/search_context.js";
import searchService from "../services/search/services/search.js";
import utils, { sanitizeSvg } from "../services/utils.js";
import { getDefaultTemplatePath, renderNoteContent } from "./content_renderer.js";
import type SAttachment from "./shaca/entities/sattachment.js";
import type SNote from "./shaca/entities/snote.js";
import shaca from "./shaca/shaca.js";
import shacaLoader from "./shaca/shaca_loader.js";
import searchService from "../services/search/services/search.js";
import SearchContext from "../services/search/search_context.js";
import type SNote from "./shaca/entities/snote.js";
import type SAttachment from "./shaca/entities/sattachment.js";
import { getDefaultTemplatePath, renderNoteContent } from "./content_renderer.js";
import utils from "../services/utils.js";
import { isShareDbReady } from "./sql.js";
function assertShareDbReady(_req: Request, res: Response, next: NextFunction) {
@@ -104,17 +103,18 @@ function renderImageAttachment(image: SNote, res: Response, attachmentName: stri
&& possibleSvgContent !== null
&& "svg" in possibleSvgContent
&& typeof possibleSvgContent.svg === "string")
? possibleSvgContent.svg
: null;
? possibleSvgContent.svg
: null;
if (contentSvg) {
svgString = contentSvg;
}
}
const svg = svgString;
const svg = sanitizeSvg(svgString);
res.set("Content-Type", "image/svg+xml");
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
res.set("Content-Security-Policy", "script-src 'none'");
res.send(svg);
}
@@ -320,7 +320,7 @@ function register(router: Router) {
return;
}
const searchContext = new SearchContext({ ancestorNoteId: ancestorNoteId });
const searchContext = new SearchContext({ ancestorNoteId });
const searchResults = searchService.findResultsWithQuery(search, searchContext);
const filteredResults = searchResults.map((sr) => {
const fullNote = shaca.notes[sr.noteId];

View File

@@ -10,7 +10,6 @@
},
"dependencies": {
"i18next": "26.0.3",
"i18next-http-backend": "3.0.4",
"preact": "10.29.0",
"preact-iso": "2.11.1",
"preact-render-to-string": "6.6.7",

View File

@@ -112,7 +112,8 @@
"header": {
"get-started": "Začít",
"documentation": "Dokumentace",
"support-us": "Podpořte nás"
"support-us": "Podpořte nás",
"resources": "Zdroje"
},
"footer": {
"copyright_and_the": " a ",
@@ -134,6 +135,74 @@
"buy_me_a_coffee": "Buy Me A Coffee"
},
"contribute": {
"title": "Další způsoby, jak přispět"
"title": "Další způsoby, jak přispět",
"way_translate": "Přeložte aplikaci do svého rodného jazyka prostřednictvím <Link>Weblate</Link>.",
"way_community": "Kontaktujte komunitu na <Discussions>GitHub Discussions</Discussions> nebo na <Matrix>Matrix</Matrix>.",
"way_reports": "Nahlaste chyby prostřednictvím <Link>GitHub issues</Link>.",
"way_document": "Vylepšujte dokumentaci tím, že nás upozorníte na její nedostatky, nebo přispějte do příruček, FAQ či návodů.",
"way_market": "Šířte dobrou věst: Sdílejte Trilium Notes s přáteli, na blogy nebo na sociální sítě."
},
"404": {
"title": "404: Nenalezeno",
"description": "Stránka, kterou jste hleděli, nebyla nalezena. Možná byla smazána nebo je URL adresa nesprávná."
},
"download_helper_desktop_windows": {
"title_x64": "Windows 64-bit",
"title_arm64": "Windows na ARM",
"description_x64": "Kompatibilní s zařízeními Intel nebo AMD s operačním systémem Windows 10 a 11.",
"description_arm64": "Kompatibilní s ARM zařízeními (např. s Qualcomm Snapdragon).",
"quick_start": "Pro instalaci pomocí Winget:",
"download_exe": "Stáhnout instalátor (.exe)",
"download_zip": "Přenosná verze (.zip)",
"download_scoop": "Scoop"
},
"download_helper_desktop_linux": {
"title_x64": "Linux 64-bit",
"title_arm64": "Linux na ARM",
"description_x64": "Pro většinu distribucí Linuxu, kompatibilní s architekturou x86_64.",
"description_arm64": "Pro distribuce Linuxu založené na ARM, kompatibilní s architekturou aarch64.",
"quick_start": "Vyberte vhodný formát balíčku podle vaší distribuce:",
"download_deb": ".deb",
"download_rpm": ".rpm",
"download_flatpak": ".flatpak",
"download_zip": "Přenosné (.zip)",
"download_nixpkgs": "nixpkgs",
"download_aur": "AUR"
},
"download_helper_desktop_macos": {
"title_x64": "macOS pro Intel",
"title_arm64": "macOS pro Apple Silicon",
"description_x64": "Pro Mac s procesorem Intel běžící pod macOS Monterey nebo novějším.",
"description_arm64": "Pro Mac s Apple Silicon, jako jsou modely s čipy M1 a M2.",
"quick_start": "Pro instalaci pomocí Homebrew:",
"download_dmg": "Stáhnout instalátor (.dmg)",
"download_homebrew_cask": "Homebrew Cask",
"download_zip": "Přenosná verze (.zip)"
},
"download_helper_server_docker": {
"title": "Vlastní hosting pomocí Docker",
"description": "Snadné nasazení na Windows, Linux nebo macOS pomocí Docker kontejneru.",
"download_dockerhub": "Docker Hub",
"download_ghcr": "ghcr.io"
},
"download_helper_server_linux": {
"title": "Vlastní hosting na Linuxu",
"description": "Nasazujte Trilium Notes na vlastním serveru nebo VPS, kompatibilní s většinou distribucí.",
"download_tar_x64": "x64 (.tar.xz)",
"download_tar_arm64": "ARM (.tar.xz)",
"download_nixos": "Modul pro NixOS"
},
"download_helper_server_hosted": {
"title": "Placený hosting",
"description": "Trilium Notes hostováno na PikaPods, placené službě pro snadný přístup a správu. Není přímo spojen s týmem Trilium.",
"download_pikapod": "Nastaveno na PikaPods",
"download_triliumcc": "Případně se podívejte na trilium.cc"
},
"resources": {
"title": "Zdroje",
"icon_packs": "Balíčky ikon",
"icon_packs_intro": "Rozšiřte výběr dostupných ikon pro své poznámky použitím balíčku ikon. Více informací o balíčcích ikon naleznete v <DocumentationLink>oficiální dokumentaci</DocumentationLink>.",
"download": "Stáhnout",
"website": "Webová stránka"
}
}

View File

@@ -7,6 +7,5 @@ To do so:
2. In `packages/commons` look for `dayjs.ts` and add a mapping for the new language in `DAYJS_LOADER`. Sort the entire list.
3. In `apps/client`, look for `collections/calendar/index.tsx` and modify `LOCALE_MAPPINGS` to add support to the new language.
4. In `apps/client`, look for `widgets/type_widgets/canvas/i18n.ts` and modify `LANGUAGE_MAPPINGS`. A unit test ensures that the language is actually loadable.
5. In `apps/client`, look for `widgets/type_widgets/MindMap.tsx` and modify `LOCALE_MAPPINGS`. The type definitions should already validate if the new value is supported by Mind Elixir.
6. In `packages/ckeditor5`, look for `i18n.ts` and modify `LOCALE_MAPPINGS`. The import validation should already check if the new value is supported by CKEditor, and there's also a test to ensure it.
7. Locale mappings for PDF.js might need adjustment. To do so, in `packages/pdfjs-viewer/scripts/build.ts` there is `LOCALE_MAPPINGS`.
5. In `packages/ckeditor5`, look for `i18n.ts` and modify `LOCALE_MAPPINGS`. The import validation should already check if the new value is supported by CKEditor, and there's also a test to ensure it.
6. Locale mappings for PDF.js might need adjustment. To do so, in `packages/pdfjs-viewer/scripts/build.ts` there is `LOCALE_MAPPINGS`.

272
docs/README-cs.md vendored
View File

@@ -124,123 +124,127 @@ Naše dokumenatce je dostupná ve vícero formátech:
motiv](https://docs.triliumnotes.org/user-guide/concepts/themes), podpora
uživatelských motivů
* [Evernote](https://docs.triliumnotes.org/user-guide/concepts/import-export/evernote)
and [Markdown import &
export](https://docs.triliumnotes.org/user-guide/concepts/import-export/markdown)
* [Web Clipper](https://docs.triliumnotes.org/user-guide/setup/web-clipper) for
easy saving of web content
* Customizable UI (sidebar buttons, user-defined widgets, ...)
* [Metrics](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics),
along with a Grafana Dashboard.
a [import & export
Markdown](https://docs.triliumnotes.org/user-guide/concepts/import-export/markdown)
* [Webový výstřižek](https://docs.triliumnotes.org/user-guide/setup/web-clipper)
pro snadné ukládání webového obsahu
* Přizpůsobitelné UI (tlačítka bočního panelu, uživatelsky definované widgety,
...)
* [Metriky](https://docs.triliumnotes.org/user-guide/advanced-usage/metrics)
spolu s Grafana Dashboard.
Check out the following third-party resources/communities for more TriliumNext
related goodies:
Podívejte se na následující externí zdroje/komunity pro další vychytávky
související s TriliumNext:
- [awesome-trilium](https://github.com/Nriver/awesome-trilium) for 3rd party
themes, scripts, plugins and more.
- [TriliumRocks!](https://trilium.rocks/) for tutorials, guides, and much more.
- [awesome-trilium](https://github.com/Nriver/awesome-trilium) pro externí
motivy, skripty, pluginy a další.
- [TriliumRocks!](https://trilium.rocks/) pro návody, průvodce a mnohem více.
## ❓Why TriliumNext?
## ❓Proč TriliumNext?
The original Trilium developer ([Zadam](https://github.com/zadam)) has
graciously given the Trilium repository to the community project which resides
at https://github.com/TriliumNext
Původní vývojář Trilium ([Zadam](https://github.com/zadam)) štědře předal
repozitář Trilium komunitnímu projektu, který sídlí na
https://github.com/TriliumNext
### ⬆Migrating from Zadam/Trilium?
### ⬆Migrujete ze Zadam/Trilium?
There are no special migration steps to migrate from a zadam/Trilium instance to
a TriliumNext/Trilium instance. Simply [install
TriliumNext/Trilium](#-installation) as usual and it will use your existing
database.
Neexistují žádné speciální kroky pro migraci z instance zadam/Trilium na
instanci TriliumNext/Trilium. Jednoduše si [ nainstalujte
TriliumNext/Trilium](#-installation) jako obvykle a použije vaši stávající
databázi.
Versions up to and including
[v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) are
compatible with the latest zadam/trilium version of
[v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later
versions of TriliumNext/Trilium have their sync versions incremented which
prevents direct migration.
Verze až do
[v0.90.4](https://github.com/TriliumNext/Trilium/releases/tag/v0.90.4) včetně
jsou kompatibilní s nejnovější verzí zadam/trilium
[v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Jakékoli
pozdější verze TriliumNext/Trilium mají zvýšené verze synchronizace, což brání
přímé migraci.
## 💬 Discuss with us
## 💬 Diskutujte s námi
Feel free to join our official conversations. We would love to hear what
features, suggestions, or issues you may have!
Nebojte se připojit k našim oficiálním konverzationím. Rádi uslyšíme vaše nápady
na funkce, návrhy nebo problémy!
- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous
discussions.)
- The `General` Matrix room is also bridged to
- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (Pro synchron
diskuse.)
- Pokoj Matrix `General` je také propojen s
[XMPP](xmpp:discuss@trilium.thisgreat.party?join)
- [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (For
asynchronous discussions.)
- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (For bug
reports and feature requests.)
- [Github Discussions](https://github.com/TriliumNext/Trilium/discussions) (Pro
asynchron diskuse.)
- [Github Issues](https://github.com/TriliumNext/Trilium/issues) (Pro hlášení
chyb a požadavky na funkce.)
## 🏗 Installation
## 🏗 Instalace
### Windows / MacOS
Download the binary release for your platform from the [latest release
page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the package
and run the `trilium` executable.
Stáhněte si binární verzi pro svou platformu z [stránky s nejnovější
verzí](https://github.com/TriliumNext/Trilium/releases/latest), rozbalte balíček
a spusťte spustitelný soubor `trilium`.
### Linux
If your distribution is listed in the table below, use your distribution's
package.
Pokud je vaše distribuce uvedena v níže uvedené tabulce, použijte balíček pro
vaši distribuci.
[![Packaging
status](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions)
[![Stav
balení](https://repology.org/badge/vertical-allrepos/triliumnext.svg)](https://repology.org/project/triliumnext/versions)
You may also download the binary release for your platform from the [latest
release page](https://github.com/TriliumNext/Trilium/releases/latest), unzip the
package and run the `trilium` executable.
Můžete si také stáhnout binární verzi pro svou platformu ze [stránky s
nejnovější verzí](https://github.com/TriliumNext/Trilium/releases/latest),
rozbalit balíček a spustit spustitelný soubor `trilium`.
TriliumNext is also provided as a Flatpak, but not yet published on FlatHub.
TriliumNext je k dispozici také jako Flatpak, ale ještě není zveřejněn na
FlatHub.
### Browser (any OS)
### Prohlížeč (jakýkoli OS)
If you use a server installation (see below), you can directly access the web
interface (which is almost identical to the desktop app).
Pokud používáte serverovou instalaci (viz níže), můžete přistupovat přímo k
webovému rozhraní (které je téměř identické s desktopovou aplikací).
Currently only the latest versions of Chrome & Firefox are supported (and
tested).
Momentálně jsou podporovány (a testovány) pouze nejnovější verze Chrome &
Firefox.
### Mobile
### Mobilní zařízení
To use TriliumNext on a mobile device, you can use a mobile web browser to
access the mobile interface of a server installation (see below).
Chcete-li používat TriliumNext na mobilním zařízení, můžete použít mobilní
webový prohlížeč k přístupu k mobilnímu rozhraní instalace serveru (viz níže).
See issue https://github.com/TriliumNext/Trilium/issues/4962 for more
information on mobile app support.
Více informací o podpoře mobilní aplikace naleznete v issue
https://github.com/TriliumNext/Trilium/issues/4962.
If you prefer a native Android app, you can use
Pokud preferujete nativní aplikaci pro Android, můžete použít
[TriliumDroid](https://apt.izzysoft.de/fdroid/index/apk/eu.fliegendewurst.triliumdroid).
Report bugs and missing features at [their
repository](https://github.com/FliegendeWurst/TriliumDroid). Note: It is best to
disable automatic updates on your server installation (see below) when using
TriliumDroid since the sync version must match between Trilium and TriliumDroid.
Chyby a chybějící funkce hlaste v [jejím
repozitáři](https://github.com/FliegendeWurst/TriliumDroid). Poznámka: Při
používání TriliumDroid je nejlepší zakázat automatické aktualizace na vaší
instalaci serveru (viz níže), protože verze pro synchronizaci musí mezi Trilium
a TriliumDroid souhlasit.
### Server
To install TriliumNext on your own server (including via Docker from
[Dockerhub](https://hub.docker.com/r/triliumnext/trilium)) follow [the server
installation docs](https://docs.triliumnotes.org/user-guide/setup/server).
Chcete-li nainstalovat TriliumNext na svůj vlastní server (včetně pomocí Docker
z [Dockerhub](https://hub.docker.com/r/triliumnext/trilium)), postupujte podle
[dokumentace k instalaci
serveru](https://docs.triliumnotes.org/user-guide/setup/server).
## 💻 Contribute
## 💻 Přispějte
### Translations
### Překlady
If you are a native speaker, help us translate Trilium by heading over to our
[Weblate page](https://hosted.weblate.org/engage/trilium/).
Pokud jste rodilý mluvčí, pomozte nám s překladem Trilium tím, že navštívíte
naši [stránku Weblate](https://hosted.weblate.org/engage/trilium/).
Here's the language coverage we have so far:
Zde je aktuální pokrytí jazyky:
[![Translation
status](https://hosted.weblate.org/widget/trilium/multi-auto.svg)](https://hosted.weblate.org/engage/trilium/)
[![Stav
překladu](https://hosted.weblate.org/widget/trilium/multi-auto.svg)](https://hosted.weblate.org/engage/trilium/)
### Code
### Kód
Download the repository, install dependencies using `pnpm` and then run the
server (available at http://localhost:8080):
Stáhněte si repozitář, nainstalujte závislosti pomocí `pnpm` a poté spusťte
server (dostupný na http://localhost:8080):
```shell
git clone https://github.com/TriliumNext/Trilium.git
cd Trilium
@@ -248,10 +252,10 @@ pnpm install
pnpm run server:start
```
### Documentation
### Dokumentace
Download the repository, install dependencies using `pnpm` and then run the
environment required to edit the documentation:
Stáhněte si repozitář, nainstalujte závislosti pomocí `pnpm` a poté spusťte
prostředí vyžadované pro úpravu dokumentace:
```shell
git clone https://github.com/TriliumNext/Trilium.git
cd Trilium
@@ -259,9 +263,9 @@ pnpm install
pnpm edit-docs:edit-docs
```
### Building the Executable
Download the repository, install dependencies using `pnpm` and then build the
desktop app for Windows:
### Kompilace spustitelného souboru
Stáhněte si repozitář, nainstalujte závislosti pomocí `pnpm` a poté sestavte
desktopovou aplikaci pro Windows:
```shell
git clone https://github.com/TriliumNext/Trilium.git
cd Trilium
@@ -269,71 +273,69 @@ pnpm install
pnpm run --filter desktop electron-forge:make --arch=x64 --platform=win32
```
For more details, see the [development
docs](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
Pro více podrobností navštivte [vývojovou
dokumentaci](https://github.com/TriliumNext/Trilium/tree/main/docs/Developer%20Guide/Developer%20Guide).
### Developer Documentation
### Vývojářská dokumentace
Please view the [documentation
guide](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md)
for details. If you have more questions, feel free to reach out via the links
described in the "Discuss with us" section above.
Podrobnosti naleznete v [průvodci
dokumentací](https://github.com/TriliumNext/Trilium/blob/main/docs/Developer%20Guide/Developer%20Guide/Environment%20Setup.md).
Pokud máte další dotazy, neváhejte nás kontaktovat prostřednictím odkazů
uvedených v sekci „Diskuse s námi“ výše.
## 👏 Shoutouts
## 👏 Poděkování
* [zadam](https://github.com/zadam) for the original concept and implementation
of the application.
* [Sarah Hussein](https://github.com/Sarah-Hussein) for designing the
application icon.
* [nriver](https://github.com/nriver) for his work on internationalization.
* [Thomas Frei](https://github.com/thfrei) for his original work on the Canvas.
* [antoniotejada](https://github.com/nriver) for the original syntax highlight
widget.
* [Dosu](https://dosu.dev/) for providing us with the automated responses to
GitHub issues and discussions.
* [Tabler Icons](https://tabler.io/icons) for the system tray icons.
* [zadam](https://github.com/zadam) za původní koncept a implementaci aplikace.
* [Sarah Hussein](https://github.com/Sarah-Hussein) za návrh ikony aplikace.
* [nriver](https://github.com/nriver) za jeho práci na internacionalizaci.
* [Thomas Frei](https://github.com/thfrei) za jeho původní práci na Plátně.
* [antoniotejada](https://github.com/nriver) za původní widget pro zvýrazňování
syntaxe.
* [Dosu](https://dosu.dev/) za poskytnutí automatických odpovědí na GitHub
issues a diskuse.
* [Tabler Icons](https://tabler.io/icons) za ikony v systémové oblasti.
Trilium would not be possible without the technologies behind it:
Trilium by nebyl možný bez technologií, které za ním stojí:
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - the visual editor behind
text notes. We are grateful for being offered a set of the premium features.
* [CodeMirror](https://github.com/codemirror/CodeMirror) - code editor with
support for huge amount of languages.
* [Excalidraw](https://github.com/excalidraw/excalidraw) - the infinite
whiteboard used in Canvas notes.
* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - providing the
mind map functionality.
* [Leaflet](https://github.com/Leaflet/Leaflet) - for rendering geographical
maps.
* [Tabulator](https://github.com/olifolkerd/tabulator) - for the interactive
table used in collections.
* [FancyTree](https://github.com/mar10/fancytree) - feature-rich tree library
without real competition.
* [jsPlumb](https://github.com/jsplumb/jsplumb) - visual connectivity library.
Used in [relation
maps](https://docs.triliumnotes.org/user-guide/note-types/relation-map) and
[link
maps](https://docs.triliumnotes.org/user-guide/advanced-usage/note-map#link-map)
* [CKEditor 5](https://github.com/ckeditor/ckeditor5) - vizuální editor pro
textové poznámky. Jsme vděční za nabídku sady prémiových funkcí.
* [CodeMirror](https://github.com/codemirror/CodeMirror) - editor kódu s
podporou obrovského množství jazyků.
* [Excalidraw](https://github.com/excalidraw/excalidraw) - nekonečná bílá tabule
používaná v poznámkách typu Canvas.
* [Mind Elixir](https://github.com/SSShooter/mind-elixir-core) - poskytuje
funkcionalitu myšlenkových map.
* [Leaflet](https://github.com/Leaflet/Leaflet) - pro vykreslování geografických
map.
* [Tabulator](https://github.com/olifolkerd/tabulator) - pro interaktiv
tabulku používanou v kolekcích.
* [FancyTree](https://github.com/mar10/fancytree) - bohatá knihovna pro stromové
struktury bez skutečné konkurence.
* [jsPlumb](https://github.com/jsplumb/jsplumb) - knihovna pro vizuální
propojení. Používá se v [mapách
vazeb](https://docs.triliumnotes.org/user-guide/note-types/relation-map) a
[mapách
odkazů](https://docs.triliumnotes.org/user-guide/advanced-usage/note-map#link-map)
## 🤝 Support
## 🤝 Podpora
Trilium is built and maintained with [hundreds of hours of
work](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Your
support keeps it open-source, improves features, and covers costs such as
hosting.
Trilium je vyvíjen a udržován s [úsilím stovek hodin
práce](https://github.com/TriliumNext/Trilium/graphs/commit-activity). Vaše
podpora pomáhá udržovat projekt jako open-source, vylepšuje funkce a pokrývá
náklady, jako je hosting.
Consider supporting the main developer
([eliandoran](https://github.com/eliandoran)) of the application via:
Zvažte podporu hlavního vývojáře ([eliandoran](https://github.com/eliandoran))
aplikace prostřednictvím:
- [GitHub Sponsors](https://github.com/sponsors/eliandoran)
- [PayPal](https://paypal.me/eliandoran)
- [Buy Me a Coffee](https://buymeacoffee.com/eliandoran)
## 🔑 License
## 🔑 Licence
Copyright 2017-2025 zadam, Elian Doran, and other contributors
Copyright 2017-2025 zadam, Elian Doran a ostatní přispěvatelé
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU Affero General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option) any
later version.
Tento program je volný software: můžete jej redistribuovat a/nebo upravovat za
podmínek GNU Affero General Public License, jak jej vydala Free Software
Foundation, buď ve verzi 3 této licence, nebo (volitelně) jakoukoli pozdější
verzi.

View File

@@ -1,6 +1,6 @@
{
"formatVersion": 2,
"appVersion": "0.102.0",
"appVersion": "0.102.1",
"files": [
{
"isClone": false,
@@ -61,6 +61,32 @@
"attachments": [],
"dirFileName": "Release Notes",
"children": [
{
"isClone": false,
"noteId": "ZdWJsMQvY1fo",
"notePath": [
"hD3V4hiu2VW4",
"ZdWJsMQvY1fo"
],
"title": "v0.102.2",
"notePosition": 10,
"prefix": null,
"isExpanded": false,
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "template",
"value": "wyurrlcDl416",
"isInheritable": false,
"position": 60
}
],
"format": "markdown",
"dataFileName": "v0.102.2.md",
"attachments": []
},
{
"isClone": false,
"noteId": "4FTGCuCiG7s7",
@@ -69,7 +95,7 @@
"4FTGCuCiG7s7"
],
"title": "v0.102.1",
"notePosition": 10,
"notePosition": 20,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -95,7 +121,7 @@
"d582eD4RY4OM"
],
"title": "v0.102.0",
"notePosition": 20,
"notePosition": 30,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -121,7 +147,7 @@
"IlBzLeN3MJhw"
],
"title": "v0.101.3",
"notePosition": 30,
"notePosition": 40,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -147,7 +173,7 @@
"vcBthaXcwAm6"
],
"title": "v0.101.2",
"notePosition": 40,
"notePosition": 50,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -173,7 +199,7 @@
"AgUcrU9nFXuW"
],
"title": "v0.101.1",
"notePosition": 50,
"notePosition": 60,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -199,7 +225,7 @@
"uYwlZ594eyJu"
],
"title": "v0.101.0",
"notePosition": 60,
"notePosition": 70,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -225,7 +251,7 @@
"iPGKEk7pwJXK"
],
"title": "v0.100.0",
"notePosition": 70,
"notePosition": 80,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -251,7 +277,7 @@
"7HKMTjmopLcM"
],
"title": "v0.99.5",
"notePosition": 80,
"notePosition": 90,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -277,7 +303,7 @@
"RMBaNYPsRpIr"
],
"title": "v0.99.4",
"notePosition": 90,
"notePosition": 100,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -303,7 +329,7 @@
"yuroLztFfpu5"
],
"title": "v0.99.3",
"notePosition": 100,
"notePosition": 110,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -329,7 +355,7 @@
"z207sehwMJ6C"
],
"title": "v0.99.2",
"notePosition": 110,
"notePosition": 120,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -355,7 +381,7 @@
"WGQsXq2jNyTi"
],
"title": "v0.99.1",
"notePosition": 120,
"notePosition": 130,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -381,7 +407,7 @@
"cyw2Yue9vXf3"
],
"title": "v0.99.0",
"notePosition": 130,
"notePosition": 140,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -407,7 +433,7 @@
"QOJwjruOUr4k"
],
"title": "v0.98.1",
"notePosition": 140,
"notePosition": 150,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -433,7 +459,7 @@
"PLUoryywi0BC"
],
"title": "v0.98.0",
"notePosition": 150,
"notePosition": 160,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -459,7 +485,7 @@
"lvOuiWsLDv8F"
],
"title": "v0.97.2",
"notePosition": 160,
"notePosition": 170,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -485,7 +511,7 @@
"OtFZ6Nd9vM3n"
],
"title": "v0.97.1",
"notePosition": 170,
"notePosition": 180,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -511,7 +537,7 @@
"SJZ5PwfzHSQ1"
],
"title": "v0.97.0",
"notePosition": 180,
"notePosition": 190,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -537,7 +563,7 @@
"mYXFde3LuNR7"
],
"title": "v0.96.0",
"notePosition": 190,
"notePosition": 200,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -563,7 +589,7 @@
"jthwbL0FdaeU"
],
"title": "v0.95.0",
"notePosition": 200,
"notePosition": 210,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -589,7 +615,7 @@
"7HGYsJbLuhnv"
],
"title": "v0.94.1",
"notePosition": 210,
"notePosition": 220,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -615,7 +641,7 @@
"Neq53ujRGBqv"
],
"title": "v0.94.0",
"notePosition": 220,
"notePosition": 230,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -641,7 +667,7 @@
"VN3xnce1vLkX"
],
"title": "v0.93.0",
"notePosition": 230,
"notePosition": 240,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -659,7 +685,7 @@
"WRaBfQqPr6qo"
],
"title": "v0.92.7",
"notePosition": 240,
"notePosition": 250,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -685,7 +711,7 @@
"a2rwfKNmUFU1"
],
"title": "v0.92.6",
"notePosition": 250,
"notePosition": 260,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -703,7 +729,7 @@
"fEJ8qErr0BKL"
],
"title": "v0.92.5-beta",
"notePosition": 260,
"notePosition": 270,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -721,7 +747,7 @@
"kkkZQQGSXjwy"
],
"title": "v0.92.4",
"notePosition": 270,
"notePosition": 280,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -739,7 +765,7 @@
"vAroNixiezaH"
],
"title": "v0.92.3-beta",
"notePosition": 280,
"notePosition": 290,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -757,7 +783,7 @@
"mHEq1wxAKNZd"
],
"title": "v0.92.2-beta",
"notePosition": 290,
"notePosition": 300,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -775,7 +801,7 @@
"IykjoAmBpc61"
],
"title": "v0.92.1-beta",
"notePosition": 300,
"notePosition": 310,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -793,7 +819,7 @@
"dq2AJ9vSBX4Y"
],
"title": "v0.92.0-beta",
"notePosition": 310,
"notePosition": 320,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -811,7 +837,7 @@
"3a8aMe4jz4yM"
],
"title": "v0.91.6",
"notePosition": 320,
"notePosition": 330,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -829,7 +855,7 @@
"8djQjkiDGESe"
],
"title": "v0.91.5",
"notePosition": 330,
"notePosition": 340,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -847,7 +873,7 @@
"OylxVoVJqNmr"
],
"title": "v0.91.4-beta",
"notePosition": 340,
"notePosition": 350,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -865,7 +891,7 @@
"tANGQDvnyhrj"
],
"title": "v0.91.3-beta",
"notePosition": 350,
"notePosition": 360,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -883,7 +909,7 @@
"hMoBfwSoj1SC"
],
"title": "v0.91.2-beta",
"notePosition": 360,
"notePosition": 370,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -901,7 +927,7 @@
"a2XMSKROCl9z"
],
"title": "v0.91.1-beta",
"notePosition": 370,
"notePosition": 380,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -919,7 +945,7 @@
"yqXFvWbLkuMD"
],
"title": "v0.90.12",
"notePosition": 380,
"notePosition": 390,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -937,7 +963,7 @@
"veS7pg311yJP"
],
"title": "v0.90.11-beta",
"notePosition": 390,
"notePosition": 400,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -955,7 +981,7 @@
"sq5W9TQxRqMq"
],
"title": "v0.90.10-beta",
"notePosition": 400,
"notePosition": 410,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -973,7 +999,7 @@
"yFEGVCUM9tPx"
],
"title": "v0.90.9-beta",
"notePosition": 410,
"notePosition": 420,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -991,7 +1017,7 @@
"o4wAGqOQuJtV"
],
"title": "v0.90.8",
"notePosition": 420,
"notePosition": 430,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1024,7 +1050,7 @@
"i4A5g9iOg9I0"
],
"title": "v0.90.7-beta",
"notePosition": 430,
"notePosition": 440,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1042,7 +1068,7 @@
"ThNf2GaKgXUs"
],
"title": "v0.90.6-beta",
"notePosition": 440,
"notePosition": 450,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1060,7 +1086,7 @@
"G4PAi554kQUr"
],
"title": "v0.90.5-beta",
"notePosition": 450,
"notePosition": 460,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1087,7 +1113,7 @@
"zATRobGRCmBn"
],
"title": "v0.90.4",
"notePosition": 460,
"notePosition": 470,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1105,7 +1131,7 @@
"sCDLf8IKn3Iz"
],
"title": "v0.90.3",
"notePosition": 470,
"notePosition": 480,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1123,7 +1149,7 @@
"VqqyBu4AuTjC"
],
"title": "v0.90.2-beta",
"notePosition": 480,
"notePosition": 490,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1141,7 +1167,7 @@
"RX3Nl7wInLsA"
],
"title": "v0.90.1-beta",
"notePosition": 490,
"notePosition": 500,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1159,7 +1185,7 @@
"GyueACukPWjk"
],
"title": "v0.90.0-beta",
"notePosition": 500,
"notePosition": 510,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1177,7 +1203,7 @@
"kzjHexDTTeVB"
],
"title": "v0.48",
"notePosition": 510,
"notePosition": 520,
"prefix": null,
"isExpanded": false,
"type": "text",
@@ -1244,7 +1270,7 @@
"wyurrlcDl416"
],
"title": "Release Template",
"notePosition": 520,
"notePosition": 530,
"prefix": null,
"isExpanded": false,
"type": "text",

View File

@@ -32,4 +32,8 @@
## 🛠️ Technical updates
* \[…\]
## 🔒️ Security improvements
* \[…\]

View File

@@ -0,0 +1,37 @@
# v0.102.2
> [!IMPORTANT]
> **This release contains important security fixes. All users are strongly encouraged to update immediately.**
>
> Several vulnerabilities affecting content handling and the desktop application have been addressed. We recommend upgrading before the next scheduled release to ensure your installation is protected.
> [!NOTE]
> If you enjoyed this release, consider showing a token of appreciation by:
>
> * Pressing the “Star” button on [GitHub](https://github.com/TriliumNext/Trilium) (top-right).
> * Considering a one-time or recurrent donation to the [lead developer](https://github.com/eliandoran) via [GitHub Sponsors](https://github.com/sponsors/eliandoran) or [PayPal](https://paypal.me/eliandoran).
> * If you are interested in an [official mobile application](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/7447)  ([#7447](https://github.com/TriliumNext/Trilium/issues/7447)) or [multi-user support](https://oss.issuehunt.io/r/TriliumNext/Trilium/issues/4956) ([#4956](https://github.com/TriliumNext/Trilium/issues/4956)), consider offering financial support via IssueHunt (see links).
## 🔒️ Security improvements
* Content Handling
* Improved request handling for SVG content in share routes
* Improved request handling for SVG content in the main API
* Enhanced content rendering in the Mermaid diagram editor
* Fixed toast notifications to properly escape content
* Added validation for the `docName` attribute in the document renderer
* Marked `docName` as a sensitive attribute in the commons module
* Desktop Application (Electron)
* Added Electron fuses to harden the desktop application against external abuse
* Improved application integrity checks
* API & Import
* Added MIME type validation for image uploads via ETAPI
* Aligned attachment upload validation with note upload validation
* Import no longer preserves named note IDs to prevent potential conflicts
* Authentication
* OpenID Connect now uses a more secure random number generator
We've also updated our SECURITY.MD file to detail our security practices and how to report vulnerabilities.

View File

@@ -151,9 +151,10 @@
runHook postInstall
'';
# This file is a symlink into /build which is not allowed.
# Symlinks pointing to /build directory are not allowed in the Nix store.
# This removes all dangling symlinks that point to the temporary build directory.
postFixup = ''
find $out/opt -name prebuild-install -path "*/better-sqlite3/node_modules/.bin/*" -delete || true
find $out/opt -type l -lname '/build/*' -delete || true
'';
components = [

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/source",
"version": "0.102.1",
"version": "0.102.2",
"description": "Build your personal knowledge base with Trilium Notes",
"directories": {
"doc": "docs"
@@ -48,7 +48,7 @@
"devDependencies": {
"@electron/rebuild": "4.0.3",
"@fast-csv/parse": "5.0.5",
"@playwright/test": "1.59.0",
"@playwright/test": "1.59.1",
"@triliumnext/server": "workspace:*",
"@types/express": "5.0.6",
"@types/js-yaml": "4.0.9",
@@ -59,7 +59,7 @@
"chalk": "5.6.2",
"cross-env": "10.1.0",
"dpdm": "4.0.1",
"esbuild": "0.27.4",
"esbuild": "0.28.0",
"eslint": "10.1.0",
"eslint-config-preact": "2.0.0",
"eslint-config-prettier": "10.1.8",
@@ -105,7 +105,7 @@
"@codemirror/language": "6.12.3",
"@lezer/highlight": "1.2.3",
"@lezer/common": "1.5.1",
"mermaid": "11.13.0",
"mermaid": "11.14.0",
"preact": "10.29.0",
"roughjs": "4.6.6",
"@types/express-serve-static-core": "5.1.1",
@@ -158,6 +158,7 @@
"handlebars@<4.7.9": ">=4.7.9",
"qs@<6.14.2": ">=6.14.2",
"minimatch@<3.1.4": "^3.1.4",
"minimatch@3>brace-expansion": "^1.1.13",
"serialize-javascript@<7.0.5": ">=7.0.5",
"webpack@<5.104.1": ">=5.104.1"
},

View File

@@ -16,12 +16,7 @@
"node": ">=18.0.0",
"npm": ">=5.7.1"
},
"files": [
"dist",
"ckeditor5-metadata.json"
],
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "55.3.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@typescript-eslint/eslint-plugin": "8.58.0",
"@typescript-eslint/parser": "8.58.0",
@@ -43,9 +38,6 @@
"author": "Elian Doran <contact@eliandoran.me>",
"license": "GPL-2.0-or-later",
"scripts": {
"build": "node ./scripts/build-dist.mjs",
"ts:build": "tsc -p ./tsconfig.release.json",
"ts:clear": "npx rimraf --glob \"src/**/*.@(js|d.ts)\"",
"lint": "eslint \"**/*.{js,ts}\" --quiet",
"stylelint": "stylelint --quiet --allow-empty-input 'theme/**/*.css'",
"test": "vitest",

View File

@@ -1,47 +0,0 @@
#!/usr/bin/env node
/**
* @license Copyright (c) 2020-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/* eslint-env node */
import { createRequire } from 'module';
import upath from 'upath';
import chalk from 'chalk';
import { build } from '@ckeditor/ckeditor5-dev-build-tools';
function dist( path ) {
return upath.join( 'dist', path );
}
( async () => {
const tsconfig = 'tsconfig.dist.ckeditor5.json';
/**
* Step 1
*/
console.log( chalk.cyan( '1/2: Generating NPM build...' ) );
const require = createRequire( import.meta.url );
const pkg = require( upath.resolve( process.cwd(), './package.json' ) );
await build( {
input: 'src/index.ts',
output: dist( './index.js' ),
tsconfig: 'tsconfig.dist.json',
external: [
'ckeditor5',
'ckeditor5-premium-features',
...Object.keys( {
...pkg.dependencies,
...pkg.peerDependencies
} )
],
clean: true,
sourceMap: true,
declarations: true,
translations: '**/*.po'
} );
} )();

View File

@@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"rootDir": "./src",
"composite": false,
"types": [
"../typings/types.d.ts"
]
}
}

View File

@@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"declaration": true
},
"exclude": [
"./tests/",
"./src",
"./sample/"
]
}

View File

@@ -17,12 +17,7 @@
"node": ">=18.0.0",
"npm": ">=5.7.1"
},
"files": [
"dist",
"ckeditor5-metadata.json"
],
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "55.3.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@typescript-eslint/eslint-plugin": "8.58.0",
"@typescript-eslint/parser": "8.58.0",
@@ -42,9 +37,6 @@
"ckeditor5": "48.0.0"
},
"scripts": {
"build": "node ./scripts/build-dist.mjs",
"ts:build": "tsc -p ./tsconfig.release.json",
"ts:clear": "npx rimraf --glob \"src/**/*.@(js|d.ts)\"",
"lint": "eslint \"**/*.{js,ts}\" --quiet",
"stylelint": "stylelint --quiet --allow-empty-input 'theme/**/*.css'",
"test": "vitest",

View File

@@ -1,47 +0,0 @@
#!/usr/bin/env node
/**
* @license Copyright (c) 2020-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/* eslint-env node */
import { createRequire } from 'module';
import upath from 'upath';
import chalk from 'chalk';
import { build } from '@ckeditor/ckeditor5-dev-build-tools';
function dist( path ) {
return upath.join( 'dist', path );
}
( async () => {
const tsconfig = 'tsconfig.dist.ckeditor5.json';
/**
* Step 1
*/
console.log( chalk.cyan( '1/2: Generating NPM build...' ) );
const require = createRequire( import.meta.url );
const pkg = require( upath.resolve( process.cwd(), './package.json' ) );
await build( {
input: 'src/index.ts',
output: dist( './index.js' ),
tsconfig: 'tsconfig.dist.json',
external: [
'ckeditor5',
'ckeditor5-premium-features',
...Object.keys( {
...pkg.dependencies,
...pkg.peerDependencies
} )
],
clean: true,
sourceMap: true,
declarations: true,
translations: '**/*.po'
} );
} )();

View File

@@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"rootDir": "./src",
"composite": false,
"types": [
"../typings/types.d.ts"
]
}
}

View File

@@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"declaration": true
},
"exclude": [
"./tests/",
"./src",
"./sample/"
]
}

View File

@@ -19,12 +19,7 @@
"node": ">=18.0.0",
"npm": ">=5.7.1"
},
"files": [
"dist",
"ckeditor5-metadata.json"
],
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "55.3.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@typescript-eslint/eslint-plugin": "8.58.0",
"@typescript-eslint/parser": "8.58.0",
@@ -44,9 +39,6 @@
"ckeditor5": "48.0.0"
},
"scripts": {
"build": "node ./scripts/build-dist.mjs",
"ts:build": "tsc -p ./tsconfig.release.json",
"ts:clear": "npx rimraf --glob \"src/**/*.@(js|d.ts)\"",
"lint": "eslint \"**/*.{js,ts}\" --quiet",
"stylelint": "stylelint --quiet --allow-empty-input 'theme/**/*.css'",
"test": "vitest",

View File

@@ -1,47 +0,0 @@
#!/usr/bin/env node
/**
* @license Copyright (c) 2020-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/* eslint-env node */
import { createRequire } from 'module';
import upath from 'upath';
import chalk from 'chalk';
import { build } from '@ckeditor/ckeditor5-dev-build-tools';
function dist( path ) {
return upath.join( 'dist', path );
}
( async () => {
const tsconfig = 'tsconfig.dist.ckeditor5.json';
/**
* Step 1
*/
console.log( chalk.cyan( '1/2: Generating NPM build...' ) );
const require = createRequire( import.meta.url );
const pkg = require( upath.resolve( process.cwd(), './package.json' ) );
await build( {
input: 'src/index.ts',
output: dist( './index.js' ),
tsconfig: 'tsconfig.dist.json',
external: [
'ckeditor5',
'ckeditor5-premium-features',
...Object.keys( {
...pkg.dependencies,
...pkg.peerDependencies
} )
],
clean: true,
sourceMap: true,
declarations: true,
translations: '**/*.po'
} );
} )();

View File

@@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"rootDir": "./src",
"composite": false,
"types": [
"../typings/types.d.ts"
]
}
}

View File

@@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"declaration": true
},
"exclude": [
"./tests/",
"./src",
"./sample/"
]
}

View File

@@ -19,12 +19,7 @@
"node": ">=18.0.0",
"npm": ">=5.7.1"
},
"files": [
"dist",
"ckeditor5-metadata.json"
],
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "55.3.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@typescript-eslint/eslint-plugin": "8.58.0",
"@typescript-eslint/parser": "8.58.0",
@@ -44,9 +39,6 @@
"ckeditor5": "48.0.0"
},
"scripts": {
"build": "node ./scripts/build-dist.mjs",
"ts:build": "tsc -p ./tsconfig.release.json",
"ts:clear": "npx rimraf --glob \"src/**/*.@(js|d.ts)\"",
"lint": "eslint \"**/*.{js,ts}\" --quiet",
"stylelint": "stylelint --quiet --allow-empty-input 'theme/**/*.css'",
"test": "vitest",
@@ -54,6 +46,6 @@
},
"dependencies": {
"@ckeditor/ckeditor5-icons": "48.0.0",
"mathlive": "0.109.0"
"mathlive": "0.109.1"
}
}

View File

@@ -1,47 +0,0 @@
#!/usr/bin/env node
/**
* @license Copyright (c) 2020-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/* eslint-env node */
import { createRequire } from 'module';
import upath from 'upath';
import chalk from 'chalk';
import { build } from '@ckeditor/ckeditor5-dev-build-tools';
function dist( path ) {
return upath.join( 'dist', path );
}
( async () => {
const tsconfig = 'tsconfig.dist.ckeditor5.json';
/**
* Step 1
*/
console.log( chalk.cyan( '1/2: Generating NPM build...' ) );
const require = createRequire( import.meta.url );
const pkg = require( upath.resolve( process.cwd(), './package.json' ) );
await build( {
input: 'src/index.ts',
output: dist( './index.js' ),
tsconfig: 'tsconfig.dist.json',
external: [
'ckeditor5',
'ckeditor5-premium-features',
...Object.keys( {
...pkg.dependencies,
...pkg.peerDependencies
} )
],
clean: true,
sourceMap: true,
declarations: true,
translations: '**/*.po'
} );
} )();

View File

@@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"rootDir": "./src",
"composite": false,
"types": [
"../typings/types.d.ts"
]
}
}

View File

@@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"declaration": true
},
"exclude": [
"./tests/",
"./src",
"./sample/"
]
}

View File

@@ -8,3 +8,6 @@ sample/ckeditor.dist.js
# Ignore compiled TypeScript files.
src/**/*.js
src/**/*.d.ts
.vitest-attachments
tests/__screenshots__

View File

@@ -19,12 +19,7 @@
"node": ">=18.0.0",
"npm": ">=5.7.1"
},
"files": [
"dist",
"ckeditor5-metadata.json"
],
"devDependencies": {
"@ckeditor/ckeditor5-dev-build-tools": "55.3.0",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@typescript-eslint/eslint-plugin": "8.58.0",
"@typescript-eslint/parser": "8.58.0",
@@ -44,16 +39,9 @@
"ckeditor5": "48.0.0"
},
"scripts": {
"build": "node ./scripts/build-dist.mjs",
"ts:build": "tsc -p ./tsconfig.release.json",
"ts:clear": "npx rimraf --glob \"src/**/*.@(js|d.ts)\"",
"lint": "eslint \"**/*.{js,ts}\" --quiet",
"stylelint": "stylelint --quiet --allow-empty-input 'theme/**/*.css'",
"test": "vitest",
"test:debug": "vitest --inspect-brk --no-file-parallelism --browser.headless=false"
},
"dependencies": {
"@types/lodash-es": "4.17.12",
"lodash-es": "4.18.1"
}
}

View File

@@ -1,47 +0,0 @@
#!/usr/bin/env node
/**
* @license Copyright (c) 2020-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md.
*/
/* eslint-env node */
import { createRequire } from 'module';
import upath from 'upath';
import chalk from 'chalk';
import { build } from '@ckeditor/ckeditor5-dev-build-tools';
function dist( path ) {
return upath.join( 'dist', path );
}
( async () => {
const tsconfig = 'tsconfig.dist.ckeditor5.json';
/**
* Step 1
*/
console.log( chalk.cyan( '1/2: Generating NPM build...' ) );
const require = createRequire( import.meta.url );
const pkg = require( upath.resolve( process.cwd(), './package.json' ) );
await build( {
input: 'src/index.ts',
output: dist( './index.js' ),
tsconfig: 'tsconfig.dist.json',
external: [
'ckeditor5',
'ckeditor5-premium-features',
...Object.keys( {
...pkg.dependencies,
...pkg.peerDependencies
} )
],
clean: true,
sourceMap: true,
declarations: true,
translations: '**/*.po'
} );
} )();

View File

@@ -5,7 +5,8 @@ import MermaidUI from './mermaidui.js';
declare global {
interface MermaidInstance {
init(config: MermaidConfig, element: HTMLElement): void;
initialize(config: MermaidConfig): void;
render(id: string, source: string): Promise<{ svg: string }>;
}
interface MermaidConfig {

View File

@@ -2,13 +2,13 @@
* @module mermaid/mermaidediting
*/
import { debounce } from 'lodash-es';
import MermaidPreviewCommand from './commands/mermaidPreviewCommand.js';
import MermaidSourceViewCommand from './commands/mermaidSourceViewCommand.js';
import MermaidSplitViewCommand from './commands/mermaidSplitViewCommand.js';
import InsertMermaidCommand from './commands/insertMermaidCommand.js';
import { DowncastAttributeEvent, DowncastConversionApi, EditorConfig, ModelElement, EventInfo, ModelItem, ModelNode, Plugin, toWidget, UpcastConversionApi, UpcastConversionData, ViewElement, ViewText, ViewUIElement } from 'ckeditor5';
import { DowncastAttributeEvent, DowncastConversionApi, EditorConfig, ModelElement, EventInfo, ModelItem, ModelNode, Plugin, toWidget, uid, UpcastConversionApi, UpcastConversionData, ViewElement, ViewText, ViewUIElement } from 'ckeditor5';
import { debounce } from './utils.js';
// Time in milliseconds.
const DEBOUNCE_TIME = 300;
@@ -20,7 +20,8 @@ type DowncastConversionData = DowncastAttributeEvent["args"][0];
export default class MermaidEditing extends Plugin {
private _config!: EditorConfig["mermaid"];
private mermaid?: MermaidInstance;
private _mermaidPromise?: Promise<MermaidInstance>;
private _renderGeneration = 0;
/**
* @inheritDoc
@@ -179,16 +180,10 @@ export default class MermaidEditing extends Plugin {
}
function createMermaidPreview(this: ViewUIElement, domDocument: Document ) {
// Taking the text from the wrapper container element for now
const mermaidSource = data.item.getAttribute( 'source' ) as string;
const domElement = this.toDomElement( domDocument );
domElement.innerHTML = mermaidSource;
window.setTimeout( () => {
// @todo: by the looks of it the domElement needs to be hooked to tree in order to allow for rendering.
that._renderMermaid( domElement );
}, 100 );
that._renderMermaid( domElement, mermaidSource );
return domElement;
}
@@ -219,10 +214,7 @@ export default class MermaidEditing extends Plugin {
const domPreviewWrapper = domConverter.viewToDom(child);
if ( domPreviewWrapper ) {
domPreviewWrapper.innerHTML = newSource;
domPreviewWrapper.removeAttribute( 'data-processed' );
this._renderMermaid( domPreviewWrapper );
this._renderMermaid( domPreviewWrapper, newSource );
}
}
}
@@ -263,14 +255,36 @@ export default class MermaidEditing extends Plugin {
}
/**
* Renders Mermaid in a given `domElement`. Expect this domElement to have mermaid
* source set as text content.
* Renders Mermaid (a parsed `source`) in a given `domElement`.
*/
async _renderMermaid( domElement: HTMLElement ) {
if (!window.mermaid && typeof this._config?.lazyLoad === "function") {
this.mermaid = await this._config.lazyLoad();
async _renderMermaid( domElement: HTMLElement, source: string ) {
if ( !this._mermaidPromise && typeof this._config?.lazyLoad === 'function' ) {
this._mermaidPromise = Promise.resolve( this._config.lazyLoad() ).then( instance => {
instance.initialize( this._config?.config ?? {} );
return instance;
} );
}
this.mermaid?.init( this._config?.config ?? {}, domElement );
const mermaid = await this._mermaidPromise;
if ( !mermaid ) {
return;
}
const generation = ++this._renderGeneration;
const id = `ck-mermaid-${ uid() }`;
try {
const { svg } = await mermaid.render( id, source );
if ( generation === this._renderGeneration ) {
domElement.innerHTML = svg;
}
} catch ( err: any ) {
if ( generation === this._renderGeneration ) {
domElement.innerText = err.message;
}
document.getElementById( id )?.remove();
}
}
}

View File

@@ -5,6 +5,16 @@
import { Editor } from "ckeditor5";
export function debounce<T extends (...args: any[]) => void>( fn: T, waitMs: number ): T {
let timeout: ReturnType<typeof setTimeout> | null = null;
return function( this: unknown, ...args: Parameters<T> ) {
if ( timeout ) {
clearTimeout( timeout );
}
timeout = setTimeout( () => fn.apply( this, args ), waitMs );
} as T;
}
/**
* Helper function for setting the `isOn` state of buttons.
*

View File

@@ -1,8 +1,6 @@
import { ClassicEditor, Essentials, Paragraph, Heading, CodeBlockEditing, _setModelData as setModelData, _getModelData as getModelData, _getViewData as getViewData } from 'ckeditor5';
import { ClassicEditor, Essentials, Paragraph, Heading, CodeBlockEditing, ViewElement, _setModelData as setModelData, _getModelData as getModelData, _getViewData as getViewData } from 'ckeditor5';
import MermaidEditing from '../src/mermaidediting.js';
import { afterEach, beforeEach, describe, it } from 'vitest';
import { expect } from 'vitest';
import { vi } from 'vitest';
import { afterEach, beforeEach, describe, it, expect, vi, type MockInstance } from 'vitest';
/* global document */
@@ -12,7 +10,7 @@ describe( 'MermaidEditing', () => {
} );
describe( 'conversion', () => {
let domElement, editor, model;
let domElement: HTMLDivElement, editor: ClassicEditor, model: ClassicEditor['model'];
beforeEach( async () => {
domElement = document.createElement( 'div' );
@@ -154,7 +152,7 @@ describe( 'MermaidEditing', () => {
} );
describe( 'textarea value', () => {
let domTextarea = null;
let domTextarea: HTMLTextAreaElement;
beforeEach( () => {
// Using editor.setData() instead of setModelData helper because of #11365.
@@ -164,8 +162,8 @@ describe( 'MermaidEditing', () => {
'</pre>'
);
const textareaView = editor.editing.view.document.getRoot().getChild( 0 ).getChild( 1 );
domTextarea = editor.editing.view.domConverter.viewToDom( textareaView );
const textareaView = editor.editing.view.document.getRoot()!.getChild( 0 )! as ViewElement;
domTextarea = editor.editing.view.domConverter.viewToDom( textareaView.getChild( 1 )! ) as HTMLTextAreaElement;
} );
it( 'is properly set during the initial conversion', () => {
@@ -175,7 +173,7 @@ describe( 'MermaidEditing', () => {
it( 'is properly updated after model\'s attribute change', () => {
const { model } = editor;
const mermaidModel = model.document.getRoot().getChild( 0 );
const mermaidModel = model.document.getRoot()!.getChild( 0 )!;
model.change( writer => {
writer.setAttribute( 'source', 'abc', mermaidModel );
@@ -187,7 +185,7 @@ describe( 'MermaidEditing', () => {
it( 'doesn\'t loop if model attribute changes to the same value', () => {
const { model } = editor;
const mermaidModel = model.document.getRoot().getChild( 0 );
const mermaidModel = model.document.getRoot()!.getChild( 0 )!;
model.change( writer => {
writer.setAttribute( 'source', 'flowchart TB\nA --> B\nB --> C', mermaidModel );
@@ -198,9 +196,12 @@ describe( 'MermaidEditing', () => {
} );
describe( 'preview div', () => {
let domPreviewContainer, renderMermaidStub;
let domPreviewContainer: HTMLElement;
let renderMermaidStub: MockInstance;
beforeEach( () => {
renderMermaidStub = vi.spyOn( editor.plugins.get( 'MermaidEditing' ) as unknown as MermaidEditing, '_renderMermaid' );
// Using editor.setData() instead of setModelData helper because of #11365.
editor.setData(
'<pre spellcheck="false">' +
@@ -208,42 +209,31 @@ describe( 'MermaidEditing', () => {
'</pre>'
);
const previewContainerView = editor.editing.view.document.getRoot().getChild( 0 ).getChild( 2 );
domPreviewContainer = editor.editing.view.domConverter.viewToDom( previewContainerView );
renderMermaidStub = vi.spyOn( editor.plugins.get( 'MermaidEditing' ), '_renderMermaid' );
const wrapperView = editor.editing.view.document.getRoot()!.getChild( 0 )! as ViewElement;
const previewContainerView = wrapperView.getChild( 2 )!;
domPreviewContainer = editor.editing.view.domConverter.viewToDom( previewContainerView ) as HTMLElement;
} );
afterEach( () => {
vi.clearAllMocks();
} );
it( 'has proper inner text set during the initial conversion', () => {
expect( domPreviewContainer.textContent ).to.equal( 'flowchart TB\nA --> B\nB --> C' );
it( 'calls render with source during the initial conversion', () => {
expect( renderMermaidStub ).toBeCalledWith( domPreviewContainer, 'flowchart TB\nA --> B\nB --> C' );
} );
it( 'has proper inner text set after a model\'s attribute change', () => {
it( 'calls render with updated source after a model\'s attribute change', () => {
const { model } = editor;
const mermaidModel = model.document.getRoot().getChild( 0 );
renderMermaidStub.mockClear();
const mermaidModel = model.document.getRoot()!.getChild( 0 )!;
model.change( writer => {
writer.setAttribute( 'source', 'abc', mermaidModel );
} );
expect( domPreviewContainer.textContent ).to.equal( 'abc' );
} );
it( 'calls mermaid render function after a model\'s attribute change', () => {
const { model } = editor;
const mermaidModel = model.document.getRoot().getChild( 0 );
model.change( writer => {
writer.setAttribute( 'source', 'abc', mermaidModel );
} );
expect(renderMermaidStub).toBeCalledWith(domPreviewContainer);
expect( renderMermaidStub ).toBeCalledWith( domPreviewContainer, 'abc' );
} );
} );
} );
@@ -251,7 +241,7 @@ describe( 'MermaidEditing', () => {
it( 'adds a editing pipeline converter that has a precedence over code block', () => {
setModelData( editor.model, '<mermaid source="foo"></mermaid>' );
const firstViewChild = editor.editing.view.document.getRoot().getChild( 0 );
const firstViewChild = editor.editing.view.document.getRoot()!.getChild( 0 ) as ViewElement;
expect( firstViewChild.name ).to.equal( 'div' );
expect( firstViewChild.hasClass( 'ck-mermaid__wrapper' ), 'has ck-mermaid__wrapper class' ).to.be.true;
@@ -260,7 +250,7 @@ describe( 'MermaidEditing', () => {
it( 'does not convert code blocks other than mermaid language', () => {
setModelData( editor.model, '<codeBlock language="javascript">foo</codeBlock>' );
const firstViewChild = editor.editing.view.document.getRoot().getChild( 0 );
const firstViewChild = editor.editing.view.document.getRoot()!.getChild( 0 ) as ViewElement;
expect( firstViewChild.name ).not.to.equal( 'div' );
expect( firstViewChild.hasClass( 'ck-mermaid__wrapper' ), 'has ck-mermaid__wrapper class' ).to.be.false;
@@ -269,7 +259,8 @@ describe( 'MermaidEditing', () => {
it( 'adds a preview element', () => {
setModelData( editor.model, '<mermaid source="foo"></mermaid>' );
const widgetChildren = [ ...editor.editing.view.document.getRoot().getChild( 0 ).getChildren() ];
const widget = editor.editing.view.document.getRoot()!.getChild( 0 ) as ViewElement;
const widgetChildren = [ ...widget.getChildren() ] as ViewElement[];
const previewView = widgetChildren.filter( item => item.name === 'div' && item.hasClass( 'ck-mermaid__preview' ) );
expect( previewView.length ).to.equal( 1 );
@@ -278,7 +269,8 @@ describe( 'MermaidEditing', () => {
it( 'adds an editing element', () => {
setModelData( editor.model, '<mermaid source="foo"></mermaid>' );
const widgetChildren = [ ...editor.editing.view.document.getRoot().getChild( 0 ).getChildren() ];
const widget = editor.editing.view.document.getRoot()!.getChild( 0 ) as ViewElement;
const widgetChildren = [ ...widget.getChildren() ] as ViewElement[];
const previewView = widgetChildren.filter(
item => item.name === 'textarea' && item.hasClass( 'ck-mermaid__editing-view' )
);

View File

@@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"rootDir": "./src",
"composite": false,
"types": [
"../typings/types.d.ts"
]
}
}

View File

@@ -1,12 +0,0 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"declaration": true
},
"exclude": [
"./tests/",
"./src",
"./sample/"
]
}

View File

@@ -14,9 +14,5 @@
"@triliumnext/ckeditor5-mermaid": "workspace:*",
"ckeditor5": "48.0.0",
"ckeditor5-premium-features": "48.0.0"
},
"devDependencies": {
"@smithy/middleware-retry": "4.4.46",
"@types/jquery": "4.0.0"
}
}

View File

@@ -25,6 +25,11 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, LocaleMapping | null> = {
coreTranslation: () => import("ckeditor5/translations/zh-cn.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/zh-cn.js"),
},
cs: {
languageCode: "cs",
coreTranslation: () => import("ckeditor5/translations/cs.js"),
premiumFeaturesTranslation: () => import("ckeditor5-premium-features/translations/cs.js"),
},
de: {
languageCode: "de",
coreTranslation: () => import("ckeditor5/translations/de.js"),

View File

@@ -18,7 +18,7 @@
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.6.0",
"@codemirror/state": "6.6.0",
"@codemirror/view": "6.40.0",
"@codemirror/view": "6.41.0",
"@fsegurai/codemirror-theme-abcdef": "6.2.3",
"@fsegurai/codemirror-theme-abyss": "6.2.3",
"@fsegurai/codemirror-theme-android-studio": "6.2.3",
@@ -52,6 +52,8 @@
"codemirror-lang-elixir": "4.0.1",
"codemirror-lang-hcl": "0.1.0",
"codemirror-lang-mermaid": "0.5.0",
"eslint-linter-browserify": "10.1.0"
"@eslint/js": "10.0.1",
"eslint-linter-browserify": "10.1.0",
"globals": "17.4.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/commons",
"version": "0.102.1",
"version": "0.102.2",
"description": "Shared library between the clients (e.g. browser, Electron) and the server, mostly for type definitions and utility methods.",
"private": true,
"type": "module",
@@ -16,7 +16,6 @@
},
"dependencies": {
"dayjs": "1.11.20",
"dayjs-plugin-utc": "0.1.2",
"marked": "17.0.5"
}
}

View File

@@ -81,6 +81,7 @@ export default [
{ type: "label", name: "webViewSrc", isDangerous: true },
{ type: "label", name: "hideHighlightWidget" },
{ type: "label", name: "iconPack", isDangerous: true },
{ type: "label", name: "docName", isDangerous: true },
{ type: "label", name: "printLandscape" },
{ type: "label", name: "printPageSize" },

View File

@@ -34,6 +34,7 @@ dayjs.extend(utc);
export const DAYJS_LOADER: Record<LOCALE_IDS, () => Promise<typeof import("dayjs/locale/en.js")>> = {
"ar": () => import("dayjs/locale/ar.js"),
"cn": () => import("dayjs/locale/zh-cn.js"),
"cs": () => import("dayjs/locale/cs.js"),
"de": () => import("dayjs/locale/de.js"),
"en": () => import("dayjs/locale/en.js"),
"en-GB": () => import("dayjs/locale/en-gb.js"),

View File

@@ -10,12 +10,13 @@ export interface Locale {
/** The value to pass to `--lang` for the Electron instance in order to set it as a locale. Not setting it will hide it from the list of supported locales. */
electronLocale?: "en" | "de" | "es" | "fr" | "zh_CN" | "zh_TW" | "ro" | "af" | "am" | "ar" | "bg" | "bn" | "ca" | "cs" | "da" | "el" | "en_GB" | "es_419" | "et" | "fa" | "fi" | "fil" | "gu" | "he" | "hi" | "hr" | "hu" | "id" | "it" | "ja" | "kn" | "ko" | "lt" | "lv" | "ml" | "mr" | "ms" | "nb" | "nl" | "pl" | "pt_BR" | "pt_PT" | "ru" | "sk" | "sl" | "sr" | "sv" | "sw" | "ta" | "te" | "th" | "tr" | "uk" | "ur" | "vi";
/** The Tesseract OCR language code for this locale (e.g. "eng", "fra", "deu"). See https://tesseract-ocr.github.io/tessdoc/Data-Files-in-different-versions.html */
tesseractCode?: "eng" | "deu" | "spa" | "fra" | "gle" | "ita" | "hin" | "jpn" | "por" | "pol" | "ron" | "rus" | "chi_sim" | "chi_tra" | "ukr" | "ara" | "heb" | "kur" | "fas" | "kor";
tesseractCode?: "eng" | "deu" | "spa" | "fra" | "gle" | "ita" | "hin" | "jpn" | "por" | "pol" | "ron" | "rus" | "chi_sim" | "chi_tra" | "ukr" | "ara" | "heb" | "kur" | "fas" | "kor" | "ces";
}
// When adding a new locale, prefer the version with hyphen instead of underscore.
const UNSORTED_LOCALES = [
{ id: "cn", name: "简体中文", electronLocale: "zh_CN", tesseractCode: "chi_sim" },
{ id: "cs", name: "Čeština", electronLocale: "cs", tesseractCode: "ces" },
{ id: "de", name: "Deutsch", electronLocale: "de", tesseractCode: "deu" },
{ id: "en", name: "English (United States)", electronLocale: "en", tesseractCode: "eng" },
{ id: "en-GB", name: "English (United Kingdom)", electronLocale: "en_GB", tesseractCode: "eng" },

View File

@@ -295,6 +295,20 @@ export interface TextRepresentationResponse {
message?: string;
}
export interface OCRProcessResponse {
success: boolean;
message?: string;
result?: {
text: string;
confidence: number;
extractedAt: string;
language?: string;
pageCount?: number;
};
/** The minimum confidence threshold that was applied (0-1 scale). */
minConfidence?: number;
}
export interface IconRegistry {
sources: {
prefix: string;

View File

@@ -11,8 +11,5 @@
"206",
"stream",
"typescript"
],
"dependencies": {
"tslib": "2.8.1"
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "@triliumnext/pdfjs-viewer",
"version": "0.102.1",
"version": "0.102.2",
"private": true,
"scripts": {
"build": "tsx scripts/build.ts",

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