Merge branch 'develop' into feat/note-edit-readonly-fix2

This commit is contained in:
Jon Fuller 2025-06-01 18:51:28 -07:00 committed by GitHub
commit c28edb674c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
601 changed files with 15176 additions and 13798 deletions

View File

@ -8,6 +8,9 @@ indent_style = space
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.sh]
end_of_line = lf
[{server,translation}.json] [{server,translation}.json]
charset = utf-8 charset = utf-8
end_of_line = lf end_of_line = lf

12
.gitattributes vendored
View File

@ -1,17 +1,21 @@
# Mark files as auto-generated to simplify reviews.
package-lock.json linguist-generated=true package-lock.json linguist-generated=true
**/package-lock.json linguist-generated=true **/package-lock.json linguist-generated=true
apps/server/src/assets/doc_notes/en/User[[:space:]]Guide/** linguist-generated
apps/server/src/assets/doc_notes/en/User[[:space:]]Guide/** linguist-generated=true # Ignore from GitHub language stats.
apps/server/src/assets/doc_notes/en/User[[:space:]]Guide/**/*.html eol=lf apps/server/src/assets/doc_notes/en/User[[:space:]]Guide/**/*.html eol=lf
apps/server/src/assets/doc_notes/** linguist-vendored=true
apps/edit-docs/demo/** linguist-vendored=true
docs/** linguist-vendored=true
# Normalize line endings.
docs/**/*.md eol=lf docs/**/*.md eol=lf
docs/**/*.json eol=lf docs/**/*.json eol=lf
demo/**/*.html eol=lf demo/**/*.html eol=lf
demo/**/*.json eol=lf demo/**/*.json eol=lf
demo/**/*.svg eol=lf demo/**/*.svg eol=lf
demo/**/*.txt eol=lf demo/**/*.txt eol=lf
demo/**/*.js eol=lf demo/**/*.js eol=lf
demo/**/*.css eol=lf demo/**/*.css eol=lf
*.sh eol=lf
apps/client/src/libraries/** linguist-vendored

View File

@ -39,7 +39,7 @@ jobs:
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4
- name: Check affected - name: Check affected
run: pnpm nx affected -t build rebuild-deps run: pnpm nx affected --verbose -t typecheck build rebuild-deps
report-electron-size: report-electron-size:
name: Report Electron size name: Report Electron size
@ -128,7 +128,7 @@ jobs:
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- name: Run the unit tests - name: Run the unit tests
run: pnpm run test run: pnpm run test:all
build_docker: build_docker:
name: Build Docker image name: Build Docker image

View File

@ -53,7 +53,7 @@ jobs:
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Install Playwright Browsers - name: Install Playwright Browsers
run: npx playwright install --with-deps run: pnpx playwright install --with-deps
- name: Run the TypeScript build - name: Run the TypeScript build
run: pnpm run server:build run: pnpm run server:build
@ -62,7 +62,7 @@ jobs:
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: apps/server context: apps/server
file: ${{ matrix.dockerfile }} file: apps/server/${{ matrix.dockerfile }}
load: true load: true
tags: ${{ env.TEST_TAG }} tags: ${{ env.TEST_TAG }}
cache-from: type=gha cache-from: type=gha
@ -70,7 +70,7 @@ jobs:
- name: Validate container run output - name: Validate container run output
run: | run: |
CONTAINER_ID=$(docker run -d --log-driver=journald --rm --network=host -e TRILIUM_PORT=8082 --volume ./integration-tests/db:/home/node/trilium-data --name trilium_local ${{ env.TEST_TAG }}) CONTAINER_ID=$(docker run -d --log-driver=journald --rm --network=host -e TRILIUM_PORT=8082 --volume ./apps/server/spec/db:/home/node/trilium-data --name trilium_local ${{ env.TEST_TAG }})
echo "Container ID: $CONTAINER_ID" echo "Container ID: $CONTAINER_ID"
- name: Wait for the healthchecks to pass - name: Wait for the healthchecks to pass
@ -82,7 +82,7 @@ jobs:
require-healthy: true require-healthy: true
- name: Run Playwright tests - name: Run Playwright tests
run: TRILIUM_DOCKER=1 npx playwright test run: TRILIUM_DOCKER=1 TRILIUM_PORT=8082 pnpx nx run server-e2e:e2e
- uses: actions/upload-artifact@v4 - uses: actions/upload-artifact@v4
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
with: with:
@ -129,7 +129,6 @@ jobs:
- name: Set TEST_TAG to lowercase - name: Set TEST_TAG to lowercase
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- uses: pnpm/action-setup@v4 - uses: pnpm/action-setup@v4
@ -142,6 +141,9 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Run the TypeScript build
run: pnpm run server:build
- name: Update build info - name: Update build info
run: pnpm run chore:update-build-info run: pnpm run chore:update-build-info
@ -184,7 +186,7 @@ jobs:
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
context: apps/server context: apps/server
file: ${{ matrix.dockerfile }} file: apps/server/${{ matrix.dockerfile }}
platforms: ${{ matrix.platform }} platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true outputs: type=image,name=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true

View File

@ -33,11 +33,11 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- run: npx playwright install --with-deps - run: pnpx playwright install --with-deps
- uses: nrwl/nx-set-shas@v4 - uses: nrwl/nx-set-shas@v4
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
# - run: npx nx-cloud record -- echo Hello World # - run: npx nx-cloud record -- echo Hello World
# Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected
# When you enable task distribution, run the e2e-ci task instead of e2e # When you enable task distribution, run the e2e-ci task instead of e2e
- run: npx nx affected -t e2e - run: pnpx nx affected -t e2e

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
22.16.0

View File

@ -1,2 +1,7 @@
_regroup _regroup
_regroup_monorepo _regroup_monorepo
# Asset copying respects .gitignore / .nxignore for some reason.
# See https://github.com/nrwl/nx/issues/20309
!dist
!node_modules

View File

@ -24,5 +24,9 @@
}, },
"github-actions.workflows.pinned.workflows": [ "github-actions.workflows.pinned.workflows": [
".github/workflows/nightly.yml" ".github/workflows/nightly.yml"
] ],
"typescript.validate.enable": true,
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.enablePromptUseWorkspaceTsdk": true
} }

141
README.md
View File

@ -4,15 +4,47 @@
[English](./README.md) | [Chinese](./docs/README-ZH_CN.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md) [English](./README.md) | [Chinese](./docs/README-ZH_CN.md) | [Russian](./docs/README.ru.md) | [Japanese](./docs/README.ja.md) | [Italian](./docs/README.it.md) | [Spanish](./docs/README.es.md)
TriliumNext Notes is an open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases. TriliumNext Notes is a free and open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases.
See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for quick overview: See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for quick overview:
<a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a> <a href="https://triliumnext.github.io/Docs/Wiki/screenshot-tour"><img src="./docs/app.png" alt="Trilium Screenshot" width="1000"></a>
## 🎁 Features
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes))
* Rich WYSIWYG note editor including e.g. tables, images and [math](https://triliumnext.github.io/Docs/Wiki/text-notes) with markdown [autoformat](https://triliumnext.github.io/Docs/Wiki/text-notes#autoformat)
* Support for editing [notes with source code](https://triliumnext.github.io/Docs/Wiki/code-notes), including syntax highlighting
* Fast and easy [navigation between notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text search and [note hoisting](https://triliumnext.github.io/Docs/Wiki/note-hoisting)
* Seamless [note versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions)
* Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be used for note organization, querying and advanced [scripting](https://triliumnext.github.io/Docs/Wiki/scripts)
* UI available in English, German, Spanish, French, Romanian, and Chinese (simplified and traditional)
* Direct [OpenID and TOTP integration](.docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Server%20Installation/Multi-Factor%20Authentication.md") for more secure login
* [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization) with self-hosted sync server
* there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting)
* [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes to public internet
* Strong [note encryption](https://triliumnext.github.io/Docs/Wiki/protected-notes) with per-note granularity
* Sketching diagrams, based on [Excalidraw](https://excalidraw.com/) (note type "canvas")
* [Relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map) and [link maps](https://triliumnext.github.io/Docs/Wiki/link-map) for visualizing notes and their relations
* Mind maps, based on [Mind Elixir](https://docs.mind-elixir.com/)
* [Geo maps](./docs/User%20Guide/User%20Guide/Note%20Types/Geo%20Map.md) with location pins and GPX tracks
* [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - see [Advanced showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases)
* [REST API](https://triliumnext.github.io/Docs/Wiki/etapi) for automation
* Scales well in both usability and performance upwards of 100 000 notes
* Touch optimized [mobile frontend](https://triliumnext.github.io/Docs/Wiki/mobile-frontend) for smartphones and tablets
* Built-in [dark theme](https://triliumnext.github.io/Docs/Wiki/themes), support for user themes
* [Evernote](https://triliumnext.github.io/Docs/Wiki/evernote-import) and [Markdown import & export](https://triliumnext.github.io/Docs/Wiki/markdown)
* [Web Clipper](https://triliumnext.github.io/Docs/Wiki/web-clipper) for easy saving of web content
* Customizable UI (sidebar buttons, user-defined widgets, ...)
✨ Check out the following third-party resources/communities for more TriliumNext related goodies:
- [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.
## ⚠️ Why TriliumNext? ## ⚠️ Why TriliumNext?
[The original Trilium project is in maintenance mode](https://github.com/zadam/trilium/issues/4620) [The original Trilium project is in maintenance mode](https://github.com/zadam/trilium/issues/4620).
### Migrating from Trilium? ### Migrating from Trilium?
@ -20,53 +52,49 @@ There are no special migration steps to migrate from a zadam/Trilium instance to
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/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 have their sync versions incremented. Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/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 have their sync versions incremented.
## 📖 Documentation
We're currently in the progress of moving the documentation to in-app (hit the `F1` key within Trilium). As a result, there may be some missing parts until we've completed the migration. If you'd prefer to navigate through the documentation within GitHub, you can navigate the [User Guide](./docs/User%20Guide/User%20Guide/) documentation.
Below are some quick links for your convenience to navigate the documentation:
- [Server installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation.md)
- [Docker installation](./docs/User%20Guide/User%20Guide/Installation%20&%20Setup/Server%20Installation/1.%20Installing%20the%20server/Using%20Docker.md)
- [Upgrading TriliumNext](./docs/User%20Guide/User%20Guide/Installation%20%26%20Setup/Upgrading%20TriliumNext.md)
- [Concepts and Features - Note](./docs/User%20Guide/User%20Guide/Basic%20Concepts%20and%20Features/Notes.md)
- [Patterns of personal knowledge base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge)
Until we finish reorganizing the documentation, you may also want to [browse the old documentation](https://triliumnext.github.io/Docs).
## 💬 Discuss with us ## 💬 Discuss with us
Feel free to join our official conversations. We would love to hear what features, suggestions, or issues you may have! Feel free to join our official conversations. We would love to hear what features, suggestions, or issues you may have!
- [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous discussions) - [Matrix](https://matrix.to/#/#triliumnext:matrix.org) (For synchronous discussions.)
- The `General` Matrix room is also bridged to [XMPP](xmpp:discuss@trilium.thisgreat.party?join) - The `General` Matrix room is also bridged to [XMPP](xmpp:discuss@trilium.thisgreat.party?join)
- [Github Discussions](https://github.com/TriliumNext/Notes/discussions) (For Asynchronous discussions) - [Github Discussions](https://github.com/TriliumNext/Notes/discussions) (For asynchronous discussions.)
- [Wiki](https://triliumnext.github.io/Docs/) (For common how-to questions and user guides) - [Github Issues](https://github.com/TriliumNext/Notes/issues) (For bug reports and feature requests.)
## 🎁 Features
* Notes can be arranged into arbitrarily deep tree. Single note can be placed into multiple places in the tree (see [cloning](https://triliumnext.github.io/Docs/Wiki/cloning-notes))
* Rich WYSIWYG note editing including e.g. tables, images and [math](https://triliumnext.github.io/Docs/Wiki/text-notes) with markdown [autoformat](https://triliumnext.github.io/Docs/Wiki/text-notes#autoformat)
* Support for editing [notes with source code](https://triliumnext.github.io/Docs/Wiki/code-notes), including syntax highlighting
* Fast and easy [navigation between notes](https://triliumnext.github.io/Docs/Wiki/note-navigation), full text search and [note hoisting](https://triliumnext.github.io/Docs/Wiki/note-hoisting)
* Seamless [note versioning](https://triliumnext.github.io/Docs/Wiki/note-revisions)
* Note [attributes](https://triliumnext.github.io/Docs/Wiki/attributes) can be used for note organization, querying and advanced [scripting](https://triliumnext.github.io/Docs/Wiki/scripts)
* Direct OpenID and TOTP integration for more secure login
* [Synchronization](https://triliumnext.github.io/Docs/Wiki/synchronization) with self-hosted sync server
* there's a [3rd party service for hosting synchronisation server](https://trilium.cc/paid-hosting)
* [Sharing](https://triliumnext.github.io/Docs/Wiki/sharing) (publishing) notes to public internet
* Strong [note encryption](https://triliumnext.github.io/Docs/Wiki/protected-notes) with per-note granularity
* Sketching diagrams with built-in Excalidraw (note type "canvas")
* [Relation maps](https://triliumnext.github.io/Docs/Wiki/relation-map) and [link maps](https://triliumnext.github.io/Docs/Wiki/link-map) for visualizing notes and their relations
* [Scripting](https://triliumnext.github.io/Docs/Wiki/scripts) - see [Advanced showcases](https://triliumnext.github.io/Docs/Wiki/advanced-showcases)
* [REST API](https://triliumnext.github.io/Docs/Wiki/etapi) for automation
* Scales well in both usability and performance upwards of 100 000 notes
* Touch optimized [mobile frontend](https://triliumnext.github.io/Docs/Wiki/mobile-frontend) for smartphones and tablets
* [Night theme](https://triliumnext.github.io/Docs/Wiki/themes)
* [Evernote](https://triliumnext.github.io/Docs/Wiki/evernote-import) and [Markdown import & export](https://triliumnext.github.io/Docs/Wiki/markdown)
* [Web Clipper](https://triliumnext.github.io/Docs/Wiki/web-clipper) for easy saving of web content
✨ Check out the following third-party resources/communities for more TriliumNext related goodies:
- [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.
## 🏗 Installation ## 🏗 Installation
### Desktop ### Windows / MacOS
To use TriliumNext on your desktop machine (Linux, MacOS, and Windows) you have a few options: Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the `trilium` executable.
* Download the binary release for your platform from the [latest release page](https://github.com/TriliumNext/Notes/releases/latest), unzip the package and run the ```trilium``` executable. ### Linux
* Access TriliumNext via the web interface of a server installation (see below)
* Currently only the latest versions of Chrome & Firefox are supported (and tested). If your distribution is listed in the table below, use your distribution's package.
* TriliumNext is also provided as a Flatpak, but not yet published on FlatHub.
[![Packaging status](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/Notes/releases/latest), unzip the package and run the `trilium` executable.
TriliumNext is also provided as a Flatpak, but not yet published on FlatHub.
### Browser (any OS)
If you use a server installation (see below), you can directly access the web interface (which is almost identical to the desktop app).
Currently only the latest versions of Chrome & Firefox are supported (and tested).
### Mobile ### Mobile
@ -80,28 +108,43 @@ See issue https://github.com/TriliumNext/Notes/issues/72 for more information on
To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/notes)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation). To install TriliumNext on your own server (including via Docker from [Dockerhub](https://hub.docker.com/r/triliumnext/notes)) follow [the server installation docs](https://triliumnext.github.io/Docs/Wiki/server-installation).
## 📝 Documentation
[See wiki for complete list of documentation pages.](https://triliumnext.github.io/Docs)
You can also read [Patterns of personal knowledge base](https://triliumnext.github.io/Docs/Wiki/patterns-of-personal-knowledge) to get some inspiration on how you might use TriliumNext.
## 💻 Contribute ## 💻 Contribute
### Code ### Code
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
```shell ```shell
git clone https://github.com/TriliumNext/Notes.git git clone https://github.com/TriliumNext/Notes.git
cd Notes cd Notes
npm install pnpm install
npm run server:start pnpm run server:start
```
### Documentation
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
```shell
git clone https://github.com/TriliumNext/Notes.git
cd Notes
pnpm install
pnpm nx run edit-docs:edit-docs
```
### Building the Executable
Download the repository, install dependencies using `pnpm` and then build the desktop app for Windows:
```shell
git clone https://github.com/TriliumNext/Notes.git
cd Notes
pnpm install
pnpm nx --project=desktop electron-forge:make -- --arch=x64 --platform=win32
``` ```
For more details, see the [development docs](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Building%20and%20deployment/Running%20a%20development%20build.md). For more details, see the [development docs](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Building%20and%20deployment/Running%20a%20development%20build.md).
### Documentation ### Developer Documentation
See the [documentation guide](https://github.com/TriliumNext/Notes/blob/develop/docs/Developer%20Guide/Developer%20Guide/Documentation.md) for details. Please view the [documentation guide](./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.
## 👏 Shoutouts ## 👏 Shoutouts
@ -119,4 +162,6 @@ Support for the TriliumNext organization will be possible in the near future. Fo
## 🔑 License ## 🔑 License
Copyright 2017-2025 zadam, Elian Doran, and other contributors
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. 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.

View File

@ -1,548 +0,0 @@
/*
* CKEditor 5 (v41.0.0) content styles.
* Generated on Fri, 26 Jan 2024 10:23:49 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html
*/
:root {
--ck-color-image-caption-background: hsl(0, 0%, 97%);
--ck-color-image-caption-text: hsl(0, 0%, 20%);
--ck-color-mention-background: hsla(341, 100%, 30%, 0.1);
--ck-color-mention-text: hsl(341, 100%, 30%);
--ck-color-selector-caption-background: hsl(0, 0%, 97%);
--ck-color-selector-caption-text: hsl(0, 0%, 20%);
--ck-highlight-marker-blue: hsl(201, 97%, 72%);
--ck-highlight-marker-green: hsl(120, 93%, 68%);
--ck-highlight-marker-pink: hsl(345, 96%, 73%);
--ck-highlight-marker-yellow: hsl(60, 97%, 73%);
--ck-highlight-pen-green: hsl(112, 100%, 27%);
--ck-highlight-pen-red: hsl(0, 85%, 49%);
--ck-image-style-spacing: 1.5em;
--ck-inline-image-style-spacing: calc(var(--ck-image-style-spacing) / 2);
--ck-todo-list-checkmark-size: 16px;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table .ck-table-resized {
table-layout: fixed;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table table {
overflow: hidden;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table td,
.ck-content .table th {
overflow-wrap: break-word;
position: relative;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table {
margin: 0.9em auto;
display: table;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
border: 1px double hsl(0, 0%, 70%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table table td,
.ck-content .table table th {
min-width: 2em;
padding: .4em;
border: 1px solid hsl(0, 0%, 75%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table table th {
font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content[dir="rtl"] .table th {
text-align: right;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content[dir="ltr"] .table th {
text-align: left;
}
/* @ckeditor/ckeditor5-table/theme/tablecaption.css */
.ck-content .table > figcaption {
display: table-caption;
caption-side: top;
word-break: break-word;
text-align: center;
color: var(--ck-color-selector-caption-text);
background-color: var(--ck-color-selector-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
position: relative;
clear: both;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block;
text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: hsl(0, 0%, 100%);
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* @ckeditor/ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 0.9em 0;
display: block;
min-width: 15em;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list {
list-style: none;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list li {
position: relative;
margin-bottom: 5px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list li .todo-list {
margin-top: 5px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input {
-webkit-appearance: none;
display: inline-block;
position: relative;
width: var(--ck-todo-list-checkmark-size);
height: var(--ck-todo-list-checkmark-size);
vertical-align: middle;
border: 0;
left: -25px;
margin-right: -15px;
right: 0;
margin-left: 0;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content[dir=rtl] .todo-list .todo-list__label > input {
left: 0;
margin-right: 0;
right: -25px;
margin-left: -15px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input::before {
display: block;
position: absolute;
box-sizing: border-box;
content: '';
width: 100%;
height: 100%;
border: 1px solid hsl(0, 0%, 20%);
border-radius: 2px;
transition: 250ms ease-in-out box-shadow;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input::after {
display: block;
position: absolute;
box-sizing: content-box;
pointer-events: none;
content: '';
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
border-style: solid;
border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
transform: rotate(45deg);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input[checked]::before {
background: hsl(126, 64%, 41%);
border-color: hsl(126, 64%, 41%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input[checked]::after {
border-color: hsl(0, 0%, 100%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] {
position: absolute;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > input,
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input {
cursor: pointer;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > input:hover::before, .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input:hover::before {
box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input {
-webkit-appearance: none;
display: inline-block;
position: relative;
width: var(--ck-todo-list-checkmark-size);
height: var(--ck-todo-list-checkmark-size);
vertical-align: middle;
border: 0;
left: -25px;
margin-right: -15px;
right: 0;
margin-left: 0;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content[dir=rtl] .todo-list .todo-list__label > span[contenteditable=false] > input {
left: 0;
margin-right: 0;
right: -25px;
margin-left: -15px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::before {
display: block;
position: absolute;
box-sizing: border-box;
content: '';
width: 100%;
height: 100%;
border: 1px solid hsl(0, 0%, 20%);
border-radius: 2px;
transition: 250ms ease-in-out box-shadow;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::after {
display: block;
position: absolute;
box-sizing: content-box;
pointer-events: none;
content: '';
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
border-style: solid;
border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
transform: rotate(45deg);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::before {
background: hsl(126, 64%, 41%);
border-color: hsl(126, 64%, 41%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::after {
border-color: hsl(0, 0%, 100%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] {
position: absolute;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol {
list-style-type: decimal;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol {
list-style-type: lower-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol {
list-style-type: lower-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol {
list-style-type: upper-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol ol {
list-style-type: upper-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul {
list-style-type: disc;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul {
list-style-type: circle;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image {
display: table;
clear: both;
text-align: center;
margin: 0.9em auto;
min-width: 50px;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 100%;
height: auto;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline {
/*
* Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).;
* Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root.
* This strange behavior does not happen with inline-flex.
*/
display: inline-flex;
max-width: 100%;
align-items: flex-start;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline picture {
display: flex;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline picture,
.ck-content .image-inline img {
flex-grow: 1;
flex-shrink: 1;
max-width: 100%;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content img.image_resized {
height: auto;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized img {
width: 100%;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized > figcaption {
display: block;
}
/* @ckeditor/ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: var(--ck-color-image-caption-text);
background-color: var(--ck-color-image-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left,
.ck-content .image-style-block-align-right {
max-width: calc(100% - var(--ck-image-style-spacing));
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left,
.ck-content .image-style-align-right {
clear: none;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side {
float: right;
margin-left: var(--ck-image-style-spacing);
max-width: 50%;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left {
float: left;
margin-right: var(--ck-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-center {
margin-left: auto;
margin-right: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-right {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-right {
margin-right: 0;
margin-left: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left {
margin-left: 0;
margin-right: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content p + .image-style-align-left,
.ck-content p + .image-style-align-right,
.ck-content p + .image-style-side {
margin-top: 0;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left,
.ck-content .image-inline.image-style-align-right {
margin-top: var(--ck-inline-image-style-spacing);
margin-bottom: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left {
margin-right: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-right {
margin-left: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */
.ck-content blockquote {
overflow: hidden;
padding-right: 1.5em;
padding-left: 1.5em;
margin-left: 0;
margin-right: 0;
font-style: italic;
border-left: solid 5px hsl(0, 0%, 80%);
}
/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* @ckeditor/ckeditor5-basic-styles/theme/code.css */
.ck-content code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-tiny {
font-size: .7em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-small {
font-size: .85em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-big {
font-size: 1.4em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-huge {
font-size: 1.8em;
}
/* @ckeditor/ckeditor5-mention/theme/mention.css */
.ck-content .mention {
background: var(--ck-color-mention-background);
color: var(--ck-color-mention-text);
}
/* @ckeditor/ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr {
margin: 15px 0;
height: 4px;
background: hsl(0, 0%, 87%);
border: 0;
}
/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */
.ck-content pre {
padding: 1em;
text-align: left;
direction: ltr;
tab-size: 4;
white-space: pre-wrap;
font-style: normal;
min-width: 200px;
border: 0px;
border-radius: 6px;
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2);
}
.ck-content pre:not(.hljs) {
color: hsl(0, 0%, 20.8%);
background: hsla(0, 0%, 78%, 0.3);
}
/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */
.ck-content pre code {
background: unset;
padding: 0;
border-radius: 0;
}
@media print {
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
padding: 0;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
display: none;
}
}

View File

@ -1,593 +0,0 @@
/* !!!!!! TRILIUM CUSTOM CHANGES !!!!!! */
.printed-content .ck-widget__selection-handle, .printed-content .ck-widget__type-around { /* gets rid of triangles: https://github.com/zadam/trilium/issues/1129 */
display: none;
}
.page-break {
page-break-after: always;
}
.printed-content .page-break:after,
.printed-content .page-break > * {
display: none !important;
}
.ck-content li p {
margin: 0 !important;
}
.admonition {
--accent-color: var(--card-border-color);
border: 1px solid var(--accent-color);
box-shadow: var(--card-box-shadow);
background: var(--card-background-color);
border-radius: 0.5em;
padding: 1em;
margin: 1.25em 0;
position: relative;
overflow: hidden;
}
.admonition p:last-child {
margin-bottom: 0;
}
.admonition p, h2 {
margin-top: 0;
}
.admonition.note { --accent-color: #69c7ff; }
.admonition.tip { --accent-color: #40c025; }
.admonition.important { --accent-color: #9839f7; }
.admonition.caution { --accent-color: #ff2e2e; }
.admonition.warning { --accent-color: #e2aa03; }
/*
* CKEditor 5 (v41.0.0) content styles.
* Generated on Fri, 26 Jan 2024 10:23:49 GMT.
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html
*/
:root {
--ck-color-image-caption-background: hsl(0, 0%, 97%);
--ck-color-image-caption-text: hsl(0, 0%, 20%);
--ck-color-mention-background: hsla(341, 100%, 30%, 0.1);
--ck-color-mention-text: hsl(341, 100%, 30%);
--ck-color-selector-caption-background: hsl(0, 0%, 97%);
--ck-color-selector-caption-text: hsl(0, 0%, 20%);
--ck-highlight-marker-blue: hsl(201, 97%, 72%);
--ck-highlight-marker-green: hsl(120, 93%, 68%);
--ck-highlight-marker-pink: hsl(345, 96%, 73%);
--ck-highlight-marker-yellow: hsl(60, 97%, 73%);
--ck-highlight-pen-green: hsl(112, 100%, 27%);
--ck-highlight-pen-red: hsl(0, 85%, 49%);
--ck-image-style-spacing: 1.5em;
--ck-inline-image-style-spacing: calc(var(--ck-image-style-spacing) / 2);
--ck-todo-list-checkmark-size: 16px;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table .ck-table-resized {
table-layout: fixed;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table table {
overflow: hidden;
}
/* @ckeditor/ckeditor5-table/theme/tablecolumnresize.css */
.ck-content .table td,
.ck-content .table th {
overflow-wrap: break-word;
position: relative;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table {
margin: 0.9em auto;
display: table;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
height: 100%;
border: 1px double hsl(0, 0%, 70%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table table td,
.ck-content .table table th {
min-width: 2em;
padding: .4em;
border: 1px solid hsl(0, 0%, 75%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content .table table th {
font-weight: bold;
background: hsla(0, 0%, 0%, 5%);
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content[dir="rtl"] .table th {
text-align: right;
}
/* @ckeditor/ckeditor5-table/theme/table.css */
.ck-content[dir="ltr"] .table th {
text-align: left;
}
/* @ckeditor/ckeditor5-table/theme/tablecaption.css */
.ck-content .table > figcaption {
display: table-caption;
caption-side: top;
word-break: break-word;
text-align: center;
color: var(--ck-color-selector-caption-text);
background-color: var(--ck-color-selector-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
position: relative;
clear: both;
padding: 5px 0;
display: flex;
align-items: center;
justify-content: center;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
content: '';
position: absolute;
border-bottom: 2px dashed hsl(0, 0%, 77%);
width: 100%;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break__label {
position: relative;
z-index: 1;
padding: .3em .6em;
display: block;
text-transform: uppercase;
border: 1px solid hsl(0, 0%, 77%);
border-radius: 2px;
font-family: Helvetica, Arial, Tahoma, Verdana, Sans-Serif;
font-size: 0.75em;
font-weight: bold;
color: hsl(0, 0%, 20%);
background: hsl(0, 0%, 100%);
box-shadow: 2px 2px 1px hsla(0, 0%, 0%, 0.15);
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
/* @ckeditor/ckeditor5-media-embed/theme/mediaembed.css */
.ck-content .media {
clear: both;
margin: 0.9em 0;
display: block;
min-width: 15em;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list {
list-style: none;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list li {
position: relative;
margin-bottom: 5px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list li .todo-list {
margin-top: 5px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input {
-webkit-appearance: none;
display: inline-block;
position: relative;
width: var(--ck-todo-list-checkmark-size);
height: var(--ck-todo-list-checkmark-size);
vertical-align: middle;
border: 0;
left: -25px;
margin-right: -15px;
right: 0;
margin-left: 0;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content[dir=rtl] .todo-list .todo-list__label > input {
left: 0;
margin-right: 0;
right: -25px;
margin-left: -15px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input::before {
display: block;
position: absolute;
box-sizing: border-box;
content: '';
width: 100%;
height: 100%;
border: 1px solid hsl(0, 0%, 20%);
border-radius: 2px;
transition: 250ms ease-in-out box-shadow;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input::after {
display: block;
position: absolute;
box-sizing: content-box;
pointer-events: none;
content: '';
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
border-style: solid;
border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
transform: rotate(45deg);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input[checked]::before {
background: hsl(126, 64%, 41%);
border-color: hsl(126, 64%, 41%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label > input[checked]::after {
border-color: hsl(0, 0%, 100%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label .todo-list__label__description {
vertical-align: middle;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] {
position: absolute;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > input,
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input {
cursor: pointer;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > input:hover::before, .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input:hover::before {
box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input {
-webkit-appearance: none;
display: inline-block;
position: relative;
width: var(--ck-todo-list-checkmark-size);
height: var(--ck-todo-list-checkmark-size);
vertical-align: middle;
border: 0;
left: -25px;
margin-right: -15px;
right: 0;
margin-left: 0;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content[dir=rtl] .todo-list .todo-list__label > span[contenteditable=false] > input {
left: 0;
margin-right: 0;
right: -25px;
margin-left: -15px;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::before {
display: block;
position: absolute;
box-sizing: border-box;
content: '';
width: 100%;
height: 100%;
border: 1px solid hsl(0, 0%, 20%);
border-radius: 2px;
transition: 250ms ease-in-out box-shadow;
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::after {
display: block;
position: absolute;
box-sizing: content-box;
pointer-events: none;
content: '';
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
border-style: solid;
border-color: transparent;
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
transform: rotate(45deg);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::before {
background: hsl(126, 64%, 41%);
border-color: hsl(126, 64%, 41%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::after {
border-color: hsl(0, 0%, 100%);
}
/* @ckeditor/ckeditor5-list/theme/todolist.css */
.ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] {
position: absolute;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol {
list-style-type: decimal;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol {
list-style-type: lower-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol {
list-style-type: lower-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol {
list-style-type: upper-latin;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ol ol ol ol ol {
list-style-type: upper-roman;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul {
list-style-type: disc;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul {
list-style-type: circle;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-list/theme/list.css */
.ck-content ul ul ul ul {
list-style-type: square;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image {
display: table;
clear: both;
text-align: center;
margin: 0.9em auto;
min-width: 50px;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image img {
display: block;
margin: 0 auto;
max-width: 100%;
min-width: 100%;
height: auto;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline {
/*
* Normally, the .image-inline would have "display: inline-block" and "img { width: 100% }" (to follow the wrapper while resizing).;
* Unfortunately, together with "srcset", it gets automatically stretched up to the width of the editing root.
* This strange behavior does not happen with inline-flex.
*/
display: inline-flex;
max-width: 100%;
align-items: flex-start;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline picture {
display: flex;
}
/* @ckeditor/ckeditor5-image/theme/image.css */
.ck-content .image-inline picture,
.ck-content .image-inline img {
flex-grow: 1;
flex-shrink: 1;
max-width: 100%;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content img.image_resized {
height: auto;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized {
max-width: 100%;
display: block;
box-sizing: border-box;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized img {
width: 100%;
}
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
.ck-content .image.image_resized > figcaption {
display: block;
}
/* @ckeditor/ckeditor5-image/theme/imagecaption.css */
.ck-content .image > figcaption {
display: table-caption;
caption-side: bottom;
word-break: break-word;
color: var(--ck-color-image-caption-text);
background-color: var(--ck-color-image-caption-background);
padding: .6em;
font-size: .75em;
outline-offset: -1px;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left,
.ck-content .image-style-block-align-right {
max-width: calc(100% - var(--ck-image-style-spacing));
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left,
.ck-content .image-style-align-right {
clear: none;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-side {
float: right;
margin-left: var(--ck-image-style-spacing);
max-width: 50%;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-left {
float: left;
margin-right: var(--ck-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-center {
margin-left: auto;
margin-right: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-align-right {
float: right;
margin-left: var(--ck-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-right {
margin-right: 0;
margin-left: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-style-block-align-left {
margin-left: 0;
margin-right: auto;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content p + .image-style-align-left,
.ck-content p + .image-style-align-right,
.ck-content p + .image-style-side {
margin-top: 0;
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left,
.ck-content .image-inline.image-style-align-right {
margin-top: var(--ck-inline-image-style-spacing);
margin-bottom: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-left {
margin-right: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
.ck-content .image-inline.image-style-align-right {
margin-left: var(--ck-inline-image-style-spacing);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-yellow {
background-color: var(--ck-highlight-marker-yellow);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-green {
background-color: var(--ck-highlight-marker-green);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-pink {
background-color: var(--ck-highlight-marker-pink);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .marker-blue {
background-color: var(--ck-highlight-marker-blue);
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-red {
color: var(--ck-highlight-pen-red);
background-color: transparent;
}
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
.ck-content .pen-green {
color: var(--ck-highlight-pen-green);
background-color: transparent;
}
/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */
.ck-content blockquote {
overflow: hidden;
padding-right: 1.5em;
padding-left: 1.5em;
margin-left: 0;
margin-right: 0;
font-style: italic;
border-left: solid 5px hsl(0, 0%, 80%);
}
/* @ckeditor/ckeditor5-block-quote/theme/blockquote.css */
.ck-content[dir="rtl"] blockquote {
border-left: 0;
border-right: solid 5px hsl(0, 0%, 80%);
}
/* @ckeditor/ckeditor5-basic-styles/theme/code.css */
.ck-content code {
background-color: hsla(0, 0%, 78%, 0.3);
padding: .15em;
border-radius: 2px;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-tiny {
font-size: .7em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-small {
font-size: .85em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-big {
font-size: 1.4em;
}
/* @ckeditor/ckeditor5-font/theme/fontsize.css */
.ck-content .text-huge {
font-size: 1.8em;
}
/* @ckeditor/ckeditor5-mention/theme/mention.css */
.ck-content .mention {
background: var(--ck-color-mention-background);
color: var(--ck-color-mention-text);
}
/* @ckeditor/ckeditor5-horizontal-line/theme/horizontalline.css */
.ck-content hr {
margin: 15px 0;
height: 4px;
background: hsl(0, 0%, 87%);
border: 0;
}
/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */
.ck-content pre {
padding: 1em;
text-align: left;
direction: ltr;
tab-size: 4;
white-space: pre-wrap;
font-style: normal;
min-width: 200px;
border: 0px;
border-radius: 6px;
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2);
}
.ck-content pre:not(.hljs) {
color: hsl(0, 0%, 20.8%);
background: hsla(0, 0%, 78%, 0.3);
}
/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */
.ck-content pre code {
background: unset;
padding: 0;
border-radius: 0;
}
@media print {
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break {
padding: 0;
}
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
.ck-content .page-break::after {
display: none;
}
}

View File

@ -1,7 +0,0 @@
{
"templates": {
"default": {
"includeDate": false
}
}
}

View File

@ -36,12 +36,12 @@
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.52.0", "@playwright/test": "1.52.0",
"@stylistic/eslint-plugin": "4.2.0", "@stylistic/eslint-plugin": "4.4.0",
"@types/express": "5.0.1", "@types/express": "5.0.1",
"@types/node": "22.15.17", "@types/node": "22.15.29",
"@types/yargs": "17.0.33", "@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.1.3", "@vitest/coverage-v8": "3.1.4",
"eslint": "9.26.0", "eslint": "9.27.0",
"eslint-plugin-simple-import-sort": "12.1.1", "eslint-plugin-simple-import-sort": "12.1.1",
"esm": "3.2.25", "esm": "3.2.25",
"jsdoc": "4.0.4", "jsdoc": "4.0.4",
@ -49,7 +49,7 @@
"rcedit": "4.0.1", "rcedit": "4.0.1",
"rimraf": "6.0.1", "rimraf": "6.0.1",
"tslib": "2.8.1", "tslib": "2.8.1",
"typedoc": "0.28.4", "typedoc": "0.28.5",
"typedoc-plugin-missing-exports": "4.0.0" "typedoc-plugin-missing-exports": "4.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {

View File

@ -0,0 +1,43 @@
### Test regular API metrics endpoint (requires session authentication)
### Get metrics from regular API (default Prometheus format)
GET {{triliumHost}}/api/metrics
> {%
client.test("API metrics endpoint returns Prometheus format by default", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.headers["content-type"].includes("text/plain"), "Content-Type should be text/plain");
client.assert(response.body.includes("trilium_info"), "Should contain trilium_info metric");
client.assert(response.body.includes("trilium_notes_total"), "Should contain trilium_notes_total metric");
client.assert(response.body.includes("# HELP"), "Should contain HELP comments");
client.assert(response.body.includes("# TYPE"), "Should contain TYPE comments");
});
%}
### Get metrics in JSON format
GET {{triliumHost}}/api/metrics?format=json
> {%
client.test("API metrics endpoint returns JSON when requested", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.headers["content-type"].includes("application/json"), "Content-Type should be application/json");
client.assert(response.body.version, "Version info not present");
client.assert(response.body.database, "Database info not present");
client.assert(response.body.timestamp, "Timestamp not present");
client.assert(typeof response.body.database.totalNotes === 'number', "Total notes should be a number");
client.assert(typeof response.body.database.activeNotes === 'number', "Active notes should be a number");
client.assert(response.body.noteTypes, "Note types breakdown not present");
client.assert(response.body.attachmentTypes, "Attachment types breakdown not present");
client.assert(response.body.statistics, "Statistics not present");
});
%}
### Test invalid format parameter
GET {{triliumHost}}/api/metrics?format=xml
> {%
client.test("Invalid format parameter returns error", function() {
client.assert(response.status === 500, "Response status should be 500");
client.assert(response.body.message.includes("prometheus"), "Error message should mention supported formats");
});
%}

View File

@ -0,0 +1,82 @@
### Test ETAPI metrics endpoint
# First login to get a token
POST {{triliumHost}}/etapi/auth/login
Content-Type: application/json
{
"password": "{{password}}"
}
> {%
client.test("Login successful", function() {
client.assert(response.status === 201, "Response status is not 201");
client.assert(response.body.authToken, "Auth token not present");
client.global.set("authToken", response.body.authToken);
});
%}
### Get metrics with authentication (default Prometheus format)
GET {{triliumHost}}/etapi/metrics
Authorization: {{authToken}}
> {%
client.test("Metrics endpoint returns Prometheus format by default", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.headers["content-type"].includes("text/plain"), "Content-Type should be text/plain");
client.assert(response.body.includes("trilium_info"), "Should contain trilium_info metric");
client.assert(response.body.includes("trilium_notes_total"), "Should contain trilium_notes_total metric");
client.assert(response.body.includes("# HELP"), "Should contain HELP comments");
client.assert(response.body.includes("# TYPE"), "Should contain TYPE comments");
});
%}
### Get metrics in JSON format
GET {{triliumHost}}/etapi/metrics?format=json
Authorization: {{authToken}}
> {%
client.test("Metrics endpoint returns JSON when requested", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.headers["content-type"].includes("application/json"), "Content-Type should be application/json");
client.assert(response.body.version, "Version info not present");
client.assert(response.body.database, "Database info not present");
client.assert(response.body.timestamp, "Timestamp not present");
client.assert(typeof response.body.database.totalNotes === 'number', "Total notes should be a number");
client.assert(typeof response.body.database.activeNotes === 'number', "Active notes should be a number");
});
%}
### Get metrics in Prometheus format explicitly
GET {{triliumHost}}/etapi/metrics?format=prometheus
Authorization: {{authToken}}
> {%
client.test("Metrics endpoint returns Prometheus format when requested", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.headers["content-type"].includes("text/plain"), "Content-Type should be text/plain");
client.assert(response.body.includes("trilium_info"), "Should contain trilium_info metric");
client.assert(response.body.includes("trilium_notes_total"), "Should contain trilium_notes_total metric");
});
%}
### Test invalid format parameter
GET {{triliumHost}}/etapi/metrics?format=xml
Authorization: {{authToken}}
> {%
client.test("Invalid format parameter returns error", function() {
client.assert(response.status === 400, "Response status should be 400");
client.assert(response.body.code === "INVALID_FORMAT", "Error code should be INVALID_FORMAT");
client.assert(response.body.message.includes("prometheus"), "Error message should mention supported formats");
});
%}
### Test without authentication (should fail)
GET {{triliumHost}}/etapi/metrics
> {%
client.test("Metrics endpoint requires authentication", function() {
client.assert(response.status === 401, "Response status should be 401");
});
%}

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/public" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
<sourceFolder url="file://$MODULE_DIR$/spec-es6" isTestSource="true" />
<excludeFolder url="file://$MODULE_DIR$/dist" />
<excludeFolder url="file://$MODULE_DIR$/src/public/app-dist" />
<excludeFolder url="file://$MODULE_DIR$/libraries" />
<excludeFolder url="file://$MODULE_DIR$/libraries" />
<excludeFolder url="file://$MODULE_DIR$/docs" />
<excludeFolder url="file://$MODULE_DIR$/bin/better-sqlite3" />
<excludeFolder url="file://$MODULE_DIR$/data" />
<excludeFolder url="file://$MODULE_DIR$/.flatpak-builder" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="@types/jquery" level="application" />
</component>
</module>

View File

@ -10,7 +10,7 @@
"url": "https://github.com/TriliumNext/Notes" "url": "https://github.com/TriliumNext/Notes"
}, },
"dependencies": { "dependencies": {
"@eslint/js": "9.26.0", "@eslint/js": "9.27.0",
"@excalidraw/excalidraw": "0.18.0", "@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.17", "@fullcalendar/core": "6.1.17",
"@fullcalendar/daygrid": "6.1.17", "@fullcalendar/daygrid": "6.1.17",
@ -22,28 +22,33 @@
"@mind-elixir/node-menu": "1.0.5", "@mind-elixir/node-menu": "1.0.5",
"@popperjs/core": "2.11.8", "@popperjs/core": "2.11.8",
"@triliumnext/ckeditor5": "workspace:*", "@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/codemirror": "workspace:*",
"@triliumnext/commons": "workspace:*", "@triliumnext/commons": "workspace:*",
"@triliumnext/highlightjs": "workspace:*",
"autocomplete.js": "0.38.1",
"bootstrap": "5.3.6", "bootstrap": "5.3.6",
"boxicons": "2.1.4",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"dayjs-plugin-utc": "0.1.2", "dayjs-plugin-utc": "0.1.2",
"debounce": "2.2.0", "debounce": "2.2.0",
"draggabilly": "3.0.0", "draggabilly": "3.0.0",
"eslint-linter-browserify": "9.26.0", "force-graph": "1.49.6",
"force-graph": "1.49.5", "globals": "16.2.0",
"globals": "16.1.0", "i18next": "25.2.1",
"i18next": "25.1.2",
"i18next-http-backend": "3.0.2", "i18next-http-backend": "3.0.2",
"jquery": "3.7.1", "jquery": "3.7.1",
"jquery-hotkeys": "0.2.2", "jquery-hotkeys": "0.2.2",
"jquery.fancytree": "2.38.5", "jquery.fancytree": "2.38.5",
"jsplumb": "2.15.6", "jsplumb": "2.15.6",
"katex": "0.16.22",
"knockout": "3.5.1", "knockout": "3.5.1",
"leaflet": "1.9.4", "leaflet": "1.9.4",
"leaflet-gpx": "2.2.0", "leaflet-gpx": "2.2.0",
"mark.js": "8.11.1", "mark.js": "8.11.1",
"marked": "15.0.11", "marked": "15.0.12",
"mermaid": "11.6.0", "mermaid": "11.6.0",
"mind-elixir": "4.5.2", "mind-elixir": "4.6.0",
"normalize.css": "8.0.1",
"panzoom": "9.4.3", "panzoom": "9.4.3",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
@ -55,13 +60,15 @@
"@ckeditor/ckeditor5-inspector": "4.1.0", "@ckeditor/ckeditor5-inspector": "4.1.0",
"@types/bootstrap": "5.2.10", "@types/bootstrap": "5.2.10",
"@types/jquery": "3.5.32", "@types/jquery": "3.5.32",
"@types/leaflet": "1.9.17", "@types/leaflet": "1.9.18",
"@types/leaflet-gpx": "1.3.7", "@types/leaflet-gpx": "1.3.7",
"@types/react": "19.1.3", "@types/mark.js": "8.11.12",
"@types/react-dom": "19.1.3", "@types/react": "19.1.6",
"@types/react-dom": "19.1.5",
"copy-webpack-plugin": "13.0.0", "copy-webpack-plugin": "13.0.0",
"happy-dom": "17.4.7", "happy-dom": "17.5.6",
"script-loader": "0.7.2" "script-loader": "0.7.2",
"vite-plugin-static-copy": "3.0.0"
}, },
"nx": { "nx": {
"name": "client" "name": "client"

View File

@ -27,6 +27,7 @@ import type EditableTextTypeWidget from "../widgets/type_widgets/editable_text.j
import type { NativeImage, TouchBar } from "electron"; import type { NativeImage, TouchBar } from "electron";
import TouchBarComponent from "./touch_bar.js"; import TouchBarComponent from "./touch_bar.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
interface Layout { interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget; getRootWidget: (appContext: AppContext) => RootWidget;
@ -191,7 +192,7 @@ export type CommandMappings = {
ExecuteCommandData<CKTextEditor> & { ExecuteCommandData<CKTextEditor> & {
callback?: GetTextEditorCallback; callback?: GetTextEditorCallback;
}; };
executeWithCodeEditor: CommandData & ExecuteCommandData<CodeMirrorInstance>; executeWithCodeEditor: CommandData & ExecuteCommandData<CodeMirror>;
/** /**
* Called upon when attempting to retrieve the content element of a {@link NoteContext}. * Called upon when attempting to retrieve the content element of a {@link NoteContext}.
* Generally should not be invoked manually, as it is used by {@link NoteContext.getContentElement}. * Generally should not be invoked manually, as it is used by {@link NoteContext.getContentElement}.
@ -283,6 +284,9 @@ export type CommandMappings = {
type EventMappings = { type EventMappings = {
initialRenderComplete: {}; initialRenderComplete: {};
frocaReloaded: {}; frocaReloaded: {};
setLeftPaneVisibility: {
leftPaneVisible: boolean | null;
}
protectedSessionStarted: {}; protectedSessionStarted: {};
notesReloaded: { notesReloaded: {
noteIds: string[]; noteIds: string[];

View File

@ -11,6 +11,7 @@ import type { ViewScope } from "../services/link.js";
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js"; import type TypeWidget from "../widgets/type_widgets/type_widget.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5"; import type { CKTextEditor } from "@triliumnext/ckeditor5";
import type CodeMirror from "@triliumnext/codemirror";
export interface SetNoteOpts { export interface SetNoteOpts {
triggerSwitchEvent?: unknown; triggerSwitchEvent?: unknown;
@ -158,6 +159,9 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
} }
saveToRecentNotes(resolvedNotePath: string) { saveToRecentNotes(resolvedNotePath: string) {
if (options.is("databaseReadonly")) {
return;
}
setTimeout(async () => { setTimeout(async () => {
// we include the note in the recent list only if the user stayed on the note at least 5 seconds // we include the note in the recent list only if the user stayed on the note at least 5 seconds
if (resolvedNotePath && resolvedNotePath === this.notePath) { if (resolvedNotePath && resolvedNotePath === this.notePath) {
@ -253,6 +257,10 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
return false; return false;
} }
if (options.is("databaseReadonly")) {
return true;
}
if (this.note.isLabelTruthy("readOnly")) { if (this.note.isLabelTruthy("readOnly")) {
return true; return true;
} }
@ -331,7 +339,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
async getCodeEditor() { async getCodeEditor() {
return this.timeout( return this.timeout(
new Promise<CodeMirrorInstance>((resolve) => new Promise<CodeMirror>((resolve) =>
appContext.triggerCommand("executeWithCodeEditor", { appContext.triggerCommand("executeWithCodeEditor", {
resolve, resolve,
ntxId: this.ntxId ntxId: this.ntxId

View File

@ -78,15 +78,15 @@ export default class RootCommandExecutor extends Component {
} }
hideLeftPaneCommand() { hideLeftPaneCommand() {
options.save(`leftPaneVisible`, "false"); appContext.triggerEvent("setLeftPaneVisibility", { leftPaneVisible: false });
} }
showLeftPaneCommand() { showLeftPaneCommand() {
options.save(`leftPaneVisible`, "true"); appContext.triggerEvent("setLeftPaneVisibility", { leftPaneVisible: true });
} }
toggleLeftPaneCommand() { toggleLeftPaneCommand() {
options.toggle("leftPaneVisible"); appContext.triggerEvent("setLeftPaneVisibility", { leftPaneVisible: null });
} }
async showBackendLogCommand() { async showBackendLogCommand() {

View File

@ -44,6 +44,9 @@ export default class TabManager extends Component {
if (!appContext.isMainWindow) { if (!appContext.isMainWindow) {
return; return;
} }
if (options.is("databaseReadonly")) {
return;
}
const openNoteContexts = this.noteContexts const openNoteContexts = this.noteContexts
.map((nc) => nc.getPojoState()) .map((nc) => nc.getPojoState())

View File

@ -54,7 +54,7 @@ export default class TouchBarComponent extends Component {
#refreshTouchBar() { #refreshTouchBar() {
const { TouchBar } = this.remote; const { TouchBar } = this.remote;
const parentComponent = this.lastFocusedComponent; const parentComponent = this.lastFocusedComponent;
let touchBar = null; let touchBar: Electron.CrossProcessExports.TouchBar | null = null;
if (this.$activeModal?.length) { if (this.$activeModal?.length) {
touchBar = this.#buildModalTouchBar(); touchBar = this.#buildModalTouchBar();

View File

@ -11,6 +11,9 @@ import options from "./services/options.js";
import type ElectronRemote from "@electron/remote"; import type ElectronRemote from "@electron/remote";
import type Electron from "electron"; import type Electron from "electron";
import "./stylesheets/bootstrap.scss"; import "./stylesheets/bootstrap.scss";
import "boxicons/css/boxicons.min.css";
import "jquery-hotkeys";
import "autocomplete.js/index_jquery.js";
await appContext.earlyInit(); await appContext.earlyInit();

View File

@ -789,7 +789,7 @@ class FNote {
*/ */
async getRelationTargets(name: string) { async getRelationTargets(name: string) {
const relations = this.getRelations(name); const relations = this.getRelations(name);
const targets = []; const targets: (FNote | null)[] = [];
for (const relation of relations) { for (const relation of relations) {
targets.push(await this.froca.getNote(relation.value)); targets.push(await this.froca.getNote(relation.value));

View File

@ -1,74 +0,0 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/LICENSE
(function(mod) {
if (typeof exports == "object" && typeof module == "object") // CommonJS
mod(require("../../lib/codemirror"));
else if (typeof define == "function" && define.amd) // AMD
define(["../../lib/codemirror"], mod);
else // Plain browser env
mod(CodeMirror);
})(function(CodeMirror) {
"use strict";
async function validatorHtml(text, options) {
const result = /<script[^>]*>([\s\S]+)<\/script>/ig.exec(text);
if (result !== null) {
// preceding code is copied over but any (non-newline) character is replaced with space
// this will preserve line numbers etc.
const prefix = text.substr(0, result.index).replace(/./g, " ");
const js = prefix + result[1];
return await validatorJavaScript(js, options);
}
return [];
}
async function validatorJavaScript(text, options) {
if (glob.isMobile()
|| glob.getActiveContextNote() == null
|| glob.getActiveContextNote().mime === 'application/json') {
// eslint doesn't seem to validate pure JSON well
return [];
}
if (text.length > 20000) {
console.log("Skipping linting because of large size: ", text.length);
return [];
}
const errors = await glob.linter(text, glob.getActiveContextNote().mime);
console.log(errors);
const result = [];
if (errors) {
parseErrors(errors, result);
}
return result;
}
CodeMirror.registerHelper("lint", "javascript", validatorJavaScript);
CodeMirror.registerHelper("lint", "html", validatorHtml);
function parseErrors(errors, output) {
for (const error of errors) {
const startLine = error.line - 1;
const endLine = error.endLine !== undefined ? error.endLine - 1 : startLine;
const startCol = error.column - 1;
const endCol = error.endColumn !== undefined ? error.endColumn - 1 : startCol + 1;
output.push({
message: error.message,
severity: error.severity === 1 ? "warning" : "error",
from: CodeMirror.Pos(startLine, startCol),
to: CodeMirror.Pos(endLine, endCol)
});
}
}
});

View File

@ -1,83 +0,0 @@
/*
* highlight.js terraform syntax highlighting definition
*
* @see https://github.com/highlightjs/highlight.js
*
* :TODO:
*
* @package: highlightjs-terraform
* @author: Nikos Tsirmirakis <nikos.tsirmirakis@winopsdba.com>
* @since: 2019-03-20
*
* Description: Terraform (HCL) language definition
* Category: scripting
*/
var module = module ? module : {}; // shim for browser use
function hljsDefineTerraform(hljs) {
var NUMBERS = {
className: 'number',
begin: '\\b\\d+(\\.\\d+)?',
relevance: 0
};
var STRINGS = {
className: 'string',
begin: '"',
end: '"',
contains: [{
className: 'variable',
begin: '\\${',
end: '\\}',
relevance: 9,
contains: [{
className: 'string',
begin: '"',
end: '"'
}, {
className: 'meta',
begin: '[A-Za-z_0-9]*' + '\\(',
end: '\\)',
contains: [
NUMBERS, {
className: 'string',
begin: '"',
end: '"',
contains: [{
className: 'variable',
begin: '\\${',
end: '\\}',
contains: [{
className: 'string',
begin: '"',
end: '"',
contains: [{
className: 'variable',
begin: '\\${',
end: '\\}'
}]
}, {
className: 'meta',
begin: '[A-Za-z_0-9]*' + '\\(',
end: '\\)'
}]
}]
},
'self']
}]
}]
};
return {
aliases: ['tf', 'hcl'],
keywords: 'resource variable provider output locals module data terraform|10',
literal: 'false true null',
contains: [
hljs.COMMENT('\\#', '$'),
NUMBERS,
STRINGS
]
}
}
hljs.registerLanguage('terraform', hljsDefineTerraform);

View File

@ -2,6 +2,8 @@ import appContext from "./components/app_context.js";
import noteAutocompleteService from "./services/note_autocomplete.js"; import noteAutocompleteService from "./services/note_autocomplete.js";
import glob from "./services/glob.js"; import glob from "./services/glob.js";
import "./stylesheets/bootstrap.scss"; import "./stylesheets/bootstrap.scss";
import "boxicons/css/boxicons.min.css";
import "autocomplete.js/index_jquery.js";
glob.setupGlobs(); glob.setupGlobs();

View File

@ -0,0 +1,5 @@
import $ from "jquery";
(window as any).$ = $;
(window as any).jQuery = $;
$("body").show();

View File

@ -4,6 +4,7 @@ import froca from "./froca.js";
import linkService from "./link.js"; import linkService from "./link.js";
import utils from "./utils.js"; import utils from "./utils.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
import toast from "./toast.js";
let clipboardBranchIds: string[] = []; let clipboardBranchIds: string[] = [];
let clipboardMode: string | null = null; let clipboardMode: string | null = null;
@ -79,7 +80,7 @@ async function copy(branchIds: string[]) {
if (utils.isElectron()) { if (utils.isElectron()) {
// https://github.com/zadam/trilium/issues/2401 // https://github.com/zadam/trilium/issues/2401
const { clipboard } = require("electron"); const { clipboard } = require("electron");
const links = []; const links: string[] = [];
for (const branch of froca.getBranches(clipboardBranchIds)) { for (const branch of froca.getBranches(clipboardBranchIds)) {
const $link = await linkService.createLink(`${branch.parentNoteId}/${branch.noteId}`, { referenceLink: true }); const $link = await linkService.createLink(`${branch.parentNoteId}/${branch.noteId}`, { referenceLink: true });
@ -108,6 +109,39 @@ function isClipboardEmpty() {
return clipboardBranchIds.length === 0; return clipboardBranchIds.length === 0;
} }
export function copyText(text: string) {
if (!text) {
return;
}
let succeeded = false;
try {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
succeeded = true;
} else {
// Fallback method: https://stackoverflow.com/a/72239825
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
succeeded = document.execCommand('copy');
document.body.removeChild(textArea);
}
} catch (e) {
console.warn(e);
succeeded = false;
}
if (succeeded) {
toast.showMessage(t("clipboard.copy_success"));
} else {
toast.showError(t("clipboard.copy_failed"));
}
}
export default { export default {
pasteAfter, pasteAfter,
pasteInto, pasteInto,

View File

@ -1,7 +1,6 @@
import renderService from "./render.js"; import renderService from "./render.js";
import protectedSessionService from "./protected_session.js"; import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js"; import protectedSessionHolder from "./protected_session_holder.js";
import libraryLoader from "./library_loader.js";
import openService from "./open.js"; import openService from "./open.js";
import froca from "./froca.js"; import froca from "./froca.js";
import utils from "./utils.js"; import utils from "./utils.js";
@ -10,12 +9,13 @@ import treeService from "./tree.js";
import FNote from "../entities/fnote.js"; import FNote from "../entities/fnote.js";
import FAttachment from "../entities/fattachment.js"; import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.js"; import imageContextMenuService from "../menus/image_context_menu.js";
import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js"; import { applySingleBlockSyntaxHighlight, formatCodeBlocks } from "./syntax_highlight.js";
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js"; import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
import { normalizeMimeTypeForCKEditor } from "./mime_type_definitions.js";
import renderDoc from "./doc_renderer.js"; import renderDoc from "./doc_renderer.js";
import { t } from "i18next"; import { t } from "../services/i18n.js";
import WheelZoom from 'vanilla-js-wheel-zoom'; import WheelZoom from 'vanilla-js-wheel-zoom';
import { renderMathInElement } from "./math.js";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
let idCounter = 1; let idCounter = 1;
@ -94,8 +94,6 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HT
$renderedContent.append($('<div class="ck-content">').html(blob.content)); $renderedContent.append($('<div class="ck-content">').html(blob.content));
if ($renderedContent.find("span.math-tex").length > 0) { if ($renderedContent.find("span.math-tex").length > 0) {
await libraryLoader.requireLibrary(libraryLoader.KATEX);
renderMathInElement($renderedContent[0], { trust: true }); renderMathInElement($renderedContent[0], { trust: true });
} }
@ -108,7 +106,7 @@ async function renderText(note: FNote | FAttachment, $renderedContent: JQuery<HT
await linkService.loadReferenceLinkTitle($(el)); await linkService.loadReferenceLinkTitle($(el));
} }
await applySyntaxHighlight($renderedContent); await formatCodeBlocks($renderedContent);
} else if (note instanceof FNote) { } else if (note instanceof FNote) {
await renderChildrenList($renderedContent, note); await renderChildrenList($renderedContent, note);
} }

View File

@ -1,6 +1,6 @@
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import { getCurrentLanguage } from "./i18n.js"; import { getCurrentLanguage } from "./i18n.js";
import { applySyntaxHighlight } from "./syntax_highlight.js"; import { formatCodeBlocks } from "./syntax_highlight.js";
export default function renderDoc(note: FNote) { export default function renderDoc(note: FNote) {
return new Promise<JQuery<HTMLElement>>((resolve) => { return new Promise<JQuery<HTMLElement>>((resolve) => {
@ -41,12 +41,13 @@ function processContent(url: string, $content: JQuery<HTMLElement>) {
$img.attr("src", dir + "/" + $img.attr("src")); $img.attr("src", dir + "/" + $img.attr("src"));
}); });
applySyntaxHighlight($content); formatCodeBlocks($content);
} }
function getUrl(docNameValue: string, language: string) { function getUrl(docNameValue: string, language: string) {
// Cannot have spaces in the URL due to how JQuery.load works. // Cannot have spaces in the URL due to how JQuery.load works.
docNameValue = docNameValue.replaceAll(" ", "%20"); docNameValue = docNameValue.replaceAll(" ", "%20");
return `${window.glob.appPath}/doc_notes/${language}/${docNameValue}.html`; const basePath = window.glob.isDev ? new URL(window.glob.assetPath).pathname : window.glob.assetPath;
return `${basePath}/doc_notes/${language}/${docNameValue}.html`;
} }

View File

@ -50,7 +50,7 @@ async function processEntityChanges(entityChanges: EntityChange[]) {
// To this we count: standard parent-child relationships and template/inherit relations (attribute inheritance follows them). // To this we count: standard parent-child relationships and template/inherit relations (attribute inheritance follows them).
// Here we watch for changes which might violate this principle - e.g., an introduction of a new "inherit" relation might // Here we watch for changes which might violate this principle - e.g., an introduction of a new "inherit" relation might
// mean we need to load the target of the relation (and then perhaps transitively the whole note path of this target). // mean we need to load the target of the relation (and then perhaps transitively the whole note path of this target).
const missingNoteIds = []; const missingNoteIds: string[] = [];
for (const { entityName, entity } of entityChanges) { for (const { entityName, entity } of entityChanges) {
if (!entity) { if (!entity) {

View File

@ -1,11 +1,9 @@
import utils from "./utils.js"; import utils from "./utils.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
import server from "./server.js"; import server from "./server.js";
import libraryLoader from "./library_loader.js";
import ws from "./ws.js"; import ws from "./ws.js";
import froca from "./froca.js"; import froca from "./froca.js";
import linkService from "./link.js"; import linkService from "./link.js";
import { lint } from "./eslint.js";
function setupGlobs() { function setupGlobs() {
window.glob.isDesktop = utils.isDesktop; window.glob.isDesktop = utils.isDesktop;
@ -18,8 +16,6 @@ function setupGlobs() {
// required for ESLint plugin and CKEditor // required for ESLint plugin and CKEditor
window.glob.getActiveContextNote = () => appContext.tabManager.getActiveContextNote(); window.glob.getActiveContextNote = () => appContext.tabManager.getActiveContextNote();
window.glob.requireLibrary = libraryLoader.requireLibrary;
window.glob.linter = lint;
window.glob.appContext = appContext; // for debugging window.glob.appContext = appContext; // for debugging
window.glob.froca = froca; window.glob.froca = froca;
window.glob.treeCache = froca; // compatibility for CKEditor builds for a while window.glob.treeCache = froca; // compatibility for CKEditor builds for a while
@ -66,7 +62,7 @@ function setupGlobs() {
}); });
for (const appCssNoteId of glob.appCssNoteIds || []) { for (const appCssNoteId of glob.appCssNoteIds || []) {
libraryLoader.requireCss(`api/notes/download/${appCssNoteId}`, false); requireCss(`api/notes/download/${appCssNoteId}`, false);
} }
utils.initHelpButtons($(window)); utils.initHelpButtons($(window));
@ -78,6 +74,18 @@ function setupGlobs() {
}); });
} }
async function requireCss(url: string, prependAssetPath = true) {
const cssLinks = Array.from(document.querySelectorAll("link")).map((el) => el.href);
if (!cssLinks.some((l) => l.endsWith(url))) {
if (prependAssetPath) {
url = `${window.glob.assetPath}/${url}`;
}
$("head").append($('<link rel="stylesheet" type="text/css" />').attr("href", url));
}
}
export default { export default {
setupGlobs setupGlobs
}; };

View File

@ -1,158 +0,0 @@
import mimeTypesService from "./mime_types.js";
import optionsService from "./options.js";
import { getStylesheetUrl } from "./syntax_highlight.js";
export interface Library {
js?: string[] | (() => string[]);
css?: string[];
}
const CODE_MIRROR: Library = {
js: () => {
const scriptsToLoad = [
"node_modules/codemirror/lib/codemirror.js",
"node_modules/codemirror/addon/display/placeholder.js",
"node_modules/codemirror/addon/edit/matchbrackets.js",
"node_modules/codemirror/addon/edit/matchtags.js",
"node_modules/codemirror/addon/fold/xml-fold.js",
"node_modules/codemirror/addon/lint/lint.js",
"node_modules/codemirror/addon/mode/loadmode.js",
"node_modules/codemirror/addon/mode/multiplex.js",
"node_modules/codemirror/addon/mode/overlay.js",
"node_modules/codemirror/addon/mode/simple.js",
"node_modules/codemirror/addon/search/match-highlighter.js",
"node_modules/codemirror/mode/meta.js",
"node_modules/codemirror/keymap/vim.js",
"libraries/codemirror/eslint.js"
];
const mimeTypes = mimeTypesService.getMimeTypes();
for (const mimeType of mimeTypes) {
if (mimeType.enabled && mimeType.codeMirrorSource) {
scriptsToLoad.push(mimeType.codeMirrorSource);
}
}
return scriptsToLoad;
},
css: ["node_modules/codemirror/lib/codemirror.css", "node_modules/codemirror/addon/lint/lint.css"]
};
const KATEX: Library = {
js: ["node_modules/katex/dist/katex.min.js", "node_modules/katex/dist/contrib/mhchem.min.js", "node_modules/katex/dist/contrib/auto-render.min.js"],
css: ["node_modules/katex/dist/katex.min.css"]
};
const HIGHLIGHT_JS: Library = {
js: () => {
const mimeTypes = mimeTypesService.getMimeTypes();
const scriptsToLoad = new Set<string>();
scriptsToLoad.add("node_modules/@highlightjs/cdn-assets/highlight.min.js");
for (const mimeType of mimeTypes) {
const id = mimeType.highlightJs;
if (!mimeType.enabled || !id) {
continue;
}
if (mimeType.highlightJsSource === "libraries") {
scriptsToLoad.add(`libraries/highlightjs/${id}.js`);
} else {
// Built-in module.
scriptsToLoad.add(`node_modules/@highlightjs/cdn-assets/languages/${id}.min.js`);
}
}
const currentTheme = String(optionsService.get("codeBlockTheme"));
loadHighlightingTheme(currentTheme);
return Array.from(scriptsToLoad);
}
};
async function requireLibrary(library: Library) {
if (library.css) {
library.css.map((cssUrl) => requireCss(cssUrl));
}
if (library.js) {
for (const scriptUrl of await unwrapValue(library.js)) {
await requireScript(scriptUrl);
}
}
}
async function unwrapValue<T>(value: T | (() => T) | Promise<T>) {
if (value && typeof value === "object" && "then" in value) {
return (await (value as Promise<() => T>))();
}
if (typeof value === "function") {
return (value as () => T)();
}
return value;
}
// we save the promises in case of the same script being required concurrently multiple times
const loadedScriptPromises: Record<string, JQuery.jqXHR> = {};
async function requireScript(url: string) {
url = `${window.glob.assetPath}/${url}`;
if (!loadedScriptPromises[url]) {
loadedScriptPromises[url] = $.ajax({
url: url,
dataType: "script",
cache: true
});
}
await loadedScriptPromises[url];
}
async function requireCss(url: string, prependAssetPath = true) {
const cssLinks = Array.from(document.querySelectorAll("link")).map((el) => el.href);
if (!cssLinks.some((l) => l.endsWith(url))) {
if (prependAssetPath) {
url = `${window.glob.assetPath}/${url}`;
}
$("head").append($('<link rel="stylesheet" type="text/css" />').attr("href", url));
}
}
let highlightingThemeEl: JQuery<HTMLElement> | null = null;
function loadHighlightingTheme(theme: string) {
if (!theme) {
return;
}
if (theme === "none") {
// Deactivate the theme.
if (highlightingThemeEl) {
highlightingThemeEl.remove();
highlightingThemeEl = null;
}
return;
}
if (!highlightingThemeEl) {
highlightingThemeEl = $(`<link rel="stylesheet" type="text/css" />`);
$("head").append(highlightingThemeEl);
}
const url = getStylesheetUrl(theme);
if (url) {
highlightingThemeEl.attr("href", url);
}
}
export default {
requireCss,
requireLibrary,
loadHighlightingTheme,
CODE_MIRROR,
KATEX,
HIGHLIGHT_JS
};

View File

@ -59,6 +59,7 @@ export interface ViewScope {
* toc will appear and then close immediately, because getToc(html) function will consume time * toc will appear and then close immediately, because getToc(html) function will consume time
*/ */
tocPreviousVisible?: boolean; tocPreviousVisible?: boolean;
tocCollapsedHeadings?: Set<string>;
} }
interface CreateLinkOptions { interface CreateLinkOptions {
@ -215,9 +216,9 @@ export function parseNavigationStateFromUrl(url: string | undefined) {
const viewScope: ViewScope = { const viewScope: ViewScope = {
viewMode: "default" viewMode: "default"
}; };
let ntxId = null; let ntxId: string | null = null;
let hoistedNoteId = null; let hoistedNoteId: string | null = null;
let searchString = null; let searchString: string | null = null;
if (paramString) { if (paramString) {
for (const pair of paramString.split("&")) { for (const pair of paramString.split("&")) {

View File

@ -0,0 +1,5 @@
import katex from "katex";
import "katex/contrib/mhchem";
import "katex/dist/katex.min.css";
export { default as renderMathInElement } from "katex/contrib/auto-render";
export default katex;

View File

@ -1,221 +0,0 @@
// TODO: deduplicate with /src/services/import/mime_type_definitions.ts
/**
* A pseudo-MIME type which is used in the editor to automatically determine the language used in code blocks via heuristics.
*/
export const MIME_TYPE_AUTO = "text-x-trilium-auto";
export interface MimeTypeDefinition {
default?: boolean;
title: string;
mime: string;
/** The name of the language/mime type as defined by highlight.js (or one of the aliases), in order to be used for syntax highlighting such as inside code blocks. */
highlightJs?: string;
/** If specified, will load the corresponding highlight.js file from the `libraries/highlightjs/${id}.js` instead of `node_modules/@highlightjs/cdn-assets/languages/${id}.min.js`. */
highlightJsSource?: "libraries";
/** If specified, will load the corresponding highlight file from the given path instead of `node_modules`. */
codeMirrorSource?: string;
}
/**
* For highlight.js-supported languages, see https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md.
*/
export const MIME_TYPES_DICT: readonly MimeTypeDefinition[] = Object.freeze([
{ title: "Plain text", mime: "text/plain", highlightJs: "plaintext", default: true },
// Keep sorted alphabetically.
{ title: "APL", mime: "text/apl" },
{ title: "ASN.1", mime: "text/x-ttcn-asn" },
{ title: "ASP.NET", mime: "application/x-aspx" },
{ title: "Asterisk", mime: "text/x-asterisk" },
{ title: "Batch file (DOS)", mime: "application/x-bat", highlightJs: "dos", codeMirrorSource: "libraries/codemirror/batch.js" },
{ title: "Brainfuck", mime: "text/x-brainfuck", highlightJs: "brainfuck" },
{ title: "C", mime: "text/x-csrc", highlightJs: "c", default: true },
{ title: "C#", mime: "text/x-csharp", highlightJs: "csharp", default: true },
{ title: "C++", mime: "text/x-c++src", highlightJs: "cpp", default: true },
{ title: "Clojure", mime: "text/x-clojure", highlightJs: "clojure" },
{ title: "ClojureScript", mime: "text/x-clojurescript" },
{ title: "Closure Stylesheets (GSS)", mime: "text/x-gss" },
{ title: "CMake", mime: "text/x-cmake", highlightJs: "cmake" },
{ title: "Cobol", mime: "text/x-cobol" },
{ title: "CoffeeScript", mime: "text/coffeescript", highlightJs: "coffeescript" },
{ title: "Common Lisp", mime: "text/x-common-lisp", highlightJs: "lisp" },
{ title: "CQL", mime: "text/x-cassandra" },
{ title: "Crystal", mime: "text/x-crystal", highlightJs: "crystal" },
{ title: "CSS", mime: "text/css", highlightJs: "css", default: true },
{ title: "Cypher", mime: "application/x-cypher-query" },
{ title: "Cython", mime: "text/x-cython" },
{ title: "D", mime: "text/x-d", highlightJs: "d" },
{ title: "Dart", mime: "application/dart", highlightJs: "dart" },
{ title: "diff", mime: "text/x-diff", highlightJs: "diff" },
{ title: "Django", mime: "text/x-django", highlightJs: "django" },
{ title: "Dockerfile", mime: "text/x-dockerfile", highlightJs: "dockerfile" },
{ title: "DTD", mime: "application/xml-dtd" },
{ title: "Dylan", mime: "text/x-dylan" },
{ title: "EBNF", mime: "text/x-ebnf", highlightJs: "ebnf" },
{ title: "ECL", mime: "text/x-ecl" },
{ title: "edn", mime: "application/edn" },
{ title: "Eiffel", mime: "text/x-eiffel" },
{ title: "Elm", mime: "text/x-elm", highlightJs: "elm" },
{ title: "Embedded Javascript", mime: "application/x-ejs" },
{ title: "Embedded Ruby", mime: "application/x-erb", highlightJs: "erb" },
{ title: "Erlang", mime: "text/x-erlang", highlightJs: "erlang" },
{ title: "Esper", mime: "text/x-esper" },
{ title: "F#", mime: "text/x-fsharp", highlightJs: "fsharp" },
{ title: "Factor", mime: "text/x-factor" },
{ title: "FCL", mime: "text/x-fcl" },
{ title: "Forth", mime: "text/x-forth" },
{ title: "Fortran", mime: "text/x-fortran", highlightJs: "fortran" },
{ title: "Gas", mime: "text/x-gas" },
{ title: "Gherkin", mime: "text/x-feature", highlightJs: "gherkin" },
{ title: "GitHub Flavored Markdown", mime: "text/x-gfm", highlightJs: "markdown" },
{ title: "Go", mime: "text/x-go", highlightJs: "go", default: true },
{ title: "Groovy", mime: "text/x-groovy", highlightJs: "groovy", default: true },
{ title: "HAML", mime: "text/x-haml", highlightJs: "haml" },
{ title: "Haskell (Literate)", mime: "text/x-literate-haskell" },
{ title: "Haskell", mime: "text/x-haskell", highlightJs: "haskell", default: true },
{ title: "Haxe", mime: "text/x-haxe", highlightJs: "haxe" },
{ title: "HTML", mime: "text/html", highlightJs: "xml", default: true },
{ title: "HTTP", mime: "message/http", highlightJs: "http", default: true },
{ title: "HXML", mime: "text/x-hxml" },
{ title: "IDL", mime: "text/x-idl" },
{ title: "Java Server Pages", mime: "application/x-jsp", highlightJs: "java" },
{ title: "Java", mime: "text/x-java", highlightJs: "java", default: true },
{ title: "Jinja2", mime: "text/jinja2" },
{ title: "JS backend", mime: "application/javascript;env=backend", highlightJs: "javascript", default: true },
{ title: "JS frontend", mime: "application/javascript;env=frontend", highlightJs: "javascript", default: true },
{ title: "JSON-LD", mime: "application/ld+json", highlightJs: "json" },
{ title: "JSON", mime: "application/json", highlightJs: "json", default: true },
{ title: "JSX", mime: "text/jsx", highlightJs: "javascript" },
{ title: "Julia", mime: "text/x-julia", highlightJs: "julia" },
{ title: "Kotlin", mime: "text/x-kotlin", highlightJs: "kotlin", default: true },
{ title: "LaTeX", mime: "text/x-latex", highlightJs: "latex" },
{ title: "LESS", mime: "text/x-less", highlightJs: "less" },
{ title: "LiveScript", mime: "text/x-livescript", highlightJs: "livescript" },
{ title: "Lua", mime: "text/x-lua", highlightJs: "lua" },
{ title: "MariaDB SQL", mime: "text/x-mariadb", highlightJs: "sql" },
{ title: "Markdown", mime: "text/x-markdown", highlightJs: "markdown", default: true },
{ title: "Mathematica", mime: "text/x-mathematica", highlightJs: "mathematica" },
{ title: "mbox", mime: "application/mbox" },
{ title: "MIPS Assembler", mime: "text/x-asm-mips", highlightJs: "mipsasm" },
{ title: "mIRC", mime: "text/mirc" },
{ title: "Modelica", mime: "text/x-modelica" },
{ title: "MS SQL", mime: "text/x-mssql", highlightJs: "sql" },
{ title: "mscgen", mime: "text/x-mscgen" },
{ title: "msgenny", mime: "text/x-msgenny" },
{ title: "MUMPS", mime: "text/x-mumps" },
{ title: "MySQL", mime: "text/x-mysql", highlightJs: "sql" },
{ title: "Nginx", mime: "text/x-nginx-conf", highlightJs: "nginx" },
{ title: "NSIS", mime: "text/x-nsis", highlightJs: "nsis" },
{ title: "NTriples", mime: "application/n-triples" },
{ title: "Objective-C", mime: "text/x-objectivec", highlightJs: "objectivec" },
{ title: "OCaml", mime: "text/x-ocaml", highlightJs: "ocaml" },
{ title: "Octave", mime: "text/x-octave" },
{ title: "Oz", mime: "text/x-oz" },
{ title: "Pascal", mime: "text/x-pascal", highlightJs: "delphi" },
{ title: "PEG.js", mime: "null" },
{ title: "Perl", mime: "text/x-perl", default: true },
{ title: "PGP", mime: "application/pgp" },
{ title: "PHP", mime: "text/x-php", default: true, highlightJs: "php" },
{ title: "Pig", mime: "text/x-pig" },
{ title: "PLSQL", mime: "text/x-plsql", highlightJs: "sql" },
{ title: "PostgreSQL", mime: "text/x-pgsql", highlightJs: "pgsql" },
{ title: "PowerShell", mime: "application/x-powershell", highlightJs: "powershell" },
{ title: "Properties files", mime: "text/x-properties", highlightJs: "properties" },
{ title: "ProtoBuf", mime: "text/x-protobuf", highlightJs: "protobuf" },
{ title: "Pug", mime: "text/x-pug" },
{ title: "Puppet", mime: "text/x-puppet", highlightJs: "puppet" },
{ title: "Python", mime: "text/x-python", highlightJs: "python", default: true },
{ title: "Q", mime: "text/x-q", highlightJs: "q" },
{ title: "R", mime: "text/x-rsrc", highlightJs: "r" },
{ title: "reStructuredText", mime: "text/x-rst" },
{ title: "RPM Changes", mime: "text/x-rpm-changes" },
{ title: "RPM Spec", mime: "text/x-rpm-spec" },
{ title: "Ruby", mime: "text/x-ruby", highlightJs: "ruby", default: true },
{ title: "Rust", mime: "text/x-rustsrc", highlightJs: "rust" },
{ title: "SAS", mime: "text/x-sas", highlightJs: "sas" },
{ title: "Sass", mime: "text/x-sass", highlightJs: "scss" },
{ title: "Scala", mime: "text/x-scala" },
{ title: "Scheme", mime: "text/x-scheme" },
{ title: "SCSS", mime: "text/x-scss", highlightJs: "scss" },
{ title: "Shell (bash)", mime: "text/x-sh", highlightJs: "bash", default: true },
{ title: "Sieve", mime: "application/sieve" },
{ title: "Slim", mime: "text/x-slim" },
{ title: "Smalltalk", mime: "text/x-stsrc", highlightJs: "smalltalk" },
{ title: "Smarty", mime: "text/x-smarty" },
{ title: "SML", mime: "text/x-sml", highlightJs: "sml" },
{ title: "Solr", mime: "text/x-solr" },
{ title: "Soy", mime: "text/x-soy" },
{ title: "SPARQL", mime: "application/sparql-query" },
{ title: "Spreadsheet", mime: "text/x-spreadsheet" },
{ title: "SQL", mime: "text/x-sql", highlightJs: "sql", default: true },
{ title: "SQLite (Trilium)", mime: "text/x-sqlite;schema=trilium", highlightJs: "sql", default: true },
{ title: "SQLite", mime: "text/x-sqlite", highlightJs: "sql" },
{ title: "Squirrel", mime: "text/x-squirrel" },
{ title: "sTeX", mime: "text/x-stex" },
{ title: "Stylus", mime: "text/x-styl", highlightJs: "stylus" },
{ title: "Swift", mime: "text/x-swift", default: true },
{ title: "SystemVerilog", mime: "text/x-systemverilog" },
{ title: "Tcl", mime: "text/x-tcl", highlightJs: "tcl" },
{ title: "Terraform (HCL)", mime: "text/x-hcl", highlightJs: "terraform", highlightJsSource: "libraries", codeMirrorSource: "libraries/codemirror/hcl.js" },
{ title: "Textile", mime: "text/x-textile" },
{ title: "TiddlyWiki ", mime: "text/x-tiddlywiki" },
{ title: "Tiki wiki", mime: "text/tiki" },
{ title: "TOML", mime: "text/x-toml", highlightJs: "ini" },
{ title: "Tornado", mime: "text/x-tornado" },
{ title: "troff", mime: "text/troff" },
{ title: "TTCN_CFG", mime: "text/x-ttcn-cfg" },
{ title: "TTCN", mime: "text/x-ttcn" },
{ title: "Turtle", mime: "text/turtle" },
{ title: "Twig", mime: "text/x-twig", highlightJs: "twig" },
{ title: "TypeScript-JSX", mime: "text/typescript-jsx", highlightJs: "typescript" },
{ title: "TypeScript", mime: "application/typescript", highlightJs: "typescript" },
{ title: "VB.NET", mime: "text/x-vb", highlightJs: "vbnet" },
{ title: "VBScript", mime: "text/vbscript", highlightJs: "vbscript" },
{ title: "Velocity", mime: "text/velocity" },
{ title: "Verilog", mime: "text/x-verilog", highlightJs: "verilog" },
{ title: "VHDL", mime: "text/x-vhdl", highlightJs: "vhdl" },
{ title: "Vue.js Component", mime: "text/x-vue" },
{ title: "Web IDL", mime: "text/x-webidl" },
{ title: "XML", mime: "text/xml", highlightJs: "xml", default: true },
{ title: "XQuery", mime: "application/xquery", highlightJs: "xquery" },
{ title: "xu", mime: "text/x-xu" },
{ title: "Yacas", mime: "text/x-yacas" },
{ title: "YAML", mime: "text/x-yaml", highlightJs: "yaml", default: true },
{ title: "Z80", mime: "text/x-z80" }
]);
/**
* Given a MIME type in the usual format (e.g. `text/csrc`), it returns a MIME type that can be passed down to the CKEditor
* code plugin.
*
* @param mimeType The MIME type to normalize, in the usual format (e.g. `text/c-src`).
* @returns the normalized MIME type (e.g. `text-c-src`).
*/
export function normalizeMimeTypeForCKEditor(mimeType: string) {
return mimeType.toLowerCase().replace(/[\W_]+/g, "-");
}
let byHighlightJsNameMappings: Record<string, MimeTypeDefinition> | null = null;
/**
* Given a Highlight.js language tag (e.g. `css`), it returns a corresponding {@link MimeTypeDefinition} if found.
*
* If there are multiple {@link MimeTypeDefinition}s for the language tag, then only the first one is retrieved. For example for `javascript`, the "JS frontend" mime type is returned.
*
* @param highlightJsName a language tag.
* @returns the corresponding {@link MimeTypeDefinition} if found, or `undefined` otherwise.
*/
export function getMimeTypeFromHighlightJs(highlightJsName: string) {
if (!byHighlightJsNameMappings) {
byHighlightJsNameMappings = {};
for (const mimeType of MIME_TYPES_DICT) {
if (mimeType.highlightJs && !byHighlightJsNameMappings[mimeType.highlightJs]) {
byHighlightJsNameMappings[mimeType.highlightJs] = mimeType;
}
}
}
return byHighlightJsNameMappings[highlightJsName];
}

View File

@ -1,13 +1,6 @@
import { MIME_TYPE_AUTO, MIME_TYPES_DICT, normalizeMimeTypeForCKEditor, type MimeTypeDefinition } from "./mime_type_definitions.js"; import { normalizeMimeTypeForCKEditor, type MimeType, MIME_TYPE_AUTO, MIME_TYPES_DICT } from "@triliumnext/commons";
import options from "./options.js"; import options from "./options.js";
interface MimeType extends MimeTypeDefinition {
/**
* True if this mime type was enabled by the user in the "Available MIME types in the dropdown" option in the Code Notes settings.
*/
enabled: boolean;
}
let mimeTypes: MimeType[] | null = null; let mimeTypes: MimeType[] | null = null;
function loadMimeTypes() { function loadMimeTypes() {
@ -45,8 +38,8 @@ export function getHighlightJsNameForMime(mimeType: string) {
for (const mimeType of mimeTypes) { for (const mimeType of mimeTypes) {
// The mime stored by CKEditor is text-x-csrc instead of text/x-csrc so we keep this format for faster lookup. // The mime stored by CKEditor is text-x-csrc instead of text/x-csrc so we keep this format for faster lookup.
const normalizedMime = normalizeMimeTypeForCKEditor(mimeType.mime); const normalizedMime = normalizeMimeTypeForCKEditor(mimeType.mime);
if (mimeType.highlightJs) { if (mimeType.mdLanguageCode) {
mimeToHighlightJsMapping[normalizedMime] = mimeType.highlightJs; mimeToHighlightJsMapping[normalizedMime] = mimeType.mdLanguageCode;
} }
} }
} }

View File

@ -8,6 +8,10 @@ import appContext from "../components/app_context.js";
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import { t } from "./i18n.js"; import { t } from "./i18n.js";
// Track all elements that open tooltips
let openTooltipElements: JQuery<HTMLElement>[] = [];
let dismissTimer: ReturnType<typeof setTimeout>;
function setupGlobalTooltip() { function setupGlobalTooltip() {
$(document).on("mouseenter", "a", mouseEnterHandler); $(document).on("mouseenter", "a", mouseEnterHandler);
@ -23,7 +27,12 @@ function setupGlobalTooltip() {
} }
function dismissAllTooltips() { function dismissAllTooltips() {
$(".note-tooltip").remove(); clearTimeout(dismissTimer);
openTooltipElements.forEach($el => {
$el.tooltip("dispose");
$el.removeAttr("aria-describedby");
});
openTooltipElements = [];
} }
function setupElementTooltip($el: JQuery<HTMLElement>) { function setupElementTooltip($el: JQuery<HTMLElement>) {
@ -86,8 +95,8 @@ async function mouseEnterHandler(this: HTMLElement) {
// we need to check if we're still hovering over the element // we need to check if we're still hovering over the element
// since the operation to get tooltip content was async, it is possible that // since the operation to get tooltip content was async, it is possible that
// we now create tooltip which won't close because it won't receive mouseleave event // we now create tooltip which won't close because it won't receive mouseleave event
if ($(this).filter(":hover").length > 0) { if ($link.filter(":hover").length > 0) {
$(this).tooltip({ $link.tooltip({
container: "body", container: "body",
// https://github.com/zadam/trilium/issues/2794 https://github.com/zadam/trilium/issues/2988 // https://github.com/zadam/trilium/issues/2794 https://github.com/zadam/trilium/issues/2988
// with bottom this flickering happens a bit less // with bottom this flickering happens a bit less
@ -103,7 +112,9 @@ async function mouseEnterHandler(this: HTMLElement) {
}); });
dismissAllTooltips(); dismissAllTooltips();
$(this).tooltip("show"); $link.tooltip("show");
openTooltipElements.push($link);
// Dismiss the tooltip immediately if a link was clicked inside the tooltip. // Dismiss the tooltip immediately if a link was clicked inside the tooltip.
$(`.${tooltipClass} a`).on("click", (e) => { $(`.${tooltipClass} a`).on("click", (e) => {
@ -115,15 +126,16 @@ async function mouseEnterHandler(this: HTMLElement) {
// click on links within tooltip etc. without tooltip disappearing // click on links within tooltip etc. without tooltip disappearing
// - once the user moves the cursor away from both link and the tooltip, hide the tooltip // - once the user moves the cursor away from both link and the tooltip, hide the tooltip
const checkTooltip = () => { const checkTooltip = () => {
if (!$(this).filter(":hover").length && !$(`.${linkId}:hover`).length) {
if (!$link.filter(":hover").length && !$(`.${linkId}:hover`).length) {
// cursor is neither over the link nor over the tooltip, user likely is not interested // cursor is neither over the link nor over the tooltip, user likely is not interested
dismissAllTooltips(); dismissAllTooltips();
} else { } else {
setTimeout(checkTooltip, 1000); dismissTimer = setTimeout(checkTooltip, 1000);
} }
}; };
setTimeout(checkTooltip, 1000); dismissTimer = setTimeout(checkTooltip, 1000);
} }
} }
@ -176,7 +188,25 @@ function renderFootnote($link: JQuery<HTMLElement>, url: string) {
.closest(".footnote-item") // find the parent container of the footnote .closest(".footnote-item") // find the parent container of the footnote
.find(".footnote-content"); // find the actual text content of the footnote .find(".footnote-content"); // find the actual text content of the footnote
return $footnoteContent.html() || ""; const isEditable = $link.closest(".ck-content").hasClass("note-detail-editable-text-editor");
if (isEditable) {
/* Remove widget buttons for tables, formulas, and images in editable notes. */
$footnoteContent.find('.ck-widget__selection-handle').remove();
$footnoteContent.find('.ck-widget__type-around').remove();
$footnoteContent.find('.ck-widget__resizer').remove();
/* Handling in-line math formulas */
$footnoteContent.find('.ck-math-tex.ck-math-tex-inline.ck-widget').each(function () {
const $katex = $(this).find('.katex').first();
if ($katex.length) {
$(this).replaceWith($('<span class="math-tex"></span>').append($('<span></span>').append($katex.clone())));
}
});
}
let footnoteContent = $footnoteContent.html();
footnoteContent = `<div class="ck-content">${footnoteContent}</div>`
return footnoteContent || "";
} }
export default { export default {

View File

@ -1,7 +1,7 @@
type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url"; type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url";
type Multiplicity = "single" | "multi"; type Multiplicity = "single" | "multi";
interface DefinitionObject { export interface DefinitionObject {
isPromoted?: boolean; isPromoted?: boolean;
labelType?: LabelType; labelType?: LabelType;
multiplicity?: Multiplicity; multiplicity?: Multiplicity;

View File

@ -3,7 +3,11 @@ import Split from "split.js"
export const DEFAULT_GUTTER_SIZE = 5; export const DEFAULT_GUTTER_SIZE = 5;
let leftPaneWidth: number;
let reservedPx: number;
let layoutOrientation: string;
let leftInstance: ReturnType<typeof Split> | null; let leftInstance: ReturnType<typeof Split> | null;
let rightPaneWidth: number;
let rightInstance: ReturnType<typeof Split> | null; let rightInstance: ReturnType<typeof Split> | null;
function setupLeftPaneResizer(leftPaneVisible: boolean) { function setupLeftPaneResizer(leftPaneVisible: boolean) {
@ -14,27 +18,34 @@ function setupLeftPaneResizer(leftPaneVisible: boolean) {
$("#left-pane").toggle(leftPaneVisible); $("#left-pane").toggle(leftPaneVisible);
layoutOrientation = layoutOrientation ?? options.get("layoutOrientation");
reservedPx = reservedPx ?? (layoutOrientation === "vertical" ? ($("#launcher-pane").outerWidth() || 0) : 0);
// Window resizing causes `window.innerWidth` to change, so `reservedWidth` needs to be recalculated each time.
const reservedWidth = reservedPx / window.innerWidth * 100;
if (!leftPaneVisible) { if (!leftPaneVisible) {
$("#rest-pane").css("width", "100%"); $("#rest-pane").css("width", layoutOrientation === "vertical" ? `${100 - reservedWidth}%` : "100%");
return; return;
} }
let leftPaneWidth = options.getInt("leftPaneWidth"); leftPaneWidth = leftPaneWidth ?? (options.getInt("leftPaneWidth") ?? 0);
if (!leftPaneWidth || leftPaneWidth < 5) { if (!leftPaneWidth || leftPaneWidth < 5) {
leftPaneWidth = 5; leftPaneWidth = 5;
} }
const restPaneWidth = 100 - leftPaneWidth - reservedWidth;
if (leftPaneVisible) { if (leftPaneVisible) {
// Delayed initialization ensures that all DOM elements are fully rendered and part of the layout, // Delayed initialization ensures that all DOM elements are fully rendered and part of the layout,
// preventing Split.js from retrieving incorrect dimensions due to #left-pane not being rendered yet, // preventing Split.js from retrieving incorrect dimensions due to #left-pane not being rendered yet,
// which would cause the minSize setting to have no effect. // which would cause the minSize setting to have no effect.
requestAnimationFrame(() => { requestAnimationFrame(() => {
leftInstance = Split(["#left-pane", "#rest-pane"], { leftInstance = Split(["#left-pane", "#rest-pane"], {
sizes: [leftPaneWidth, 100 - leftPaneWidth], sizes: [leftPaneWidth, restPaneWidth],
gutterSize: DEFAULT_GUTTER_SIZE, gutterSize: DEFAULT_GUTTER_SIZE,
minSize: [150, 300], minSize: [150, 300],
onDragEnd: (sizes) => options.save("leftPaneWidth", Math.round(sizes[0])) onDragEnd: (sizes) => {
leftPaneWidth = Math.round(sizes[0]);
options.save("leftPaneWidth", Math.round(sizes[0]));
}
}); });
}); });
} }
@ -54,7 +65,7 @@ function setupRightPaneResizer() {
return; return;
} }
let rightPaneWidth = options.getInt("rightPaneWidth"); rightPaneWidth = rightPaneWidth ?? (options.getInt("rightPaneWidth") ?? 0);
if (!rightPaneWidth || rightPaneWidth < 5) { if (!rightPaneWidth || rightPaneWidth < 5) {
rightPaneWidth = 5; rightPaneWidth = 5;
} }
@ -63,8 +74,11 @@ function setupRightPaneResizer() {
rightInstance = Split(["#center-pane", "#right-pane"], { rightInstance = Split(["#center-pane", "#right-pane"], {
sizes: [100 - rightPaneWidth, rightPaneWidth], sizes: [100 - rightPaneWidth, rightPaneWidth],
gutterSize: DEFAULT_GUTTER_SIZE, gutterSize: DEFAULT_GUTTER_SIZE,
minSize: [ 300, 180 ], minSize: [300, 180],
onDragEnd: (sizes) => options.save("rightPaneWidth", Math.round(sizes[1])) onDragEnd: (sizes) => {
rightPaneWidth = Math.round(sizes[1]);
options.save("rightPaneWidth", Math.round(sizes[1]));
}
}); });
} }
} }

View File

@ -58,8 +58,11 @@ async function getWithSilentNotFound<T>(url: string, componentId?: string) {
return await call<T>("GET", url, componentId, { silentNotFound: true }); return await call<T>("GET", url, componentId, { silentNotFound: true });
} }
async function get<T>(url: string, componentId?: string) { /**
return await call<T>("GET", url, componentId); * @param raw if `true`, the value will be returned as a string instead of a JavaScript object if JSON, XMLDocument if XML, etc.
*/
async function get<T>(url: string, componentId?: string, raw?: boolean) {
return await call<T>("GET", url, componentId, { raw });
} }
async function post<T>(url: string, data?: unknown, componentId?: string) { async function post<T>(url: string, data?: unknown, componentId?: string) {
@ -102,6 +105,8 @@ let maxKnownEntityChangeId = 0;
interface CallOptions { interface CallOptions {
data?: unknown; data?: unknown;
silentNotFound?: boolean; silentNotFound?: boolean;
// If `true`, the value will be returned as a string instead of a JavaScript object if JSON, XMLDocument if XML, etc.
raw?: boolean;
} }
async function call<T>(method: string, url: string, componentId?: string, options: CallOptions = {}) { async function call<T>(method: string, url: string, componentId?: string, options: CallOptions = {}) {
@ -132,7 +137,7 @@ async function call<T>(method: string, url: string, componentId?: string, option
}); });
})) as any; })) as any;
} else { } else {
resp = await ajax(url, method, data, headers, !!options.silentNotFound); resp = await ajax(url, method, data, headers, !!options.silentNotFound, options.raw);
} }
const maxEntityChangeIdStr = resp.headers["trilium-max-entity-change-id"]; const maxEntityChangeIdStr = resp.headers["trilium-max-entity-change-id"];
@ -144,7 +149,10 @@ async function call<T>(method: string, url: string, componentId?: string, option
return resp.body as T; return resp.body as T;
} }
function ajax(url: string, method: string, data: unknown, headers: Headers, silentNotFound: boolean): Promise<Response> { /**
* @param raw if `true`, the value will be returned as a string instead of a JavaScript object if JSON, XMLDocument if XML, etc.
*/
function ajax(url: string, method: string, data: unknown, headers: Headers, silentNotFound: boolean, raw?: boolean): Promise<Response> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
const options: JQueryAjaxSettings = { const options: JQueryAjaxSettings = {
url: window.glob.baseApiUrl + url, url: window.glob.baseApiUrl + url,
@ -186,6 +194,10 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile
} }
}; };
if (raw) {
options.dataType = "text";
}
if (data) { if (data) {
try { try {
options.data = JSON.stringify(data); options.data = JSON.stringify(data);

View File

@ -1,28 +1,21 @@
import library_loader from "./library_loader.js"; import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes, type AutoHighlightResult, type HighlightResult, type Theme } from "@triliumnext/highlightjs";
import mime_types from "./mime_types.js"; import mime_types from "./mime_types.js";
import options from "./options.js"; import options from "./options.js";
import { t } from "./i18n.js";
import { copyText } from "./clipboard.js";
export function getStylesheetUrl(theme: string) { let highlightingLoaded = false;
if (!theme) {
return null;
}
const defaultPrefix = "default:";
if (theme.startsWith(defaultPrefix)) {
return `${window.glob.assetPath}/node_modules/@highlightjs/cdn-assets/styles/${theme.substr(defaultPrefix.length)}.min.css`;
}
return null;
}
/** /**
* Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks. * Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks.
* Additionally, adds a "Copy to clipboard" button.
* *
* @param $container the container under which to look for code blocks and to apply syntax highlighting to them. * @param $container the container under which to look for code blocks and to apply syntax highlighting to them.
*/ */
export async function applySyntaxHighlight($container: JQuery<HTMLElement>) { export async function formatCodeBlocks($container: JQuery<HTMLElement>) {
if (!isSyntaxHighlightEnabled()) { const syntaxHighlightingEnabled = isSyntaxHighlightEnabled();
return; if (syntaxHighlightingEnabled) {
await ensureMimeTypesForHighlighting();
} }
const codeBlocks = $container.find("pre code"); const codeBlocks = $container.find("pre code");
@ -32,10 +25,22 @@ export async function applySyntaxHighlight($container: JQuery<HTMLElement>) {
continue; continue;
} }
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType); applyCopyToClipboardButton($(codeBlock));
if (syntaxHighlightingEnabled) {
applySingleBlockSyntaxHighlight($(codeBlock), normalizedMimeType);
}
} }
} }
export function applyCopyToClipboardButton($codeBlock: JQuery<HTMLElement>) {
const $copyButton = $("<button>")
.addClass("bx component icon-action tn-tool-button bx-copy copy-button")
.attr("title", t("code_block.copy_title"))
.on("click", () => copyText($codeBlock.text()));
$codeBlock.parent().append($copyButton);
}
/** /**
* Applies syntax highlight to the given code block (assumed to be <pre><code>), using highlight.js. * Applies syntax highlight to the given code block (assumed to be <pre><code>), using highlight.js.
*/ */
@ -43,20 +48,13 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLEle
$codeBlock.parent().toggleClass("hljs"); $codeBlock.parent().toggleClass("hljs");
const text = $codeBlock.text(); const text = $codeBlock.text();
if (!window.hljs) { let highlightedText: HighlightResult | AutoHighlightResult | null = null;
await library_loader.requireLibrary(library_loader.HIGHLIGHT_JS);
}
let highlightedText = null;
if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) { if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) {
highlightedText = hljs.highlightAuto(text); await ensureMimeTypesForHighlighting();
highlightedText = highlightAuto(text);
} else if (normalizedMimeType) { } else if (normalizedMimeType) {
const language = mime_types.getHighlightJsNameForMime(normalizedMimeType); await ensureMimeTypesForHighlighting();
if (language) { highlightedText = highlight(text, { language: normalizedMimeType });
highlightedText = hljs.highlight(text, { language });
} else {
console.warn(`Unknown mime type: ${normalizedMimeType}.`);
}
} }
if (highlightedText) { if (highlightedText) {
@ -64,13 +62,42 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLEle
} }
} }
export async function ensureMimeTypesForHighlighting() {
if (highlightingLoaded) {
return;
}
// Load theme.
const currentThemeName = String(options.get("codeBlockTheme"));
loadHighlightingTheme(currentThemeName);
// Load mime types.
const mimeTypes = mime_types.getMimeTypes();
await ensureMimeTypes(mimeTypes);
highlightingLoaded = true;
}
export function loadHighlightingTheme(themeName: string) {
const themePrefix = "default:";
let theme: Theme | null = null;
if (themeName.includes(themePrefix)) {
theme = Themes[themeName.substring(themePrefix.length)];
}
if (!theme) {
theme = Themes.default;
}
loadTheme(theme);
}
/** /**
* Indicates whether syntax highlighting should be enabled for code blocks, by querying the value of the `codeblockTheme` option. * Indicates whether syntax highlighting should be enabled for code blocks, by querying the value of the `codeblockTheme` option.
* @returns whether syntax highlighting should be enabled for code blocks. * @returns whether syntax highlighting should be enabled for code blocks.
*/ */
export function isSyntaxHighlightEnabled() { export function isSyntaxHighlightEnabled() {
const theme = options.get("codeBlockTheme"); const theme = options.get("codeBlockTheme");
return theme && theme !== "none"; return !!theme && theme !== "none";
} }
/** /**

View File

@ -34,8 +34,8 @@ async function resolveNotePathToSegments(notePath: string, hoistedNoteId = "root
path.push("root"); path.push("root");
} }
const effectivePathSegments = []; const effectivePathSegments: string[] = [];
let childNoteId = null; let childNoteId: string | null = null;
let i = 0; let i = 0;
while (true) { while (true) {
@ -197,7 +197,7 @@ function getNotePath(node: Fancytree.FancytreeNode) {
return ""; return "";
} }
const path = []; const path: string[] = [];
while (node) { while (node) {
if (node.data.noteId) { if (node.data.noteId) {
@ -236,7 +236,7 @@ async function getNoteTitle(noteId: string, parentNoteId: string | null = null)
} }
async function getNotePathTitleComponents(notePath: string) { async function getNotePathTitleComponents(notePath: string) {
const titleComponents = []; const titleComponents: string[] = [];
if (notePath.startsWith("root/")) { if (notePath.startsWith("root/")) {
notePath = notePath.substr(5); notePath = notePath.substr(5);

View File

@ -51,7 +51,7 @@ function getMonthsInDateRange(startDate: string, endDate: string) {
const end = endDate.split("-"); const end = endDate.split("-");
const startYear = parseInt(start[0]); const startYear = parseInt(start[0]);
const endYear = parseInt(end[0]); const endYear = parseInt(end[0]);
const dates = []; const dates: string[] = [];
for (let i = startYear; i <= endYear; i++) { for (let i = startYear; i <= endYear; i++) {
const endMonth = i != endYear ? 11 : parseInt(end[1]) - 1; const endMonth = i != endYear ? 11 : parseInt(end[1]) - 1;
@ -84,7 +84,7 @@ function formatTimeInterval(ms: number) {
const hours = Math.floor(minutes / 60); const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24); const days = Math.floor(hours / 24);
const plural = (count: number, name: string) => `${count} ${name}${count > 1 ? "s" : ""}`; const plural = (count: number, name: string) => `${count} ${name}${count > 1 ? "s" : ""}`;
const segments = []; const segments: string[] = [];
if (days > 0) { if (days > 0) {
segments.push(plural(days, "day")); segments.push(plural(days, "day"));
@ -149,7 +149,7 @@ function isMac() {
export const hasTouchBar = (isMac() && isElectron()); export const hasTouchBar = (isMac() && isElectron());
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement>) { function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) {
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey); return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
} }

View File

@ -1,3 +1,5 @@
import "jquery";
import "jquery-hotkeys";
import utils from "./services/utils.js"; import utils from "./services/utils.js";
import ko from "knockout"; import ko from "knockout";
import "./stylesheets/bootstrap.scss"; import "./stylesheets/bootstrap.scss";

View File

@ -1,4 +1,5 @@
import "./stylesheets/bootstrap.scss"; import "normalize.css";
import "@triliumnext/ckeditor5/content.css";
/** /**
* Fetch note with given ID from backend * Fetch note with given ID from backend

View File

@ -272,4 +272,179 @@
justify-content: center; justify-content: center;
padding: 1rem; padding: 1rem;
color: var(--muted-text-color); color: var(--muted-text-color);
}
/* Thinking display styles */
.llm-thinking-container {
margin: 1rem 0;
animation: fadeInUp 0.3s ease-out;
}
.thinking-bubble {
background-color: var(--accented-background-color, var(--main-background-color));
border: 1px solid var(--subtle-border-color, var(--main-border-color));
border-radius: 0.75rem;
padding: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
transition: all 0.2s ease;
}
.thinking-bubble:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.thinking-bubble::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, var(--hover-item-background-color, rgba(0, 0, 0, 0.03)), transparent);
animation: shimmer 2s infinite;
opacity: 0.5;
}
.thinking-header {
cursor: pointer;
transition: all 0.2s ease;
border-radius: 0.375rem;
}
.thinking-header:hover {
background-color: var(--hover-item-background-color, rgba(0, 0, 0, 0.03));
padding: 0.25rem;
margin: -0.25rem;
}
.thinking-dots {
display: flex;
gap: 3px;
align-items: center;
}
.thinking-dots span {
width: 6px;
height: 6px;
background-color: var(--link-color, var(--hover-item-text-color));
border-radius: 50%;
animation: thinkingPulse 1.4s infinite ease-in-out;
}
.thinking-dots span:nth-child(1) {
animation-delay: -0.32s;
}
.thinking-dots span:nth-child(2) {
animation-delay: -0.16s;
}
.thinking-dots span:nth-child(3) {
animation-delay: 0s;
}
.thinking-label {
font-weight: 500;
color: var(--link-color, var(--hover-item-text-color)) !important;
}
.thinking-toggle {
color: var(--muted-text-color) !important;
transition: transform 0.2s ease;
background: transparent !important;
border: none !important;
}
.thinking-toggle:hover {
color: var(--main-text-color) !important;
}
.thinking-toggle.expanded {
transform: rotate(180deg);
}
.thinking-content {
margin-top: 0.75rem;
padding-top: 0.75rem;
border-top: 1px solid var(--subtle-border-color, var(--main-border-color));
animation: expandDown 0.3s ease-out;
}
.thinking-text {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
font-size: 0.875rem;
line-height: 1.5;
color: var(--main-text-color);
white-space: pre-wrap;
word-wrap: break-word;
background-color: var(--input-background-color);
padding: 0.75rem;
border-radius: 0.5rem;
border: 1px solid var(--subtle-border-color, var(--main-border-color));
max-height: 300px;
overflow-y: auto;
transition: border-color 0.2s ease;
}
.thinking-text:hover {
border-color: var(--main-border-color);
}
/* Animations */
@keyframes thinkingPulse {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.6;
}
40% {
transform: scale(1);
opacity: 1;
}
}
@keyframes shimmer {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes expandDown {
from {
opacity: 0;
max-height: 0;
}
to {
opacity: 1;
max-height: 300px;
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.thinking-bubble {
margin: 0.5rem 0;
padding: 0.5rem;
}
.thinking-text {
font-size: 0.8rem;
padding: 0.5rem;
max-height: 200px;
}
} }

View File

@ -15,6 +15,18 @@
src: url(../fonts/JetBrainsMono-Light.woff2) format("woff"); src: url(../fonts/JetBrainsMono-Light.woff2) format("woff");
} }
:root {
--admonition-note-accent-color: #69c7ff;
--admonition-tip-accent-color: #40c025;
--admonition-important-accent-color: #9839f7;
--admonition-caution-accent-color: #ff2e2e;
--admonition-warning-accent-color: #e2aa03;
--bs-body-font-family: var(--main-font-family) !important;
--bs-body-font-weight: var(--main-font-weight) !important;
--bs-body-color: var(--main-text-color) !important;
--bs-body-bg: var(--main-background-color) !important;
}
.table { .table {
--bs-table-bg: transparent !important; --bs-table-bg: transparent !important;
} }
@ -420,33 +432,24 @@ body.desktop #context-menu-container .dropdown-item > span {
width: 100%; width: 100%;
} }
.CodeMirror { .cm-editor {
height: 100%; height: 100%;
background: inherit; outline: none !important;
border-radius: 6px;
overflow: hidden;
margin: 4px;
} }
body .CodeMirror { body .cm-editor {
font-size: var(--monospace-font-size); font-size: var(--monospace-font-size);
} }
.CodeMirror-gutters { body .cm-editor .cm-gutters {
background-color: inherit !important; background-color: inherit !important;
border-right: none; border-right: none;
} }
.cm-matchhighlight { body .cm-editor .cm-placeholder {
background-color: #eeeeee;
}
.cm-matchhighlight.ck-find-result{
background: var(--ck-color-highlight-background);
}
.cm-matchhighlight.ck-find-result_selected {
background-color: #ff9633;
}
.CodeMirror pre.CodeMirror-placeholder {
color: #999 !important; color: #999 !important;
} }
@ -457,11 +460,11 @@ body .CodeMirror {
margin-bottom: 10px; margin-bottom: 10px;
} }
#sql-console-query .CodeMirror { #sql-console-query .cm-editor {
height: 150px; height: 150px;
} }
#sql-console-query .CodeMirror-scroll { #sql-console-query .cm-editor .cm-scroller {
min-height: inherit !important; min-height: inherit !important;
} }
@ -524,12 +527,40 @@ button.btn-sm {
padding: 0; padding: 0;
} }
pre:not(.CodeMirror-line):not(.hljs) { pre:not(.hljs) {
color: var(--main-text-color) !important; color: var(--main-text-color) !important;
white-space: pre-wrap; white-space: pre-wrap;
font-size: 100%; font-size: 100%;
} }
:root pre {
--padding-size: 1em;
--copy-button-margin-size: .35em;
--copy-button-width: 37px;
position: relative;
padding: var(--padding-size);
}
pre > button.copy-button {
position: absolute;
top: var(--copy-button-margin-size);
right: var(--copy-button-margin-size);
}
:root pre:has(> button.copy-button) {
padding-right: calc(var(--copy-button-width) + (var(--copy-button-margin-size) * 2));
}
pre > button.copy-button:hover {
color: inherit !important;
opacity: 1;
}
pre > button.copy-button:active {
background-color: unset !important;
}
.pointer { .pointer {
cursor: pointer; cursor: pointer;
} }
@ -599,11 +630,6 @@ table.promoted-attributes-in-tooltip th {
} }
.tooltip-trigger { .tooltip-trigger {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: transparent; background: transparent;
pointer-events: none; pointer-events: none;
} }
@ -1016,9 +1042,10 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
font-size: var(--detail-font-size) !important; font-size: var(--detail-font-size) !important;
} }
.ck-mentions .ck-button.ck-on { .ck-mentions {
background-color: var(--active-item-background-color) !important; --ck-color-list-button-on-background: var(--active-item-background-color);
color: var(--active-item-text-color) !important; --ck-color-list-button-on-background-focus: var(--ck-color-list-button-on-background);
--ck-color-list-button-on-text: var(--active-item-text-color);
} }
.ck-mentions .ck-button b { .ck-mentions .ck-button b {
@ -1222,6 +1249,10 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
background-color: inherit; background-color: inherit;
} }
::selection {
background-color: var(--selection-background-color);
}
[data-bs-toggle="tooltip"]:not(.button-widget) span { [data-bs-toggle="tooltip"]:not(.button-widget) span {
padding-bottom: 0; padding-bottom: 0;
border-bottom: 1px dotted; border-bottom: 1px dotted;
@ -1778,7 +1809,7 @@ body.zen .title-row {
height: unset !important; height: unset !important;
-webkit-app-region: drag; -webkit-app-region: drag;
padding-left: env(titlebar-area-x); padding-left: env(titlebar-area-x);
padding-right: 2.5em; padding-right: calc(100vw - env(titlebar-area-width, 100vw) + 2.5em);
} }
body.zen .floating-buttons { body.zen .floating-buttons {
@ -2018,11 +2049,11 @@ footer.file-footer button {
left: 1em; left: 1em;
} }
.admonition.note { --accent-color: #69c7ff; } .admonition.note { --accent-color: var(--admonition-note-accent-color); }
.admonition.tip { --accent-color: #40c025; } .admonition.tip { --accent-color: var(--admonition-tip-accent-color); }
.admonition.important { --accent-color: #9839f7; } .admonition.important { --accent-color: var(--admonition-important-accent-color); }
.admonition.caution { --accent-color: #ff2e2e; } .admonition.caution { --accent-color: var(--admonition-caution-accent-color); }
.admonition.warning { --accent-color: #e2aa03; } .admonition.warning { --accent-color: var(--admonition-warning-accent-color); }
.admonition.note::before { content: "\eb21"; } .admonition.note::before { content: "\eb21"; }
.admonition.tip::before { content: "\ea0d"; } .admonition.tip::before { content: "\ea0d"; }

View File

@ -81,10 +81,6 @@ body ::-webkit-calendar-picker-indicator {
filter: invert(1); filter: invert(1);
} }
body .CodeMirror {
filter: invert(90%) hue-rotate(180deg);
}
.excalidraw.theme--dark { .excalidraw.theme--dark {
--theme-filter: invert(80%) hue-rotate(180deg) !important; --theme-filter: invert(80%) hue-rotate(180deg) !important;
} }

View File

@ -5,6 +5,12 @@
* Color scheme * Color scheme
*/ */
:root { :root {
/*
* NOTICE: This theme is currently in the beta stage of development.
* The names and purposes of these CSS variables are subject to frequent changes.
*/
--theme-style: dark; --theme-style: dark;
--native-titlebar-background: #00000000; --native-titlebar-background: #00000000;
@ -89,6 +95,7 @@
--menu-item-arrow-color: #ffffffa3; --menu-item-arrow-color: #ffffffa3;
--menu-item-delimiter-color: #ffffff1c; --menu-item-delimiter-color: #ffffff1c;
--menu-item-group-header-color: #ffffff91; --menu-item-group-header-color: #ffffff91;
--menu-section-background-color: #fefefe08;
--modal-backdrop-color: #000; --modal-backdrop-color: #000;
--modal-shadow-color: rgba(0, 0, 0, .5); --modal-shadow-color: rgba(0, 0, 0, .5);
@ -195,6 +202,8 @@
--scrollbar-background-color: transparent; --scrollbar-background-color: transparent;
--scrollbar-border-color: unset; /* Deprecated */ --scrollbar-border-color: unset; /* Deprecated */
--selection-background-color: #3399FF70;
--link-color: lightskyblue; --link-color: lightskyblue;
--mermaid-theme: dark; --mermaid-theme: dark;
@ -234,6 +243,11 @@
--help-code-background: #565656; --help-code-background: #565656;
--ck-editor-popup-border-color: var(--modal-border-color); --ck-editor-popup-border-color: var(--modal-border-color);
--ck-editor-toolbar-button-on-background: #ffffff3b;
--ck-editor-toolbar-button-on-color: white;
--ck-editor-toolbar-button-on-shadow: 1px 1px 2px rgba(0, 0, 0, .75);
--ck-editor-toolbar-dropdown-button-open-background: #ffffff14;
} }
/* /*
@ -244,10 +258,6 @@ body ::-webkit-calendar-picker-indicator {
filter: invert(1); filter: invert(1);
} }
body .CodeMirror {
filter: invert(90%) hue-rotate(180deg);
}
.excalidraw.theme--dark { .excalidraw.theme--dark {
--theme-filter: invert(80%) hue-rotate(180deg) !important; --theme-filter: invert(80%) hue-rotate(180deg) !important;
} }

View File

@ -5,6 +5,12 @@
* Color scheme * Color scheme
*/ */
:root { :root {
/*
* NOTICE: This theme is currently in the beta stage of development.
* The names and purposes of these CSS variables are subject to frequent changes.
*/
--theme-style: light; --theme-style: light;
--native-titlebar-background: #ffffff00; --native-titlebar-background: #ffffff00;
@ -83,6 +89,7 @@
--menu-item-arrow-color: #00000080; --menu-item-arrow-color: #00000080;
--menu-item-delimiter-color: #00000030; --menu-item-delimiter-color: #00000030;
--menu-item-group-header-color: #00000061; --menu-item-group-header-color: #00000061;
--menu-section-background-color: #00000006;
--modal-backdrop-color: #7c7c7c; --modal-backdrop-color: #7c7c7c;
--modal-shadow-color: #00000033; --modal-shadow-color: #00000033;
@ -194,6 +201,8 @@
--scrollbar-background-color: transparent; --scrollbar-background-color: transparent;
--scrollbar-border-color: unset; /* Deprecated */ --scrollbar-border-color: unset; /* Deprecated */
--selection-background-color: #3399FF70;
--link-color: blue; --link-color: blue;
--mermaid-theme: default; --mermaid-theme: default;
@ -234,4 +243,9 @@
--help-code-background: #d7d5d5; --help-code-background: #d7d5d5;
--ck-editor-popup-border-color: var(--dropdown-border-color); --ck-editor-popup-border-color: var(--dropdown-border-color);
--ck-editor-toolbar-button-on-background: #00000030;
--ck-editor-toolbar-button-on-color: black;
--ck-editor-toolbar-button-on-shadow: none;
--ck-editor-toolbar-dropdown-button-open-background: #0000000f;
} }

View File

@ -110,7 +110,8 @@ body.mobile .dropdown-menu .dropdown-menu {
border-radius: unset !important; border-radius: unset !important;
} }
body.desktop .dropdown-menu::before { body.desktop .dropdown-menu::before,
:root .ck.ck-dropdown__panel::before {
content: ""; content: "";
backdrop-filter: var(--dropdown-backdrop-filter); backdrop-filter: var(--dropdown-backdrop-filter);
border-radius: var(--dropdown-border-radius); border-radius: var(--dropdown-border-radius);

View File

@ -79,7 +79,7 @@ button.btn.btn-success kbd {
*/ */
:root .icon-action:not(.global-menu-button), :root .icon-action:not(.global-menu-button),
:root .tn-tool-button, :root .btn.tn-tool-button,
:root .btn-group .tn-tool-button:not(:last-child), :root .btn-group .tn-tool-button:not(:last-child),
:root .btn-group .tn-tool-button:last-child { :root .btn-group .tn-tool-button:last-child {
width: var(--icon-button-size); width: var(--icon-button-size);
@ -110,7 +110,7 @@ button.btn.btn-success kbd {
:root .icon-action:not(.global-menu-button)::before, :root .icon-action:not(.global-menu-button)::before,
:root .tn-tool-button::before { :root .tn-tool-button::before {
display: block; display: block;
line-height: var(--icon-button-size); line-height: 1;
font-size: calc(var(--icon-button-size) * var(--icon-button-icon-ratio)); font-size: calc(var(--icon-button-size) * var(--icon-button-icon-ratio));
} }

View File

@ -10,7 +10,8 @@
* Toolbar * Toolbar
*/ */
.ck.ck-toolbar { .ck.ck-toolbar,
.ck.ck-block-toolbar-button {
--ck-color-toolbar-background: transparent; --ck-color-toolbar-background: transparent;
--ck-color-button-default-background: transparent; --ck-color-button-default-background: transparent;
@ -19,19 +20,62 @@
--ck-color-button-on-background: transparent; --ck-color-button-on-background: transparent;
--ck-color-button-on-hover-background: var(--hover-item-background-color); --ck-color-button-on-hover-background: var(--hover-item-background-color);
--ck-color-button-default-active-background: var(--hover-item-background-color);
--ck-focus-ring: 1px solid var(--input-focus-outline-color); --ck-color-split-button-hover-background: var(--ck-editor-toolbar-dropdown-button-open-background);
--ck-focus-ring: 1px solid transparent;
--ck-color-focus-border: var(--input-focus-outline-color); --ck-color-focus-border: var(--input-focus-outline-color);
--ck-focus-outer-shadow: none; --ck-focus-outer-shadow: none;
--ck-focus-disabled-outer-shadow: none;
--ck-border-radius: 6px; --ck-border-radius: 6px;
} }
/* Toolbar button in on state */
.ck.ck-toolbar .ck-button.ck-on:not(.ck-dropdown__button):not(.ck-list-item-button):not(.ck-button_with-text) {
--ck-color-button-on-background: var(--ck-editor-toolbar-button-on-background);
--ck-color-button-on-color: var(--ck-editor-toolbar-button-on-color);
box-shadow: var(--ck-editor-toolbar-button-on-shadow);
}
/* Toolbar button with its dropdown open */
.ck.ck-toolbar .ck-button.ck-dropdown__button {
--ck-color-button-on-background: var(--ck-editor-toolbar-dropdown-button-open-background);
--ck-color-button-on-color: currentColor;
}
/* The toolbar show / hide button for the current text block */
.ck.ck-block-toolbar-button {
--ck-color-button-on-background: transparent;
--ck-color-button-on-color: currentColor;
}
:root .ck.ck-toolbar .ck-button:not(.ck-disabled):active,
.ck.ck-block-toolbar-button:active {
background-color: var(--hover-item-background-color);
}
.ck.ck-toolbar .ck-button:active:not(.ck-list-item-button):not(.ck-button_with-text):not(.ck-disabled) svg:not(.ck-dropdown__arrow),
.ck.ck-block-toolbar-button:active svg {
transform: scale(.8);
}
/* Disabled button */ /* Disabled button */
:root .classic-toolbar-widget .ck.ck-button.ck-disabled { :root .classic-toolbar-widget .ck.ck-button.ck-disabled {
opacity: .75; opacity: .75;
} }
/* Focus visible */
.ck.ck-toolbar .ck-button:focus-visible {
--ck-focus-ring: 1px solid var(--input-focus-outline-color);
}
/* Remove the border from hovered split arrow button */
.ck.ck-splitbutton:hover > .ck-splitbutton__arrow:not(.ck-disabled)::after {
visibility: collapse;
}
/* /*
* Dropdowns * Dropdowns
*/ */
@ -49,16 +93,61 @@
border: 1px solid var(--ck-editor-popup-border-color) !important; border: 1px solid var(--ck-editor-popup-border-color) !important;
border-radius: var(--dropdown-border-radius) !important; border-radius: var(--dropdown-border-radius) !important;
background: var(--menu-background-color) !important; background: var(--menu-background-color) !important;
backdrop-filter: var(--dropdown-backdrop-filter);
padding: var(--ck-editor-popup-padding); padding: var(--ck-editor-popup-padding);
} }
/*
* Backdrop blur pseudo-element
*
* Since .ck-balloon-panel already uses the :after and :before pseudo-elements, there is no other
* option than using a :before on the children element to apply the backdrop blur.
* This pseudoelement will overflow and cover the entire surface of .ck-balloon-panel.
*/
:root .ck-balloon-panel > .ck-toolbar,
:root .ck-balloon-panel > .ck-balloon-rotator {
position: relative;
}
:root .ck-balloon-panel > .ck-toolbar::before,
:root .ck-balloon-panel > .ck-balloon-rotator::before {
--negative-padding: calc(0px - var(--ck-editor-popup-padding)); /* Compensate the parent's padding */
content: "";
position: absolute;
top: var(--negative-padding);
right: var(--negative-padding);
bottom: var(--negative-padding);
left: var(--negative-padding);
border-radius: var(--dropdown-border-radius);
backdrop-filter: var(--dropdown-backdrop-filter);
z-index: -1;
}
:root .ck.ck-dropdown__panel {
--ck-editor-popup-padding: var(--menu-padding-size);
}
/* Dropdown panel containing a toolbar */
:root .ck.ck-dropdown__panel:has(>.ck-toolbar) {
--ck-editor-popup-padding: calc(var(--menu-padding-size) - var(--ck-spacing-small));
}
/* Bulleted / number list toolbar */
.ck-list-styles-list {
--ck-spacing-large: var(--ck-spacing-small);
}
:root ul.ck.ck-list, :root ul.ck.ck-list,
:root div.ck.ck-balloon-panel:not(.ck-tooltip) { :root div.ck.ck-balloon-panel:not(.ck-tooltip) {
border: none; border: none;
background: transparent; background: transparent;
} }
:root .ck.ck-list {
padding: 0;
}
/* Tooltip */ /* Tooltip */
:root div.ck.ck-balloon-panel.ck-tooltip { :root div.ck.ck-balloon-panel.ck-tooltip {
--ck-color-panel-background: var(--toast-background); /* Arrow */ --ck-color-panel-background: var(--toast-background); /* Arrow */
@ -73,24 +162,45 @@
} }
/* Dropdown list item */ /* Dropdown list item */
:root ul.ck.ck-list button.ck-button { :root ul.ck.ck-list button.ck-button,
:root .ck.ck-collapsible > button.ck-button {
padding: 2px 16px; padding: 2px 16px;
background: transparent; background: transparent;
border-radius: 6px !important; border-radius: 6px !important;
box-shadow: unset; box-shadow: unset;
} }
/* Checked list item */ :root .ck.ck-list__item {
:root ul.ck.ck-list button.ck-button.ck-on:not(:hover) { min-width: 10em;
background: transparent !important;
} }
/* Item with icon */
:root .ck.ck-button_with-text svg:first-child {
color: var(--menu-item-icon-color);
}
/* Checked list item */
:root ul.ck.ck-list button.ck-button:hover, :root ul.ck.ck-list button.ck-button:hover,
:root ul.ck.ck-list button.ck-button.ck-on:hover { :root ul.ck.ck-list button.ck-button.ck-on:hover,
:root .ck.ck-collapsible > button.ck-button:not(.ck-disabled):hover,
:root .ck.ck-collapsible > button.ck-button:not(.ck-disabled):not(:focus):hover {
background: var(--hover-item-background-color); background: var(--hover-item-background-color);
color: var(--hover-item-color); color: var(--hover-item-color);
} }
/* List item checkmark container */
:root .ck.ck-list-item-button .ck-list-item-button__check-holder {
margin-inline-start: var(--ck-spacing-small);
margin-inline-end: var(--menu-padding-size);
}
:root .ck.ck-list-item-button .ck-list-item-button__check-holder svg {
transform: scale(1.2);
color: var(--menu-item-icon-color);
}
/* Separator */ /* Separator */
:root .ck .ck-list__separator { :root .ck .ck-list__separator {
margin: .5em 0; margin: .5em 0;
@ -99,8 +209,82 @@
background: var(--menu-item-delimiter-color); background: var(--menu-item-delimiter-color);
} }
/* Collapsible section */
.ck.ck-collapsible {
position: relative;
border: unset !important;
padding-top: var(--ck-editor-popup-padding);
}
.ck.ck-collapsible::before {
/* Adds a background shade which overlaps the dropdown's padding */
--negative-padding: calc(0px - var(--ck-editor-popup-padding));
display: block;
content: "";
position: absolute;
top: 0;
bottom: var(--negative-padding);
left: var(--negative-padding);
right: var(--negative-padding);
border-top: 1px solid var(--ck-editor-popup-border-color);
background: var(--menu-section-background-color);
}
.ck.ck-collapsible:last-child::before {
border-radius: 0 0 var(--dropdown-border-radius) var(--dropdown-border-radius);
}
.ck.ck-collapsible.ck-collapsible_collapsed > button.ck-button {
font-weight: normal !important;
}
.ck.ck-collapsible .ck-collapsible__children {
padding-top: 1em;
}
/* Toolbar separators */
:root .ck.ck-toolbar .ck.ck-toolbar__separator {
background: transparent;
border-left: 1px solid var(--ck-color-toolbar-border);
}
/* The last separator of the toolbar */
:root .classic-toolbar-widget .ck.ck-toolbar__separator:last-of-type {
flex-grow: 1;
}
/* Heading dropdown */
:root .ck.ck-dropdown.ck-heading-dropdown .ck-dropdown__panel .ck-list__item {
min-width: 170px;
}
/* Font size dropdown */
.ck-fontsize-option {
min-height: 2rem !important;
}
.ck-fontsize-option.text-tiny {--size: .75em;}
.ck-fontsize-option.text-small {--size: .85em;}
.ck-fontsize-option.text-big {--size: 1.4em;}
.ck-fontsize-option.text-huge {--size: 1.8em;}
:root .ck-fontsize-option .ck-button__label {
font-size: var(--size);
}
/* Color picker dropdown */ /* Color picker dropdown */
/* Color palette */
:root .ck.ck-color-selector .ck-color-grid {
--ck-editor-toolbar-button-on-shadow: none; /* Remove the shadow of the selected color button */
}
/* Color picker button */ /* Color picker button */
:root .ck.ck-color-selector .ck-color-grids-fragment .ck-button.ck-color-selector__color-picker { :root .ck.ck-color-selector .ck-color-grids-fragment .ck-button.ck-color-selector__color-picker {
--ck-color-base-border: transparent; /* Remove the top border */ --ck-color-base-border: transparent; /* Remove the top border */
@ -109,13 +293,69 @@
border-bottom-right-radius: var(--ck-border-radius); border-bottom-right-radius: var(--ck-border-radius);
} }
/* Current color checkmark */
:root .ck.ck-color-selector .ck-color-grid .ck-icon {
color: white;
}
:root .ck.ck-color-selector .ck-color-grid .ck-icon__fill {
fill: black !important;
}
/* Numbered list */
:root .ck.ck-list-properties_with-numbered-properties .ck.ck-list-styles-list {
min-width: 200px;
grid-template-columns: repeat(3, auto);
justify-content: space-between;
padding-bottom: calc(var(--ck-spacing-medium) + var(--menu-padding-size));
}
/* Table dropdown */ /* Table dropdown */
/* Table rows / columns button grid container */
.ck-insert-table-dropdown__grid { .ck-insert-table-dropdown__grid {
--ck-insert-table-dropdown-box-width: 16px;
--ck-insert-table-dropdown-box-height: 16px;
--ck-insert-table-dropdown-box-margin: 2px;
--ck-color-base-border: var(--ck-color-panel-border); /* Cell box color */ --ck-color-base-border: var(--ck-color-panel-border); /* Cell box color */
--ck-color-focus-border: var(--hover-item-text-color); /* Selected cell box border color */ --ck-color-focus-border: var(--hover-item-text-color); /* Selected cell box border color */
--ck-color-focus-outer-shadow: var(--hover-item-background-color); /* Selected cell box background color */ --ck-color-focus-outer-shadow: var(--hover-item-background-color); /* Selected cell box background color */
--ck-border-radius: 0; --ck-border-radius: 0;
--ck-editor-toolbar-button-on-shadow: 1px 1px 1px rgba(0, 0, 0, .35);
}
/* Selected rows / column counter */
.ck.ck-insert-table-dropdown__label {
margin-top: var(--ck-spacing-medium);
}
/* Admonitions dropdown */
.ck-tn-admonition-note { --icon: "\eb21"; --accent: var(--admonition-note-accent-color);}
.ck-tn-admonition-tip { --icon: "\ea0d"; --accent: var(--admonition-tip-accent-color);}
.ck-tn-admonition-important { --icon: "\ea7c"; --accent: var(--admonition-important-accent-color);}
.ck-tn-admonition-caution { --icon: "\eac7"; --accent: var(--admonition-caution-accent-color);}
.ck-tn-admonition-warning { --icon: "\eac5"; --accent: var(--admonition-warning-accent-color);}
:root .ck.ck-tn-admonition-option .ck-button__label {
display: inline-flex;
align-items: center;
width: 100%;
margin: 4px;
padding-right: 2em;
border: 1px solid var(--accent);
border-radius: 6px;
}
:root .ck.ck-tn-admonition-option .ck-button__label::before {
display: inline-block;
content: var(--icon);
width: 2em;
text-align: center;
font-size: 1.4em;
font-family: boxicons;
color: var(--accent);
} }
/* Action buttons */ /* Action buttons */
@ -133,6 +373,20 @@
background: var(--hover-item-background-color); background: var(--hover-item-background-color);
} }
/* Mention list (the autocompletion list for emojis, labels and relations) */
:root .ck-mentions {
--ck-color-list-button-on-background: var(--hover-item-background-color);
--ck-color-list-button-on-text: var(--hover-item-text-color);
--ck-color-list-button-hover-background: var(--ck-editor-toolbar-dropdown-button-open-background);
--ck-focus-ring: 1px solid transparent;
}
/* "Keep on typing to see the emoji" placeholder */
#mention-list-item-id\:__EMOJI_HINT {
background: transparent;
}
/* /*
* EDITOR'S CONTENT * EDITOR'S CONTENT
*/ */
@ -142,19 +396,39 @@
*/ */
.attachment-content-wrapper pre, .attachment-content-wrapper pre,
.ck-content pre, :root .ck-content pre,
.ck-mermaid__editing-view { .ck-mermaid__editing-view {
border: 0; border: 0;
border-radius: 6px; border-radius: 6px;
box-shadow: var(--code-block-box-shadow); box-shadow: var(--code-block-box-shadow);
padding: 0 !important; padding: 0;
margin-top: 2px !important; margin-top: 2px !important;
} }
.ck-content pre { :root .ck-content pre {
--icon-button-size: 1.8em;
--copy-button-width: var(--icon-button-size);
/* The margin of the copy button is computed so the button will appear vertically centered
* for single-line code blocks */
--copy-button-margin-size: calc((1em * 1.5 + var(--padding-size) * 2 - var(--icon-button-size)) / 2);
/* Where: Line height
* Font size
*/
overflow: unset; overflow: unset;
} }
pre button.copy-button.icon-action {
font-size: 1em; /* Workaround: --icon-button-size does not work properly with em units */
}
:root pre:has(> button.copy-button) {
padding-right: calc(var(--icon-button-size) + (var(--copy-button-margin-size) * 2));
}
html .note-detail-editable-text :not(figure, .include-note, hr):first-child { html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
/* Create some space for the top-side shadow */ /* Create some space for the top-side shadow */
margin-top: 1px !important; margin-top: 1px !important;
@ -170,10 +444,10 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
} }
.attachment-content-wrapper pre code, .attachment-content-wrapper pre code,
.ck-content pre code, :root .ck-content pre code,
.ck-mermaid__editing-view { .ck-mermaid__editing-view {
display: block; display: block;
padding: 1em !important; padding: var(--padding-size, 1em);
overflow: auto; overflow: auto;
} }
@ -245,7 +519,6 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
.note-detail-printable:not(.word-wrap) pre code { .note-detail-printable:not(.word-wrap) pre code {
white-space: pre; white-space: pre;
margin-right: 1em;
} }
.code-sample-wrapper .hljs { .code-sample-wrapper .hljs {

View File

@ -108,6 +108,25 @@ div.editability-dropdown a.dropdown-item {
font-size: 0.85em; font-size: 0.85em;
} }
/*
* Edited notes (for calendar notes)
*/
/* The path of the note */
.edited-notes-list small {
margin-inline-start: 4px;
font-size: inherit;
color: var(--muted-text-color);
}
.edited-notes-list small::before {
content: "(";
}
.edited-notes-list small::after {
content: ")";
}
/* /*
* Owned attributes * Owned attributes
*/ */

View File

@ -127,10 +127,12 @@ body.layout-horizontal > .horizontal {
--launcher-pane-button-gap: var(--launcher-pane-vert-button-gap); --launcher-pane-button-gap: var(--launcher-pane-vert-button-gap);
width: var(--launcher-pane-size) !important; width: var(--launcher-pane-size) !important;
min-width: var(--launcher-pane-size) !important;
padding-bottom: var(--launcher-pane-button-gap); padding-bottom: var(--launcher-pane-button-gap);
} }
#launcher-pane.vertical #launcher-container { #launcher-pane.vertical #launcher-container {
width: var(--launcher-pane-size);
height: 100%; height: 100%;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
@ -1400,6 +1402,7 @@ div.floating-buttons .show-floating-buttons-button:active {
div.floating-buttons-children .close-floating-buttons-button::before, div.floating-buttons-children .close-floating-buttons-button::before,
div.floating-buttons .show-floating-buttons-button::before { div.floating-buttons .show-floating-buttons-button::before {
display: block; display: block;
line-height: 1;
} }
/* "Show buttons" button */ /* "Show buttons" button */
@ -1636,7 +1639,9 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
#right-pane .toc li, #right-pane .toc li,
#right-pane .highlights-list li { #right-pane .highlights-list li {
padding: 2px 8px; padding-top: 2px;
padding-right: 8px;
padding-bottom: 2px;
border-radius: 4px; border-radius: 4px;
text-align: unset; text-align: unset;
transition: transition:

View File

@ -28,7 +28,7 @@ interface NoteDefinition extends AttributeDefinitions, RelationDefinitions {
* ]); * ]);
*/ */
export function buildNotes(notes: NoteDefinition[]) { export function buildNotes(notes: NoteDefinition[]) {
const ids = []; const ids: string[] = [];
for (const noteDef of notes) { for (const noteDef of notes) {
ids.push(buildNote(noteDef).noteId); ids.push(buildNote(noteDef).noteId);

View File

@ -274,15 +274,15 @@
"revision_last_edited": "此修订版本上次编辑于 {{date}}", "revision_last_edited": "此修订版本上次编辑于 {{date}}",
"confirm_delete_all": "您是否要删除此笔记的所有修订版本?", "confirm_delete_all": "您是否要删除此笔记的所有修订版本?",
"no_revisions": "此笔记暂无修订版本...", "no_revisions": "此笔记暂无修订版本...",
"restore_button": "", "restore_button": "恢复",
"confirm_restore": "您是否要恢复此修订版本?这将使用此修订版本覆盖笔记的当前标题和内容。", "confirm_restore": "您是否要恢复此修订版本?这将使用此修订版本覆盖笔记的当前标题和内容。",
"delete_button": "", "delete_button": "删除",
"confirm_delete": "您是否要删除此修订版本?", "confirm_delete": "您是否要删除此修订版本?",
"revisions_deleted": "笔记修订版本已删除。", "revisions_deleted": "笔记修订版本已删除。",
"revision_restored": "笔记修订版本已恢复。", "revision_restored": "笔记修订版本已恢复。",
"revision_deleted": "笔记修订版本已删除。", "revision_deleted": "笔记修订版本已删除。",
"snapshot_interval": "笔记快照保存间隔: {{seconds}}秒。", "snapshot_interval": "笔记快照保存间隔: {{seconds}}秒。",
"maximum_revisions": "当前笔记的最历史数量: {{number}}。", "maximum_revisions": "当前笔记的最历史数量: {{number}}。",
"settings": "笔记修订设置", "settings": "笔记修订设置",
"download_button": "下载", "download_button": "下载",
"mime": "MIME 类型:", "mime": "MIME 类型:",
@ -806,7 +806,7 @@
"open_full": "展开显示", "open_full": "展开显示",
"collapse": "折叠到正常大小", "collapse": "折叠到正常大小",
"title": "笔记地图", "title": "笔记地图",
"fix-nodes": "修复节点", "fix-nodes": "固定节点",
"link-distance": "链接距离" "link-distance": "链接距离"
}, },
"note_paths": { "note_paths": {
@ -1213,7 +1213,7 @@
"color": "字体颜色", "color": "字体颜色",
"bg_color": "背景颜色", "bg_color": "背景颜色",
"visibility_title": "高亮列表可见性", "visibility_title": "高亮列表可见性",
"visibility_description": "您可以通过添加 #hideHighlightWidget 标签来隐藏个笔记的高亮小部件。", "visibility_description": "您可以通过添加 #hideHighlightWidget 标签来隐藏个笔记的高亮小部件。",
"shortcut_info": "您可以在选项 -> 快捷键中为快速切换右侧面板(包括高亮列表)配置键盘快捷键(名称为 'toggleRightPane')。" "shortcut_info": "您可以在选项 -> 快捷键中为快速切换右侧面板(包括高亮列表)配置键盘快捷键(名称为 'toggleRightPane')。"
}, },
"table_of_contents": { "table_of_contents": {
@ -1547,7 +1547,7 @@
"close_other_tabs": "关闭其他标签页", "close_other_tabs": "关闭其他标签页",
"close_right_tabs": "关闭右侧标签页", "close_right_tabs": "关闭右侧标签页",
"close_all_tabs": "关闭所有标签页", "close_all_tabs": "关闭所有标签页",
"reopen_last_tab": "重新打开最后一个关闭的标签页", "reopen_last_tab": "重新打开关闭的标签页",
"move_tab_to_new_window": "将此标签页移动到新窗口", "move_tab_to_new_window": "将此标签页移动到新窗口",
"copy_tab_to_new_window": "将此标签页复制到新窗口", "copy_tab_to_new_window": "将此标签页复制到新窗口",
"new_tab": "新标签页" "new_tab": "新标签页"
@ -1616,29 +1616,32 @@
"auto-detect-language": "自动检测" "auto-detect-language": "自动检测"
}, },
"highlighting": { "highlighting": {
"title": "文本笔记的代码语法高亮", "title": "代码块",
"description": "控制文本笔记中代码块的语法高亮,代码笔记不会受到影响。", "description": "控制文本笔记中代码块的语法高亮,代码笔记不会受到影响。",
"color-scheme": "颜色方案" "color-scheme": "颜色方案"
}, },
"code_block": { "code_block": {
"word_wrapping": "自动换行" "word_wrapping": "自动换行",
"theme_none": "无语法高亮",
"theme_group_light": "浅色主题",
"theme_group_dark": "深色主题"
}, },
"classic_editor_toolbar": { "classic_editor_toolbar": {
"title": "格式化" "title": "格式"
}, },
"editor": { "editor": {
"title": "编辑器" "title": "编辑器"
}, },
"editing": { "editing": {
"editor_type": { "editor_type": {
"label": "格式工具栏", "label": "格式工具栏",
"floating": { "floating": {
"title": "浮动", "title": "浮动",
"description": "编辑工具出现在光标附近;" "description": "编辑工具出现在光标附近;"
}, },
"fixed": { "fixed": {
"title": "固定", "title": "固定",
"description": "编辑工具出现在 \"格式\" 功能区标签中。" "description": "编辑工具出现在 \"格式\" 功能区标签中。"
}, },
"multiline-toolbar": "如果工具栏无法完全显示,则分多行显示。" "multiline-toolbar": "如果工具栏无法完全显示,则分多行显示。"
} }

View File

@ -1568,12 +1568,15 @@
"auto-detect-language": "Automatisch erkannt" "auto-detect-language": "Automatisch erkannt"
}, },
"highlighting": { "highlighting": {
"title": "Code-Syntax-Hervorhebung für Textnotizen", "title": "",
"description": "Steuert die Syntaxhervorhebung für Codeblöcke in Textnotizen, Code-Notizen sind nicht betroffen.", "description": "Steuert die Syntaxhervorhebung für Codeblöcke in Textnotizen, Code-Notizen sind nicht betroffen.",
"color-scheme": "Farbschema" "color-scheme": "Farbschema"
}, },
"code_block": { "code_block": {
"word_wrapping": "Wortumbruch" "word_wrapping": "Wortumbruch",
"theme_none": "Keine Syntax-Hervorhebung",
"theme_group_light": "Helle Themen",
"theme_group_dark": "Dunkle Themen"
}, },
"classic_editor_toolbar": { "classic_editor_toolbar": {
"title": "Format" "title": "Format"

View File

@ -1247,13 +1247,11 @@
"reprocessing_embeddings": "Reprocessing...", "reprocessing_embeddings": "Reprocessing...",
"reprocess_started": "Embedding reprocessing started in the background", "reprocess_started": "Embedding reprocessing started in the background",
"reprocess_error": "Error starting embedding reprocessing", "reprocess_error": "Error starting embedding reprocessing",
"reprocess_index": "Rebuild Search Index", "reprocess_index": "Rebuild Search Index",
"reprocess_index_description": "Optimize the search index for better performance. This uses existing embeddings without regenerating them (much faster than reprocessing all embeddings).", "reprocess_index_description": "Optimize the search index for better performance. This uses existing embeddings without regenerating them (much faster than reprocessing all embeddings).",
"reprocessing_index": "Rebuilding...", "reprocessing_index": "Rebuilding...",
"reprocess_index_started": "Search index optimization started in the background", "reprocess_index_started": "Search index optimization started in the background",
"reprocess_index_error": "Error rebuilding search index", "reprocess_index_error": "Error rebuilding search index",
"index_rebuild_progress": "Index Rebuild Progress", "index_rebuild_progress": "Index Rebuild Progress",
"index_rebuilding": "Optimizing index ({{percentage}}%)", "index_rebuilding": "Optimizing index ({{percentage}}%)",
"index_rebuild_complete": "Index optimization complete", "index_rebuild_complete": "Index optimization complete",
@ -1781,7 +1779,9 @@
}, },
"clipboard": { "clipboard": {
"cut": "Note(s) have been cut into clipboard.", "cut": "Note(s) have been cut into clipboard.",
"copied": "Note(s) have been copied into clipboard." "copied": "Note(s) have been copied into clipboard.",
"copy_failed": "Cannot copy to clipboard due to permission issues.",
"copy_success": "Copied to clipboard."
}, },
"entrypoints": { "entrypoints": {
"note-revision-created": "Note revision has been created.", "note-revision-created": "Note revision has been created.",
@ -1824,12 +1824,16 @@
"auto-detect-language": "Auto-detected" "auto-detect-language": "Auto-detected"
}, },
"highlighting": { "highlighting": {
"title": "Code Syntax Highlighting for Text Notes", "title": "Code Blocks",
"description": "Controls the syntax highlighting for code blocks inside text notes, code notes will not be affected.", "description": "Controls the syntax highlighting for code blocks inside text notes, code notes will not be affected.",
"color-scheme": "Color Scheme" "color-scheme": "Color Scheme"
}, },
"code_block": { "code_block": {
"word_wrapping": "Word wrapping" "word_wrapping": "Word wrapping",
"theme_none": "No syntax highlighting",
"theme_group_light": "Light themes",
"theme_group_dark": "Dark themes",
"copy_title": "Copy to clipboard"
}, },
"classic_editor_toolbar": { "classic_editor_toolbar": {
"title": "Formatting" "title": "Formatting"
@ -1953,5 +1957,10 @@
}, },
"svg": { "svg": {
"export_to_png": "The diagram could not be exported to PNG." "export_to_png": "The diagram could not be exported to PNG."
},
"code_theme": {
"title": "Appearance",
"word_wrapping": "Word wrapping",
"color-scheme": "Color scheme"
} }
} }

View File

@ -1584,12 +1584,15 @@
"auto-detect-language": "Detectado automáticamente" "auto-detect-language": "Detectado automáticamente"
}, },
"highlighting": { "highlighting": {
"title": "Resaltado de sintaxis de de código para Notas de Texto", "title": "",
"description": "Controla el resaltado de sintaxis para bloques de código dentro de las notas de texto, las notas de código no serán afectadas.", "description": "Controla el resaltado de sintaxis para bloques de código dentro de las notas de texto, las notas de código no serán afectadas.",
"color-scheme": "Esquema de color" "color-scheme": "Esquema de color"
}, },
"code_block": { "code_block": {
"word_wrapping": "Ajuste de palabras" "word_wrapping": "Ajuste de palabras",
"theme_none": "Sin resaltado de sintaxis",
"theme_group_light": "Temas claros",
"theme_group_dark": "Temas oscuros"
}, },
"classic_editor_toolbar": { "classic_editor_toolbar": {
"title": "Formato" "title": "Formato"

View File

@ -1574,12 +1574,15 @@
"auto-detect-language": "Détecté automatiquement" "auto-detect-language": "Détecté automatiquement"
}, },
"highlighting": { "highlighting": {
"title": "Coloration syntaxique du code pour les notes texte", "title": "",
"description": "Contrôle la coloration syntaxique des blocs de code à l'intérieur des notes texte, les notes de code ne seront pas affectées.", "description": "Contrôle la coloration syntaxique des blocs de code à l'intérieur des notes texte, les notes de code ne seront pas affectées.",
"color-scheme": "Jeu de couleurs" "color-scheme": "Jeu de couleurs"
}, },
"code_block": { "code_block": {
"word_wrapping": "Saut à la ligne automatique suivant la largeur" "word_wrapping": "Saut à la ligne automatique suivant la largeur",
"theme_none": "Pas de coloration syntaxique",
"theme_group_light": "Thèmes clairs",
"theme_group_dark": "Thèmes sombres"
}, },
"classic_editor_toolbar": { "classic_editor_toolbar": {
"title": "Mise en forme" "title": "Mise en forme"

View File

@ -1,5 +1,10 @@
{ {
"revisions": { "revisions": {
"delete_button": "" "delete_button": ""
},
"code_block": {
"theme_none": "Sem destaque de sintaxe",
"theme_group_light": "Temas claros",
"theme_group_dark": "Temas escuros"
} }
} }

View File

@ -1581,11 +1581,14 @@
}, },
"highlighting": { "highlighting": {
"color-scheme": "Temă de culori", "color-scheme": "Temă de culori",
"title": "Evidențiere de sintaxă pentru notițele de tip text", "title": "",
"description": "Controlează evidențierea de sintaxă pentru blocurile de cod în interiorul notițelor text, notițele de tip cod nu vor fi afectate de aceste setări." "description": "Controlează evidențierea de sintaxă pentru blocurile de cod în interiorul notițelor text, notițele de tip cod nu vor fi afectate de aceste setări."
}, },
"code_block": { "code_block": {
"word_wrapping": "Încadrare text" "word_wrapping": "Încadrare text",
"theme_none": "Fără evidențiere de sintaxă",
"theme_group_dark": "Teme întunecate",
"theme_group_light": "Teme luminoase"
}, },
"classic_editor_toolbar": { "classic_editor_toolbar": {
"title": "Formatare" "title": "Formatare"

View File

@ -1514,12 +1514,15 @@
"auto-detect-language": "自動檢測" "auto-detect-language": "自動檢測"
}, },
"highlighting": { "highlighting": {
"title": "文字筆記的程式碼語法高亮", "title": "",
"description": "控制文字筆記中程式碼塊的語法高亮,程式碼筆記不會受到影響。", "description": "控制文字筆記中程式碼塊的語法高亮,程式碼筆記不會受到影響。",
"color-scheme": "顏色方案" "color-scheme": "顏色方案"
}, },
"code_block": { "code_block": {
"word_wrapping": "自動換行" "word_wrapping": "自動換行",
"theme_none": "無格式高亮",
"theme_group_light": "淺色主題",
"theme_group_dark": "深色主題"
}, },
"classic_editor_toolbar": { "classic_editor_toolbar": {
"title": "格式化" "title": "格式化"

View File

@ -3,9 +3,7 @@ declare module "*.png" {
export default path; export default path;
} }
declare module "*.json?external" { declare module "@triliumnext/ckeditor5/emoji_definitions/en.json?url" {
var path: string; var path: string;
export default path; export default path;
} }
declare module "script-loader!mark.js/dist/jquery.mark.min.js";

View File

@ -24,3 +24,30 @@ declare module "draggabilly" {
declare module "@mind-elixir/node-menu" { declare module "@mind-elixir/node-menu" {
export default mindmap; export default mindmap;
} }
declare module "katex/contrib/auto-render" {
var renderMathInElement: (element: HTMLElement, options: {
trust: boolean;
}) => void;
export default renderMathInElement;
}
import * as L from "leaflet";
declare module "leaflet" {
interface GPXMarker {
startIcon?: DivIcon | Icon | string | undefined;
endIcon?: DivIcon | Icon | string | undefined;
wptIcons?: {
[key: string]: DivIcon | Icon | string;
};
wptTypeIcons?: {
[key: string]: DivIcon | Icon | string;
};
pointMatchers?: Array<{ regex: RegExp; icon: DivIcon | Icon | string}>;
}
interface GPXOptions {
markers?: GPXMarker | undefined;
}
}

View File

@ -22,7 +22,6 @@ interface CustomGlobals {
getReferenceLinkTitle: (href: string) => Promise<string>; getReferenceLinkTitle: (href: string) => Promise<string>;
getReferenceLinkTitleSync: (href: string) => string; getReferenceLinkTitleSync: (href: string) => string;
getActiveContextNote: () => FNote | null; getActiveContextNote: () => FNote | null;
requireLibrary: typeof library_loader.requireLibrary;
ESLINT: Library; ESLINT: Library;
appContext: AppContext; appContext: AppContext;
froca: Froca; froca: Froca;
@ -94,16 +93,6 @@ declare global {
getSelectedExternalLink(): string | undefined; getSelectedExternalLink(): string | undefined;
setSelectedExternalLink(externalLink: string | null | undefined); setSelectedExternalLink(externalLink: string | null | undefined);
setNote(noteId: string); setNote(noteId: string);
markRegExp(regex: RegExp, opts: {
element: string;
className: string;
separateWordSearch: boolean;
caseSensitive: boolean;
done?: () => void;
});
unmark(opts?: {
done: () => void;
});
} }
interface JQueryStatic { interface JQueryStatic {
@ -123,92 +112,6 @@ declare global {
var require: RequireMethod; var require: RequireMethod;
var __non_webpack_require__: RequireMethod | undefined; var __non_webpack_require__: RequireMethod | undefined;
// Libraries
// TODO: Replace once library loader is replaced with webpack.
var hljs: {
highlightAuto(text: string);
highlight(text: string, {
language: string
});
};
var renderMathInElement: (element: HTMLElement, options: {
trust: boolean;
}) => void;
interface CodeMirrorOpts {
value: string;
viewportMargin: number;
indentUnit: number;
matchBrackets: boolean;
matchTags: { bothTags: boolean };
highlightSelectionMatches: {
showToken: boolean;
annotateScrollbar: boolean;
};
lineNumbers: boolean;
lineWrapping: boolean;
keyMap?: "vim" | "default";
lint?: boolean;
gutters?: string[];
tabindex?: number;
dragDrop?: boolean;
placeholder?: string;
readOnly?: boolean;
}
var CodeMirror: {
(el: HTMLElement, opts: CodeMirrorOpts): CodeMirrorInstance;
keyMap: {
default: Record<string, string>;
};
modeURL: string;
modeInfo: ModeInfo[];
findModeByMIME(mime: string): ModeInfo;
autoLoadMode(instance: CodeMirrorInstance, mode: string)
registerHelper(type: string, filter: string | null, callback: (text: string, options: object) => unknown);
Pos(line: number, col: number);
}
interface ModeInfo {
name: string;
mode: string;
mime: string;
mimes: string[];
}
interface CodeMirrorInstance {
getValue(): string;
setValue(val: string);
clearHistory();
setOption(name: string, value: string);
refresh();
focus();
getCursor(): { line: number, col: number, ch: number };
setCursor(line: number, col: number);
getSelection(): string;
lineCount(): number;
on(event: string, callback: () => void);
operation(callback: () => void);
scrollIntoView(pos: number);
doc: {
getValue(): string;
markText(
from: { line: number, ch: number } | number,
to: { line: number, ch: number } | number,
opts: {
className: string
});
setSelection(from: number, to: number);
replaceRange(text: string, from: number, to: number);
}
}
var katex: {
renderToString(text: string, opts: {
throwOnError: boolean
});
}
/* /*
* Panzoom * Panzoom
*/ */

View File

@ -718,7 +718,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
} }
buildDefinitionValue() { buildDefinitionValue() {
const props = []; const props: string[] = [];
if (this.$inputPromoted.is(":checked")) { if (this.$inputPromoted.is(":checked")) {
props.push("promoted"); props.push("promoted");
@ -728,10 +728,10 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
} }
} }
props.push(this.$inputMultiplicity.val()); props.push(this.$inputMultiplicity.val() as string);
if (this.attrType === "label-definition") { if (this.attrType === "label-definition") {
props.push(this.$inputLabelType.val()); props.push(this.$inputLabelType.val() as string);
if (this.$inputLabelType.val() === "number" && this.$inputNumberPrecision.val() !== "") { if (this.$inputLabelType.val() === "number" && this.$inputNumberPrecision.val() !== "") {
props.push(`precision=${this.$inputNumberPrecision.val()}`); props.push(`precision=${this.$inputNumberPrecision.val()}`);

View File

@ -198,6 +198,8 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
], ],
selectMenuItemHandler: ({ command }) => this.handleAddNewAttributeCommand(command) selectMenuItemHandler: ({ command }) => this.handleAddNewAttributeCommand(command)
}); });
// Prevent automatic hiding of the context menu due to the button being clicked.
e.stopPropagation();
} }
// triggered from keyboard shortcut // triggered from keyboard shortcut

View File

@ -1,27 +0,0 @@
import { t } from "../../services/i18n.js";
import options from "../../services/options.js";
import CommandButtonWidget from "./command_button.js";
export default class CreateAiChatButton extends CommandButtonWidget {
constructor() {
super();
this.icon("bx bx-bot")
.title(t("ai.create_new_ai_chat"))
.titlePlacement("bottom")
.command("createAiChat")
.class("icon-action");
}
isEnabled() {
return options.get("aiEnabled") === "true";
}
async refreshWithNote() {
if (this.isEnabled()) {
this.$widget.show();
} else {
this.$widget.hide();
}
}
}

View File

@ -8,7 +8,10 @@ export default class CreatePaneButton extends OnClickButtonWidget {
this.icon("bx-dock-right") this.icon("bx-dock-right")
.title(t("create_pane_button.create_new_split")) .title(t("create_pane_button.create_new_split"))
.titlePlacement("bottom") .titlePlacement("bottom")
.onClick((widget) => widget.triggerCommand("openNewNoteSplit", { ntxId: widget.getClosestNtxId() })) .onClick((widget, e) => {
widget.triggerCommand("openNewNoteSplit", { ntxId: widget.getClosestNtxId() });
e.stopPropagation();
})
.class("icon-action"); .class("icon-action");
} }
} }

View File

@ -53,10 +53,6 @@ const TPL = /*html*/`
pointer-events: none; pointer-events: none;
} }
.update-to-latest-version-button {
display: none;
}
.global-menu .zoom-container { .global-menu .zoom-container {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -235,7 +231,7 @@ const TPL = /*html*/`
${t("global_menu.about")} ${t("global_menu.about")}
</li> </li>
<li class="dropdown-item update-to-latest-version-button" data-trigger-command="downloadLatestVersion"> <li class="dropdown-item update-to-latest-version-button" style="display: none;" data-trigger-command="downloadLatestVersion">
<span class="bx bx-sync"></span> <span class="bx bx-sync"></span>
<span class="version-text"></span> <span class="version-text"></span>

View File

@ -19,10 +19,10 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
return "bx-sidebar"; return "bx-sidebar";
} }
return options.is("leftPaneVisible") ? "bx-chevrons-left" : "bx-chevrons-right"; return this.currentLeftPaneVisible ? "bx-chevrons-left" : "bx-chevrons-right";
}; };
this.settings.title = () => (options.is("leftPaneVisible") ? t("left_pane_toggle.hide_panel") : t("left_pane_toggle.show_panel")); this.settings.title = () => (this.currentLeftPaneVisible ? t("left_pane_toggle.hide_panel") : t("left_pane_toggle.show_panel"));
this.settings.command = () => (this.currentLeftPaneVisible ? "hideLeftPane" : "showLeftPane"); this.settings.command = () => (this.currentLeftPaneVisible ? "hideLeftPane" : "showLeftPane");
@ -32,16 +32,12 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
} }
refreshIcon() { refreshIcon() {
if (document.hasFocus() || this.currentLeftPaneVisible === true) { super.refreshIcon();
super.refreshIcon(); splitService.setupLeftPaneResizer(this.currentLeftPaneVisible);
splitService.setupLeftPaneResizer(this.currentLeftPaneVisible);
}
} }
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { setLeftPaneVisibilityEvent({ leftPaneVisible }: EventData<"setLeftPaneVisibility">) {
if (loadResults.isOptionReloaded("leftPaneVisible") && document.hasFocus()) { this.currentLeftPaneVisible = leftPaneVisible ?? !this.currentLeftPaneVisible;
this.currentLeftPaneVisible = options.is("leftPaneVisible"); this.refreshIcon();
this.refreshIcon();
}
} }
} }

View File

@ -186,7 +186,7 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment()); this.$convertNoteIntoAttachmentButton.toggle(note.isEligibleForConversionToAttachment());
this.toggleDisabled(this.$findInTextButton, ["text", "code", "book"].includes(note.type)); this.toggleDisabled(this.$findInTextButton, ["text", "code", "book", "mindMap"].includes(note.type));
this.toggleDisabled(this.$showAttachmentsButton, !isInOptions); this.toggleDisabled(this.$showAttachmentsButton, !isInOptions);
this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "geoMap"].includes(note.type)); this.toggleDisabled(this.$showSourceButton, ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "geoMap"].includes(note.type));

View File

@ -29,7 +29,7 @@ export default class LauncherContainer extends FlexContainer<LauncherWidget> {
return; return;
} }
const newChildren = []; const newChildren: LauncherWidget[] = [];
for (const launcherNote of await visibleLaunchersRoot.getChildNotes()) { for (const launcherNote of await visibleLaunchersRoot.getChildNotes()) {
try { try {

View File

@ -4,28 +4,33 @@ import appContext, { type EventData } from "../../components/app_context.js";
import type Component from "../../components/component.js"; import type Component from "../../components/component.js";
export default class LeftPaneContainer extends FlexContainer<Component> { export default class LeftPaneContainer extends FlexContainer<Component> {
private currentLeftPaneVisible: boolean;
constructor() { constructor() {
super("column"); super("column");
this.currentLeftPaneVisible = options.is("leftPaneVisible");
this.id("left-pane"); this.id("left-pane");
this.css("height", "100%"); this.css("height", "100%");
this.collapsible(); this.collapsible();
} }
isEnabled() { isEnabled() {
return super.isEnabled() && options.is("leftPaneVisible"); return super.isEnabled() && this.currentLeftPaneVisible;
} }
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { setLeftPaneVisibilityEvent({ leftPaneVisible }: EventData<"setLeftPaneVisibility">) {
if (loadResults.isOptionReloaded("leftPaneVisible") && document.hasFocus()) { this.currentLeftPaneVisible = leftPaneVisible ?? !this.currentLeftPaneVisible;
const visible = this.isEnabled(); const visible = this.isEnabled();
this.toggleInt(visible); this.toggleInt(visible);
if (visible) { if (visible) {
this.triggerEvent("focusTree", {}); this.triggerEvent("focusTree", {});
} else { } else {
this.triggerEvent("focusOnDetail", { ntxId: appContext.tabManager.getActiveContext()?.ntxId }); this.triggerEvent("focusOnDetail", { ntxId: appContext.tabManager.getActiveContext()?.ntxId });
}
} }
options.save("leftPaneVisible", this.currentLeftPaneVisible.toString());
} }
} }

View File

@ -217,7 +217,7 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
} }
refreshNotShown(data: NoteSwitchedContext | EventData<"activeContextChanged">) { refreshNotShown(data: NoteSwitchedContext | EventData<"activeContextChanged">) {
const promises = []; const promises: (Promise<unknown> | null | undefined)[] = [];
for (const subContext of data.noteContext.getMainContext().getSubContexts()) { for (const subContext of data.noteContext.getMainContext().getSubContexts()) {
if (!subContext.ntxId) { if (!subContext.ntxId) {

View File

@ -110,6 +110,8 @@ export default class NoteTypeChooserDialog extends BasicWidget {
//@ts-ignore //@ts-ignore
if (e.clickEvent) { if (e.clickEvent) {
e.preventDefault(); e.preventDefault();
} else {
this.modal.hide();
} }
}); });
} }

View File

@ -3,7 +3,6 @@ import utils from "../../services/utils.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
import appContext from "../../components/app_context.js"; import appContext from "../../components/app_context.js";
import libraryLoader from "../../services/library_loader.js";
import openService from "../../services/open.js"; import openService from "../../services/open.js";
import protectedSessionHolder from "../../services/protected_session_holder.js"; import protectedSessionHolder from "../../services/protected_session_holder.js";
import BasicWidget from "../basic_widget.js"; import BasicWidget from "../basic_widget.js";
@ -12,6 +11,7 @@ import options from "../../services/options.js";
import type FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
import type { NoteType } from "../../entities/fnote.js"; import type { NoteType } from "../../entities/fnote.js";
import { Dropdown, Modal } from "bootstrap"; import { Dropdown, Modal } from "bootstrap";
import { renderMathInElement } from "../../services/math.js";
const TPL = /*html*/` const TPL = /*html*/`
<div class="revisions-dialog modal fade mx-auto" tabindex="-1" role="dialog"> <div class="revisions-dialog modal fade mx-auto" tabindex="-1" role="dialog">
@ -315,8 +315,6 @@ export default class RevisionsDialog extends BasicWidget {
this.$content.html(`<div class="ck-content">${fullRevision.content}</div>`); this.$content.html(`<div class="ck-content">${fullRevision.content}</div>`);
if (this.$content.find("span.math-tex").length > 0) { if (this.$content.find("span.math-tex").length > 0) {
await libraryLoader.requireLibrary(libraryLoader.KATEX);
renderMathInElement(this.$content[0], { trust: true }); renderMathInElement(this.$content[0], { trust: true });
} }
} else if (revisionItem.type === "code") { } else if (revisionItem.type === "code") {

View File

@ -188,7 +188,7 @@ export default class FindWidget extends NoteContextAwareWidget {
return; return;
} }
if (!["text", "code", "render"].includes(this.note?.type ?? "")) { if (!["text", "code", "render", "mindMap"].includes(this.note?.type ?? "")) {
return; return;
} }
@ -198,13 +198,8 @@ export default class FindWidget extends NoteContextAwareWidget {
let selectedText = ""; let selectedText = "";
if (this.note?.type === "code" && this.noteContext) { if (this.note?.type === "code" && this.noteContext) {
if (isReadOnly){ const codeEditor = await this.noteContext.getCodeEditor();
const $content = await this.noteContext.getContentElement(); selectedText = codeEditor.getSelectedText();
selectedText = $content.find('.cm-matchhighlight').first().text();
} else {
const codeEditor = await this.noteContext.getCodeEditor();
selectedText = codeEditor.getSelection();
}
} else { } else {
selectedText = window.getSelection()?.toString() || ""; selectedText = window.getSelection()?.toString() || "";
} }
@ -247,16 +242,18 @@ export default class FindWidget extends NoteContextAwareWidget {
} }
async getHandler() { async getHandler() {
if (this.note?.type === "render") { switch (this.note?.type) {
return this.htmlHandler; case "render":
} return this.htmlHandler;
case "code":
const readOnly = await this.noteContext?.isReadOnly(); return this.codeHandler;
case "text":
if (readOnly) { const readOnly = await this.noteContext?.isReadOnly();
return this.htmlHandler; return readOnly ? this.htmlHandler : this.textHandler;
} else { case "mindMap":
return this.note?.type === "code" ? this.codeHandler : this.textHandler; return this.htmlHandler;
default:
console.warn("FindWidget: Unsupported note type for find widget", this.note?.type);
} }
} }
@ -357,7 +354,7 @@ export default class FindWidget extends NoteContextAwareWidget {
} }
isEnabled() { isEnabled() {
return super.isEnabled() && ["text", "code", "render"].includes(this.note?.type ?? ""); return super.isEnabled() && ["text", "code", "render", "mindMap"].includes(this.note?.type ?? "");
} }
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {

View File

@ -17,10 +17,16 @@ interface Match {
}; };
} }
interface SearchParameters {
searchTerm: string;
matchCase: boolean;
wholeWord: boolean;
}
export default class FindInCode { export default class FindInCode {
private parent: FindWidget; private parent: FindWidget;
private findResult?: Match[] | null; private searchParameters: SearchParameters | null = null;
constructor(parent: FindWidget) { constructor(parent: FindWidget) {
this.parent = parent; this.parent = parent;
@ -31,217 +37,54 @@ export default class FindInCode {
} }
async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) { async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
let findResult: Match[] | null = null;
let totalFound = 0;
let currentFound = -1;
// See https://codemirror.net/addon/search/searchcursor.js for tips
const codeEditor = await this.getCodeEditor(); const codeEditor = await this.getCodeEditor();
if (!codeEditor) { if (!codeEditor) {
return { totalFound: 0, currentFound: 0 }; return { totalFound: 0, currentFound: 0 };
} }
const doc = codeEditor.doc; this.searchParameters = {
const text = doc.getValue(); searchTerm,
matchCase,
// Clear all markers wholeWord,
if (this.findResult) {
codeEditor.operation(() => {
const findResult = this.findResult as Match[];
for (let i = 0; i < findResult.length; ++i) {
const marker = findResult[i];
marker.clear();
}
});
}
if (searchTerm !== "") {
searchTerm = utils.escapeRegExp(searchTerm);
// Find and highlight matches
// Find and highlight matches
// XXX Using \\b and not using the unicode flag probably doesn't
// work with non-ASCII alphabets, findAndReplace uses a more
// complicated regexp, see
// https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/utils.js#L145
const wholeWordChar = wholeWord ? "\\b" : "";
const re = new RegExp(wholeWordChar + searchTerm + wholeWordChar, "g" + (matchCase ? "" : "i"));
let curLine = 0;
let curChar = 0;
let curMatch: RegExpExecArray | null = null;
findResult = [];
// All those markText take several seconds on e.g., this ~500-line
// script, batch them inside an operation, so they become
// unnoticeable. Alternatively, an overlay could be used, see
// https://codemirror.net/addon/search/match-highlighter.js ?
codeEditor.operation(() => {
for (let i = 0; i < text.length; ++i) {
// Fetch the next match if it's the first time or if past the current match start
if (curMatch == null || curMatch.index < i) {
curMatch = re.exec(text);
if (curMatch == null) {
// No more matches
break;
}
}
// Create a non-selected highlight marker for the match, the
// selected marker highlight will be done later
if (i === curMatch.index) {
let fromPos = { line: curLine, ch: curChar };
// If multiline is supported, this needs to recalculate curLine since the match may span lines
let toPos = { line: curLine, ch: curChar + curMatch[0].length };
// or css = "color: #f3"
let marker = doc.markText(fromPos, toPos, { className: FIND_RESULT_CSS_CLASSNAME });
findResult?.push(marker);
// Set the first match beyond the cursor as the current match
if (currentFound === -1) {
const cursorPos = codeEditor.getCursor();
if (fromPos.line > cursorPos.line || (fromPos.line === cursorPos.line && fromPos.ch >= cursorPos.ch)) {
currentFound = totalFound;
}
}
totalFound++;
}
// Do line and char position tracking
if (text[i] === "\n") {
curLine++;
curChar = 0;
} else {
curChar++;
}
}
});
}
this.findResult = findResult;
// Calculate curfound if not already, highlight it as selected
if (findResult && totalFound > 0) {
currentFound = Math.max(0, currentFound);
let marker = findResult[currentFound];
let pos = marker.find();
codeEditor.scrollIntoView(pos.to);
marker.clear();
findResult[currentFound] = doc.markText(pos.from, pos.to, { className: FIND_RESULT_SELECTED_CSS_CLASSNAME });
}
return {
totalFound,
currentFound: Math.min(currentFound + 1, totalFound)
}; };
const { totalFound, currentFound } = await codeEditor.performFind(searchTerm, matchCase, wholeWord);
return { totalFound, currentFound };
} }
async findNext(direction: number, currentFound: number, nextFound: number) { async findNext(direction: number, currentFound: number, nextFound: number) {
const codeEditor = await this.getCodeEditor(); const codeEditor = await this.getCodeEditor();
if (!codeEditor || !this.findResult) { if (!codeEditor) {
return; return;
} }
const doc = codeEditor.doc; codeEditor.findNext(direction, currentFound, nextFound);
//
// Dehighlight current, highlight & scrollIntoView next
//
let marker = this.findResult[currentFound];
let pos = marker.find();
marker.clear();
marker = doc.markText(pos.from, pos.to, { className: FIND_RESULT_CSS_CLASSNAME });
this.findResult[currentFound] = marker;
marker = this.findResult[nextFound];
pos = marker.find();
marker.clear();
marker = doc.markText(pos.from, pos.to, { className: FIND_RESULT_SELECTED_CSS_CLASSNAME });
this.findResult[nextFound] = marker;
codeEditor.scrollIntoView(pos.from);
} }
async findBoxClosed(totalFound: number, currentFound: number) { async findBoxClosed(totalFound: number, currentFound: number) {
const codeEditor = await this.getCodeEditor(); const codeEditor = await this.getCodeEditor();
codeEditor?.cleanSearch();
if (codeEditor && totalFound > 0) {
const doc = codeEditor.doc;
const pos = this.findResult?.[currentFound].find();
// Note setting the selection sets the cursor to
// the end of the selection and scrolls it into
// view
if (pos) {
doc.setSelection(pos.from, pos.to);
}
// Clear all markers
codeEditor.operation(() => {
if (!this.findResult) {
return;
}
for (let i = 0; i < this.findResult.length; ++i) {
let marker = this.findResult[i];
marker.clear();
}
});
}
this.findResult = null;
codeEditor?.focus(); codeEditor?.focus();
} }
async replace(replaceText: string) { async replace(replaceText: string) {
// this.findResult may be undefined and null
if (!this.findResult || this.findResult.length === 0) {
return;
}
let currentFound = -1;
this.findResult.forEach((marker, index) => {
const pos = marker.find();
if (pos) {
if (marker.className === FIND_RESULT_SELECTED_CSS_CLASSNAME) {
currentFound = index;
return;
}
}
});
if (currentFound >= 0) {
let marker = this.findResult[currentFound];
let pos = marker.find();
const codeEditor = await this.getCodeEditor();
const doc = codeEditor?.doc;
if (doc) {
doc.replaceRange(replaceText, pos.from, pos.to);
}
marker.clear();
let nextFound;
if (currentFound === this.findResult.length - 1) {
nextFound = 0;
} else {
nextFound = currentFound;
}
this.findResult.splice(currentFound, 1);
if (this.findResult.length > 0) {
this.findNext(0, nextFound, nextFound);
}
}
}
async replaceAll(replaceText: string) {
if (!this.findResult || this.findResult.length === 0) {
return;
}
const codeEditor = await this.getCodeEditor(); const codeEditor = await this.getCodeEditor();
const doc = codeEditor?.doc; await codeEditor?.replace(replaceText);
codeEditor?.operation(() => { this.rerunSearch();
if (!this.findResult) {
return;
}
for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) {
let marker = this.findResult[currentFound];
let pos = marker.find();
doc?.replaceRange(replaceText, pos.from, pos.to);
marker.clear();
}
});
this.findResult = [];
} }
async replaceAll(replaceText: string) {
const codeEditor = await this.getCodeEditor();
await codeEditor?.replaceAll(replaceText);
this.rerunSearch();
}
private rerunSearch() {
if (this.searchParameters) {
this.performFind(
this.searchParameters.searchTerm,
this.searchParameters.matchCase,
this.searchParameters.wholeWord);
}
}
} }

View File

@ -1,6 +1,7 @@
// ck-find-result and ck-find-result_selected are the styles ck-editor // ck-find-result and ck-find-result_selected are the styles ck-editor
// uses for highlighting matches, use the same one on CodeMirror // uses for highlighting matches, use the same one on CodeMirror
// for consistency // for consistency
import type Mark from "mark.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import type FindWidget from "./find.js"; import type FindWidget from "./find.js";
import type { FindResult } from "./find.js"; import type { FindResult } from "./find.js";
@ -13,6 +14,7 @@ export default class FindInHtml {
private parent: FindWidget; private parent: FindWidget;
private currentIndex: number; private currentIndex: number;
private $results: JQuery<HTMLElement> | null; private $results: JQuery<HTMLElement> | null;
private mark?: Mark;
constructor(parent: FindWidget) { constructor(parent: FindWidget) {
this.parent = parent; this.parent = parent;
@ -21,28 +23,31 @@ export default class FindInHtml {
} }
async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) { async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
await import("script-loader!mark.js/dist/jquery.mark.min.js");
const $content = await this.parent?.noteContext?.getContentElement(); const $content = await this.parent?.noteContext?.getContentElement();
if (!$content || !$content.length) {
return Promise.resolve({ totalFound: 0, currentFound: 0 });
}
if (!this.mark) {
this.mark = new (await import("mark.js")).default($content[0]);
}
const wholeWordChar = wholeWord ? "\\b" : ""; const wholeWordChar = wholeWord ? "\\b" : "";
const regExp = new RegExp(wholeWordChar + utils.escapeRegExp(searchTerm) + wholeWordChar, matchCase ? "g" : "gi"); const regExp = new RegExp(wholeWordChar + utils.escapeRegExp(searchTerm) + wholeWordChar, matchCase ? "g" : "gi");
return new Promise<FindResult>((res) => { return new Promise<FindResult>((res) => {
$content?.unmark({ this.mark!.unmark({
done: () => { done: () => {
$content.markRegExp(regExp, { this.mark!.markRegExp(regExp, {
element: "span", element: "span",
className: FIND_RESULT_CSS_CLASSNAME, className: FIND_RESULT_CSS_CLASSNAME,
separateWordSearch: false,
caseSensitive: matchCase,
done: async () => { done: async () => {
this.$results = $content.find(`.${FIND_RESULT_CSS_CLASSNAME}`); this.$results = $content.find(`.${FIND_RESULT_CSS_CLASSNAME}`);
const scrollingContainer = $content[0].closest('.scrolling-container'); const scrollingContainer = $content[0].closest('.scrolling-container');
const containerTop = scrollingContainer?.getBoundingClientRect().top ?? 0; const containerTop = scrollingContainer?.getBoundingClientRect().top ?? 0;
const closestIndex = this.$results.toArray().findIndex(el => el.getBoundingClientRect().top >= containerTop); const closestIndex = this.$results.toArray().findIndex(el => el.getBoundingClientRect().top >= containerTop);
this.currentIndex = closestIndex >= 0 ? closestIndex : 0; this.currentIndex = closestIndex >= 0 ? closestIndex : 0;
await this.jumpTo(); await this.jumpTo();
res({ res({
@ -73,17 +78,14 @@ export default class FindInHtml {
} }
async findBoxClosed(totalFound: number, currentFound: number) { async findBoxClosed(totalFound: number, currentFound: number) {
const $content = await this.parent?.noteContext?.getContentElement(); this.mark?.unmark();
if (typeof $content?.unmark === 'function') {
$content.unmark();
}
} }
async jumpTo() { async jumpTo() {
if (this.$results?.length) { if (this.$results?.length) {
const $current = this.$results.eq(this.currentIndex); const $current = this.$results.eq(this.currentIndex);
this.$results.removeClass(FIND_RESULT_SELECTED_CSS_CLASSNAME); this.$results.removeClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
$current[0].scrollIntoView(); $current[0].scrollIntoView({ block: 'center', inline: 'center'});
$current.addClass(FIND_RESULT_SELECTED_CSS_CLASSNAME); $current.addClass(FIND_RESULT_SELECTED_CSS_CLASSNAME);
} }
} }

View File

@ -2,16 +2,6 @@ import type { FindAndReplaceState, FindCommandResult } from "@triliumnext/ckedit
import type { FindResult } from "./find.js"; import type { FindResult } from "./find.js";
import type FindWidget from "./find.js"; import type FindWidget from "./find.js";
// TODO: Deduplicate.
interface Match {
className: string;
clear(): void;
find(): {
from: number;
to: number;
};
}
export default class FindInText { export default class FindInText {
private parent: FindWidget; private parent: FindWidget;
@ -35,7 +25,7 @@ export default class FindInText {
} }
const model = textEditor.model; const model = textEditor.model;
let findResult = null; let findResult: FindCommandResult | null = null;
let totalFound = 0; let totalFound = 0;
let currentFound = -1; let currentFound = -1;

View File

@ -6,6 +6,7 @@ import { t } from "../../services/i18n.js";
import LoadResults from "../../services/load_results.js"; import LoadResults from "../../services/load_results.js";
import type { AttributeRow } from "../../services/load_results.js"; import type { AttributeRow } from "../../services/load_results.js";
import FNote from "../../entities/fnote.js"; import FNote from "../../entities/fnote.js";
import options from "../../services/options.js";
export default class EditButton extends OnClickButtonWidget { export default class EditButton extends OnClickButtonWidget {
isEnabled(): boolean { isEnabled(): boolean {
@ -27,6 +28,10 @@ export default class EditButton extends OnClickButtonWidget {
} }
async refreshWithNote(note: FNote): Promise<void> { async refreshWithNote(note: FNote): Promise<void> {
if (options.is("databaseReadonly")) {
this.toggleInt(false);
return;
}
if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) { if (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) {
this.toggleInt(false); this.toggleInt(false);
} else { } else {

View File

@ -11,8 +11,8 @@ import RightPanelWidget from "./right_panel_widget.js";
import options from "../services/options.js"; import options from "../services/options.js";
import OnClickButtonWidget from "./buttons/onclick_button.js"; import OnClickButtonWidget from "./buttons/onclick_button.js";
import appContext, { type EventData } from "../components/app_context.js"; import appContext, { type EventData } from "../components/app_context.js";
import libraryLoader from "../services/library_loader.js";
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import katex from "../services/math.js";
const TPL = /*html*/`<div class="highlights-list-widget"> const TPL = /*html*/`<div class="highlights-list-widget">
<style> <style>
@ -175,7 +175,6 @@ export default class HighlightsListWidget extends RightPanelWidget {
} catch (e) { } catch (e) {
if (e instanceof ReferenceError && e.message.includes("katex is not defined")) { if (e instanceof ReferenceError && e.message.includes("katex is not defined")) {
// Load KaTeX if it is not already loaded // Load KaTeX if it is not already loaded
await libraryLoader.requireLibrary(libraryLoader.KATEX);
try { try {
rendered = katex.renderToString(latexCode, { rendered = katex.renderToString(latexCode, {
throwOnError: false throwOnError: false
@ -244,7 +243,7 @@ export default class HighlightsListWidget extends RightPanelWidget {
// Used to determine if a string is only a formula // Used to determine if a string is only a formula
const onlyMathRegex = /^<span class="math-tex">\\\([^\)]*?\)<\/span>(?:<span class="math-tex">\\\([^\)]*?\)<\/span>)*$/; const onlyMathRegex = /^<span class="math-tex">\\\([^\)]*?\)<\/span>(?:<span class="math-tex">\\\([^\)]*?\)<\/span>)*$/;
for (let match = null, hltIndex = 0; (match = combinedRegex.exec(content)) !== null; hltIndex++) { for (let match: RegExpMatchArray | null = null, hltIndex = 0; (match = combinedRegex.exec(content)) !== null; hltIndex++) {
const subHtml = match[0]; const subHtml = match[0];
const startIndex = match.index; const startIndex = match.index;
const endIndex = combinedRegex.lastIndex; const endIndex = combinedRegex.lastIndex;
@ -325,8 +324,9 @@ export default class HighlightsListWidget extends RightPanelWidget {
}); });
} else { } else {
const textEditor = await this.noteContext.getTextEditor(); const textEditor = await this.noteContext.getTextEditor();
if (textEditor) { const el = textEditor?.editing.view.domRoots.values().next().value;
targetElement = $(textEditor.editing.view.domRoots.values().next().value) if (el) {
targetElement = $(el)
.find(findSubStr) .find(findSubStr)
.filter(function () { .filter(function () {
// When finding span[style*="color"] but not looking for span[style*="background-color"], // When finding span[style*="color"] but not looking for span[style*="background-color"],

View File

@ -5,6 +5,7 @@ import BasicWidget from "../basic_widget.js";
import toastService from "../../services/toast.js"; import toastService from "../../services/toast.js";
import appContext from "../../components/app_context.js"; import appContext from "../../components/app_context.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import noteAutocompleteService from "../../services/note_autocomplete.js";
import { TPL, addMessageToChat, showSources, hideSources, showLoadingIndicator, hideLoadingIndicator } from "./ui.js"; import { TPL, addMessageToChat, showSources, hideSources, showLoadingIndicator, hideLoadingIndicator } from "./ui.js";
import { formatMarkdown } from "./utils.js"; import { formatMarkdown } from "./utils.js";
@ -12,14 +13,17 @@ import { createChatSession, checkSessionExists, setupStreamingResponse, getDirec
import { extractInChatToolSteps } from "./message_processor.js"; import { extractInChatToolSteps } from "./message_processor.js";
import { validateEmbeddingProviders } from "./validation.js"; import { validateEmbeddingProviders } from "./validation.js";
import type { MessageData, ToolExecutionStep, ChatData } from "./types.js"; import type { MessageData, ToolExecutionStep, ChatData } from "./types.js";
import { applySyntaxHighlight } from "../../services/syntax_highlight.js"; import { formatCodeBlocks } from "../../services/syntax_highlight.js";
import { ClassicEditor, type CKTextEditor, type MentionFeed } from "@triliumnext/ckeditor5";
import type { Suggestion } from "../../services/note_autocomplete.js";
import "../../stylesheets/llm_chat.css"; import "../../stylesheets/llm_chat.css";
export default class LlmChatPanel extends BasicWidget { export default class LlmChatPanel extends BasicWidget {
private noteContextChatMessages!: HTMLElement; private noteContextChatMessages!: HTMLElement;
private noteContextChatForm!: HTMLFormElement; private noteContextChatForm!: HTMLFormElement;
private noteContextChatInput!: HTMLTextAreaElement; private noteContextChatInput!: HTMLElement;
private noteContextChatInputEditor!: CKTextEditor;
private noteContextChatSendButton!: HTMLButtonElement; private noteContextChatSendButton!: HTMLButtonElement;
private chatContainer!: HTMLElement; private chatContainer!: HTMLElement;
private loadingIndicator!: HTMLElement; private loadingIndicator!: HTMLElement;
@ -29,6 +33,10 @@ export default class LlmChatPanel extends BasicWidget {
private useAdvancedContextCheckbox!: HTMLInputElement; private useAdvancedContextCheckbox!: HTMLInputElement;
private showThinkingCheckbox!: HTMLInputElement; private showThinkingCheckbox!: HTMLInputElement;
private validationWarning!: HTMLElement; private validationWarning!: HTMLElement;
private thinkingContainer!: HTMLElement;
private thinkingBubble!: HTMLElement;
private thinkingText!: HTMLElement;
private thinkingToggle!: HTMLElement;
private chatNoteId: string | null = null; private chatNoteId: string | null = null;
private noteId: string | null = null; // The actual noteId for the Chat Note private noteId: string | null = null; // The actual noteId for the Chat Note
private currentNoteId: string | null = null; private currentNoteId: string | null = null;
@ -104,7 +112,7 @@ export default class LlmChatPanel extends BasicWidget {
const element = this.$widget[0]; const element = this.$widget[0];
this.noteContextChatMessages = element.querySelector('.note-context-chat-messages') as HTMLElement; this.noteContextChatMessages = element.querySelector('.note-context-chat-messages') as HTMLElement;
this.noteContextChatForm = element.querySelector('.note-context-chat-form') as HTMLFormElement; this.noteContextChatForm = element.querySelector('.note-context-chat-form') as HTMLFormElement;
this.noteContextChatInput = element.querySelector('.note-context-chat-input') as HTMLTextAreaElement; this.noteContextChatInput = element.querySelector('.note-context-chat-input') as HTMLElement;
this.noteContextChatSendButton = element.querySelector('.note-context-chat-send-button') as HTMLButtonElement; this.noteContextChatSendButton = element.querySelector('.note-context-chat-send-button') as HTMLButtonElement;
this.chatContainer = element.querySelector('.note-context-chat-container') as HTMLElement; this.chatContainer = element.querySelector('.note-context-chat-container') as HTMLElement;
this.loadingIndicator = element.querySelector('.loading-indicator') as HTMLElement; this.loadingIndicator = element.querySelector('.loading-indicator') as HTMLElement;
@ -114,6 +122,10 @@ export default class LlmChatPanel extends BasicWidget {
this.useAdvancedContextCheckbox = element.querySelector('.use-advanced-context-checkbox') as HTMLInputElement; this.useAdvancedContextCheckbox = element.querySelector('.use-advanced-context-checkbox') as HTMLInputElement;
this.showThinkingCheckbox = element.querySelector('.show-thinking-checkbox') as HTMLInputElement; this.showThinkingCheckbox = element.querySelector('.show-thinking-checkbox') as HTMLInputElement;
this.validationWarning = element.querySelector('.provider-validation-warning') as HTMLElement; this.validationWarning = element.querySelector('.provider-validation-warning') as HTMLElement;
this.thinkingContainer = element.querySelector('.llm-thinking-container') as HTMLElement;
this.thinkingBubble = element.querySelector('.thinking-bubble') as HTMLElement;
this.thinkingText = element.querySelector('.thinking-text') as HTMLElement;
this.thinkingToggle = element.querySelector('.thinking-toggle') as HTMLElement;
// Set up event delegation for the settings link // Set up event delegation for the settings link
this.validationWarning.addEventListener('click', (e) => { this.validationWarning.addEventListener('click', (e) => {
@ -124,15 +136,84 @@ export default class LlmChatPanel extends BasicWidget {
} }
}); });
this.initializeEventListeners(); // Set up thinking toggle functionality
this.setupThinkingToggle();
// Initialize CKEditor with mention support (async)
this.initializeCKEditor().then(() => {
this.initializeEventListeners();
}).catch(error => {
console.error('Failed to initialize CKEditor, falling back to basic event listeners:', error);
this.initializeBasicEventListeners();
});
return this.$widget; return this.$widget;
} }
private async initializeCKEditor() {
const mentionSetup: MentionFeed[] = [
{
marker: "@",
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
itemRenderer: (item) => {
const suggestion = item as Suggestion;
const itemElement = document.createElement("button");
itemElement.innerHTML = `${suggestion.highlightedNotePathTitle} `;
return itemElement;
},
minimumCharacters: 0
}
];
this.noteContextChatInputEditor = await ClassicEditor.create(this.noteContextChatInput, {
toolbar: {
items: [] // No toolbar for chat input
},
placeholder: this.noteContextChatInput.getAttribute('data-placeholder') || 'Enter your message...',
mention: {
feeds: mentionSetup
},
licenseKey: "GPL"
});
// Set minimal height
const editorElement = this.noteContextChatInputEditor.ui.getEditableElement();
if (editorElement) {
editorElement.style.minHeight = '60px';
editorElement.style.maxHeight = '200px';
editorElement.style.overflowY = 'auto';
}
// Set up keybindings after editor is ready
this.setupEditorKeyBindings();
console.log('CKEditor initialized successfully for LLM chat input');
}
private initializeBasicEventListeners() {
// Fallback event listeners for when CKEditor fails to initialize
this.noteContextChatForm.addEventListener('submit', (e) => {
e.preventDefault();
// In fallback mode, the noteContextChatInput should contain a textarea
const textarea = this.noteContextChatInput.querySelector('textarea');
if (textarea) {
const content = textarea.value;
this.sendMessage(content);
}
});
}
cleanup() { cleanup() {
console.log(`LlmChatPanel cleanup called, removing any active WebSocket subscriptions`); console.log(`LlmChatPanel cleanup called, removing any active WebSocket subscriptions`);
this._messageHandler = null; this._messageHandler = null;
this._messageHandlerId = null; this._messageHandlerId = null;
// Clean up CKEditor instance
if (this.noteContextChatInputEditor) {
this.noteContextChatInputEditor.destroy().catch(error => {
console.error('Error destroying CKEditor:', error);
});
}
} }
/** /**
@ -531,18 +612,31 @@ export default class LlmChatPanel extends BasicWidget {
private async sendMessage(content: string) { private async sendMessage(content: string) {
if (!content.trim()) return; if (!content.trim()) return;
// Extract mentions from the content if using CKEditor
let mentions: Array<{noteId: string; title: string; notePath: string}> = [];
let plainTextContent = content;
if (this.noteContextChatInputEditor) {
const extracted = this.extractMentionsAndContent(content);
mentions = extracted.mentions;
plainTextContent = extracted.content;
}
// Add the user message to the UI and data model // Add the user message to the UI and data model
this.addMessageToChat('user', content); this.addMessageToChat('user', plainTextContent);
this.messages.push({ this.messages.push({
role: 'user', role: 'user',
content: content content: plainTextContent,
mentions: mentions.length > 0 ? mentions : undefined
}); });
// Save the data immediately after a user message // Save the data immediately after a user message
await this.saveCurrentData(); await this.saveCurrentData();
// Clear input and show loading state // Clear input and show loading state
this.noteContextChatInput.value = ''; if (this.noteContextChatInputEditor) {
this.noteContextChatInputEditor.setData('');
}
showLoadingIndicator(this.loadingIndicator); showLoadingIndicator(this.loadingIndicator);
this.hideSources(); this.hideSources();
@ -555,9 +649,10 @@ export default class LlmChatPanel extends BasicWidget {
// Create the message parameters // Create the message parameters
const messageParams = { const messageParams = {
content, content: plainTextContent,
useAdvancedContext, useAdvancedContext,
showThinking showThinking,
mentions: mentions.length > 0 ? mentions : undefined
}; };
// Try websocket streaming (preferred method) // Try websocket streaming (preferred method)
@ -621,7 +716,9 @@ export default class LlmChatPanel extends BasicWidget {
} }
// Clear input and show loading state // Clear input and show loading state
this.noteContextChatInput.value = ''; if (this.noteContextChatInputEditor) {
this.noteContextChatInputEditor.setData('');
}
showLoadingIndicator(this.loadingIndicator); showLoadingIndicator(this.loadingIndicator);
this.hideSources(); this.hideSources();
@ -898,6 +995,16 @@ export default class LlmChatPanel extends BasicWidget {
* Update the UI with streaming content * Update the UI with streaming content
*/ */
private updateStreamingUI(assistantResponse: string, isDone: boolean = false) { private updateStreamingUI(assistantResponse: string, isDone: boolean = false) {
// Parse and handle thinking content if present
if (!isDone) {
const thinkingContent = this.parseThinkingContent(assistantResponse);
if (thinkingContent) {
this.updateThinkingText(thinkingContent);
// Don't display the raw response with think tags in the chat
return;
}
}
// Get the existing assistant message or create a new one // Get the existing assistant message or create a new one
let assistantMessageEl = this.noteContextChatMessages.querySelector('.assistant-message:last-child'); let assistantMessageEl = this.noteContextChatMessages.querySelector('.assistant-message:last-child');
@ -919,13 +1026,19 @@ export default class LlmChatPanel extends BasicWidget {
assistantMessageEl.appendChild(messageContent); assistantMessageEl.appendChild(messageContent);
} }
// Clean the response to remove thinking tags before displaying
const cleanedResponse = this.removeThinkingTags(assistantResponse);
// Update the content // Update the content
const messageContent = assistantMessageEl.querySelector('.message-content') as HTMLElement; const messageContent = assistantMessageEl.querySelector('.message-content') as HTMLElement;
messageContent.innerHTML = formatMarkdown(assistantResponse); messageContent.innerHTML = formatMarkdown(cleanedResponse);
// Apply syntax highlighting if this is the final update // Apply syntax highlighting if this is the final update
if (isDone) { if (isDone) {
applySyntaxHighlight($(assistantMessageEl as HTMLElement)); formatCodeBlocks($(assistantMessageEl as HTMLElement));
// Hide the thinking display when response is complete
this.hideThinkingDisplay();
// Update message in the data model for storage // Update message in the data model for storage
// Find the last assistant message to update, or add a new one if none exists // Find the last assistant message to update, or add a new one if none exists
@ -934,13 +1047,13 @@ export default class LlmChatPanel extends BasicWidget {
this.messages.lastIndexOf(assistantMessages[assistantMessages.length - 1]) : -1; this.messages.lastIndexOf(assistantMessages[assistantMessages.length - 1]) : -1;
if (lastAssistantMsgIndex >= 0) { if (lastAssistantMsgIndex >= 0) {
// Update existing message // Update existing message with cleaned content
this.messages[lastAssistantMsgIndex].content = assistantResponse; this.messages[lastAssistantMsgIndex].content = cleanedResponse;
} else { } else {
// Add new message // Add new message with cleaned content
this.messages.push({ this.messages.push({
role: 'assistant', role: 'assistant',
content: assistantResponse content: cleanedResponse
}); });
} }
@ -957,6 +1070,16 @@ export default class LlmChatPanel extends BasicWidget {
this.chatContainer.scrollTop = this.chatContainer.scrollHeight; this.chatContainer.scrollTop = this.chatContainer.scrollHeight;
} }
/**
* Remove thinking tags from response content
*/
private removeThinkingTags(content: string): string {
if (!content) return content;
// Remove <think>...</think> blocks from the content
return content.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
}
/** /**
* Handle general errors in the send message flow * Handle general errors in the send message flow
*/ */
@ -1203,32 +1326,308 @@ export default class LlmChatPanel extends BasicWidget {
* Show thinking state in the UI * Show thinking state in the UI
*/ */
private showThinkingState(thinkingData: string) { private showThinkingState(thinkingData: string) {
// Thinking state is now updated via the in-chat UI in updateStreamingUI // Parse the thinking content to extract text between <think> tags
// This method is now just a hook for the WebSocket handlers const thinkingContent = this.parseThinkingContent(thinkingData);
// Show the loading indicator if (thinkingContent) {
this.showThinkingDisplay(thinkingContent);
} else {
// Fallback: show raw thinking data
this.showThinkingDisplay(thinkingData);
}
// Show the loading indicator as well
this.loadingIndicator.style.display = 'flex'; this.loadingIndicator.style.display = 'flex';
} }
/**
* Parse thinking content from LLM response
*/
private parseThinkingContent(content: string): string | null {
if (!content) return null;
// Look for content between <think> and </think> tags
const thinkRegex = /<think>([\s\S]*?)<\/think>/gi;
const matches: string[] = [];
let match: RegExpExecArray | null;
while ((match = thinkRegex.exec(content)) !== null) {
matches.push(match[1].trim());
}
if (matches.length > 0) {
return matches.join('\n\n--- Next thought ---\n\n');
}
// Check for incomplete thinking blocks (streaming in progress)
const incompleteThinkRegex = /<think>([\s\S]*?)$/i;
const incompleteMatch = content.match(incompleteThinkRegex);
if (incompleteMatch && incompleteMatch[1]) {
return incompleteMatch[1].trim() + '\n\n[Thinking in progress...]';
}
// If no think tags found, check if the entire content might be thinking
if (content.toLowerCase().includes('thinking') ||
content.toLowerCase().includes('reasoning') ||
content.toLowerCase().includes('let me think') ||
content.toLowerCase().includes('i need to') ||
content.toLowerCase().includes('first, ') ||
content.toLowerCase().includes('step 1') ||
content.toLowerCase().includes('analysis:')) {
return content;
}
return null;
}
private initializeEventListeners() { private initializeEventListeners() {
this.noteContextChatForm.addEventListener('submit', (e) => { this.noteContextChatForm.addEventListener('submit', (e) => {
e.preventDefault(); e.preventDefault();
const content = this.noteContextChatInput.value;
this.sendMessage(content);
});
// Add auto-resize functionality to the textarea let content = '';
this.noteContextChatInput.addEventListener('input', () => {
this.noteContextChatInput.style.height = 'auto';
this.noteContextChatInput.style.height = `${this.noteContextChatInput.scrollHeight}px`;
});
// Handle Enter key (send on Enter, new line on Shift+Enter) if (this.noteContextChatInputEditor && this.noteContextChatInputEditor.getData) {
this.noteContextChatInput.addEventListener('keydown', (e) => { // Use CKEditor content
if (e.key === 'Enter' && !e.shiftKey) { content = this.noteContextChatInputEditor.getData();
e.preventDefault(); } else {
this.noteContextChatForm.dispatchEvent(new Event('submit')); // Fallback: check if there's a textarea (fallback mode)
const textarea = this.noteContextChatInput.querySelector('textarea');
if (textarea) {
content = textarea.value;
} else {
// Last resort: try to get text content from the div
content = this.noteContextChatInput.textContent || this.noteContextChatInput.innerText || '';
}
}
if (content.trim()) {
this.sendMessage(content);
} }
}); });
// Handle Enter key (send on Enter, new line on Shift+Enter) via CKEditor
// We'll set this up after CKEditor is initialized
this.setupEditorKeyBindings();
}
private setupEditorKeyBindings() {
if (this.noteContextChatInputEditor && this.noteContextChatInputEditor.keystrokes) {
try {
this.noteContextChatInputEditor.keystrokes.set('Enter', (key, stop) => {
if (!key.shiftKey) {
stop();
this.noteContextChatForm.dispatchEvent(new Event('submit'));
}
});
console.log('CKEditor keybindings set up successfully');
} catch (error) {
console.warn('Failed to set up CKEditor keybindings:', error);
}
}
}
/**
* Extract note mentions and content from CKEditor
*/
private extractMentionsAndContent(editorData: string): { content: string; mentions: Array<{noteId: string; title: string; notePath: string}> } {
const mentions: Array<{noteId: string; title: string; notePath: string}> = [];
// Parse the HTML content to extract mentions
const tempDiv = document.createElement('div');
tempDiv.innerHTML = editorData;
// Find all mention elements - CKEditor uses specific patterns for mentions
// Look for elements with data-mention attribute or specific mention classes
const mentionElements = tempDiv.querySelectorAll('[data-mention], .mention, span[data-id]');
mentionElements.forEach(mentionEl => {
try {
// Try different ways to extract mention data based on CKEditor's format
let mentionData: any = null;
// Method 1: data-mention attribute (JSON format)
if (mentionEl.hasAttribute('data-mention')) {
mentionData = JSON.parse(mentionEl.getAttribute('data-mention') || '{}');
}
// Method 2: data-id attribute (simple format)
else if (mentionEl.hasAttribute('data-id')) {
const dataId = mentionEl.getAttribute('data-id');
const textContent = mentionEl.textContent || '';
// Parse the dataId to extract note information
if (dataId && dataId.startsWith('@')) {
const cleanId = dataId.substring(1); // Remove the @
mentionData = {
id: cleanId,
name: textContent,
notePath: cleanId // Assume the ID contains the path
};
}
}
// Method 3: Check if this is a reference link (href=#notePath)
else if (mentionEl.tagName === 'A' && mentionEl.hasAttribute('href')) {
const href = mentionEl.getAttribute('href');
if (href && href.startsWith('#')) {
const notePath = href.substring(1);
mentionData = {
notePath: notePath,
noteTitle: mentionEl.textContent || 'Unknown Note'
};
}
}
if (mentionData && (mentionData.notePath || mentionData.link)) {
const notePath = mentionData.notePath || mentionData.link?.substring(1); // Remove # from link
const noteId = notePath ? notePath.split('/').pop() : null;
const title = mentionData.noteTitle || mentionData.name || mentionEl.textContent || 'Unknown Note';
if (noteId) {
mentions.push({
noteId: noteId,
title: title,
notePath: notePath
});
console.log(`Extracted mention: noteId=${noteId}, title=${title}, notePath=${notePath}`);
}
}
} catch (e) {
console.warn('Failed to parse mention data:', e, mentionEl);
}
});
// Convert to plain text for the LLM, but preserve the structure
const content = tempDiv.textContent || tempDiv.innerText || '';
console.log(`Extracted ${mentions.length} mentions from editor content`);
return { content, mentions };
}
private setupThinkingToggle() {
if (this.thinkingToggle) {
this.thinkingToggle.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleThinkingDetails();
});
}
// Also make the entire header clickable
const thinkingHeader = this.thinkingBubble?.querySelector('.thinking-header');
if (thinkingHeader) {
thinkingHeader.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (!target.closest('.thinking-toggle')) {
this.toggleThinkingDetails();
}
});
}
}
private toggleThinkingDetails() {
const content = this.thinkingBubble?.querySelector('.thinking-content') as HTMLElement;
const toggle = this.thinkingToggle?.querySelector('i');
if (content && toggle) {
const isVisible = content.style.display !== 'none';
if (isVisible) {
content.style.display = 'none';
toggle.className = 'bx bx-chevron-down';
this.thinkingToggle.classList.remove('expanded');
} else {
content.style.display = 'block';
toggle.className = 'bx bx-chevron-up';
this.thinkingToggle.classList.add('expanded');
}
}
}
/**
* Show the thinking display with optional initial content
*/
private showThinkingDisplay(initialText: string = '') {
if (this.thinkingContainer) {
this.thinkingContainer.style.display = 'block';
if (initialText && this.thinkingText) {
this.updateThinkingText(initialText);
}
// Scroll to show the thinking display
this.chatContainer.scrollTop = this.chatContainer.scrollHeight;
}
}
/**
* Update the thinking text content
*/
private updateThinkingText(text: string) {
if (this.thinkingText) {
// Format the thinking text for better readability
const formattedText = this.formatThinkingText(text);
this.thinkingText.textContent = formattedText;
// Auto-scroll if content is expanded
const content = this.thinkingBubble?.querySelector('.thinking-content') as HTMLElement;
if (content && content.style.display !== 'none') {
this.chatContainer.scrollTop = this.chatContainer.scrollHeight;
}
}
}
/**
* Format thinking text for better presentation
*/
private formatThinkingText(text: string): string {
if (!text) return text;
// Clean up the text
let formatted = text.trim();
// Add some basic formatting
formatted = formatted
// Add spacing around section markers
.replace(/(\d+\.\s)/g, '\n$1')
// Clean up excessive whitespace
.replace(/\n\s*\n\s*\n/g, '\n\n')
// Trim again
.trim();
return formatted;
}
/**
* Hide the thinking display
*/
private hideThinkingDisplay() {
if (this.thinkingContainer) {
this.thinkingContainer.style.display = 'none';
// Reset the toggle state
const content = this.thinkingBubble?.querySelector('.thinking-content') as HTMLElement;
const toggle = this.thinkingToggle?.querySelector('i');
if (content && toggle) {
content.style.display = 'none';
toggle.className = 'bx bx-chevron-down';
this.thinkingToggle?.classList.remove('expanded');
}
// Clear the text content
if (this.thinkingText) {
this.thinkingText.textContent = '';
}
}
}
/**
* Append to existing thinking content (for streaming updates)
*/
private appendThinkingText(additionalText: string) {
if (this.thinkingText && additionalText) {
const currentText = this.thinkingText.textContent || '';
const newText = currentText + additionalText;
this.updateThinkingText(newText);
}
} }
} }

View File

@ -24,6 +24,11 @@ export interface MessageData {
role: string; role: string;
content: string; content: string;
timestamp?: Date; timestamp?: Date;
mentions?: Array<{
noteId: string;
title: string;
notePath: string;
}>;
} }
export interface ChatData { export interface ChatData {

View File

@ -13,6 +13,27 @@ export const TPL = `
<div class="note-context-chat-container flex-grow-1 overflow-auto p-3"> <div class="note-context-chat-container flex-grow-1 overflow-auto p-3">
<div class="note-context-chat-messages"></div> <div class="note-context-chat-messages"></div>
<!-- Thinking display area -->
<div class="llm-thinking-container" style="display: none;">
<div class="thinking-bubble">
<div class="thinking-header d-flex align-items-center">
<div class="thinking-dots">
<span></span>
<span></span>
<span></span>
</div>
<span class="thinking-label ms-2 text-muted small">AI is thinking...</span>
<button type="button" class="btn btn-sm btn-link p-0 ms-auto thinking-toggle" title="Toggle thinking details">
<i class="bx bx-chevron-down"></i>
</button>
</div>
<div class="thinking-content" style="display: none;">
<div class="thinking-text"></div>
</div>
</div>
</div>
<div class="loading-indicator" style="display: none;"> <div class="loading-indicator" style="display: none;">
<div class="spinner-border spinner-border-sm text-primary" role="status"> <div class="spinner-border spinner-border-sm text-primary" role="status">
<span class="visually-hidden">Loading...</span> <span class="visually-hidden">Loading...</span>
@ -31,11 +52,11 @@ export const TPL = `
<form class="note-context-chat-form d-flex flex-column border-top p-2"> <form class="note-context-chat-form d-flex flex-column border-top p-2">
<div class="d-flex chat-input-container mb-2"> <div class="d-flex chat-input-container mb-2">
<textarea <div
class="form-control note-context-chat-input" class="form-control note-context-chat-input flex-grow-1"
placeholder="${t('ai_llm.enter_message')}" style="min-height: 60px; max-height: 200px; overflow-y: auto;"
rows="2" data-placeholder="${t('ai_llm.enter_message')}"
></textarea> ></div>
<button type="submit" class="btn btn-primary note-context-chat-send-button ms-2 d-flex align-items-center justify-content-center"> <button type="submit" class="btn btn-primary note-context-chat-send-button ms-2 d-flex align-items-center justify-content-center">
<i class="bx bx-send"></i> <i class="bx bx-send"></i>
</button> </button>

View File

@ -2,7 +2,7 @@
* Utility functions for LLM Chat * Utility functions for LLM Chat
*/ */
import { marked } from "marked"; import { marked } from "marked";
import { applySyntaxHighlight } from "../../services/syntax_highlight.js"; import { formatCodeBlocks } from "../../services/syntax_highlight.js";
/** /**
* Format markdown content for display * Format markdown content for display
@ -62,7 +62,7 @@ export function escapeHtml(text: string): string {
* Apply syntax highlighting to content * Apply syntax highlighting to content
*/ */
export function applyHighlighting(element: HTMLElement): void { export function applyHighlighting(element: HTMLElement): void {
applySyntaxHighlight($(element)); formatCodeBlocks($(element));
} }
/** /**

View File

@ -16,49 +16,53 @@ export async function validateEmbeddingProviders(validationWarning: HTMLElement)
return; return;
} }
// Get provider precedence // Get precedence list from options
const precedenceStr = options.get('aiProviderPrecedence') || 'openai,anthropic,ollama'; const precedenceStr = options.get('aiProviderPrecedence') || 'openai,anthropic,ollama';
let precedenceList: string[] = []; let precedenceList: string[] = [];
if (precedenceStr) { if (precedenceStr) {
if (precedenceStr.startsWith('[') && precedenceStr.endsWith(']')) { if (precedenceStr.startsWith('[') && precedenceStr.endsWith(']')) {
precedenceList = JSON.parse(precedenceStr); try {
precedenceList = JSON.parse(precedenceStr);
} catch (e) {
console.error('Error parsing precedence list:', e);
precedenceList = ['openai']; // Default if parsing fails
}
} else if (precedenceStr.includes(',')) { } else if (precedenceStr.includes(',')) {
precedenceList = precedenceStr.split(',').map(p => p.trim()); precedenceList = precedenceStr.split(',').map(p => p.trim());
} else { } else {
precedenceList = [precedenceStr]; precedenceList = [precedenceStr];
} }
} }
// Get enabled providers - this is a simplification since we don't have direct DB access // Check for configuration issues with providers in the precedence list
// We'll determine enabled status based on the presence of keys or settings const configIssues: string[] = [];
const enabledProviders: string[] = [];
// Check each provider in the precedence list for proper configuration
// OpenAI is enabled if API key is set for (const provider of precedenceList) {
const openaiKey = options.get('openaiApiKey'); if (provider === 'openai') {
if (openaiKey) { // Check OpenAI configuration
enabledProviders.push('openai'); const apiKey = options.get('openaiApiKey');
if (!apiKey) {
configIssues.push(`OpenAI API key is missing`);
}
} else if (provider === 'anthropic') {
// Check Anthropic configuration
const apiKey = options.get('anthropicApiKey');
if (!apiKey) {
configIssues.push(`Anthropic API key is missing`);
}
} else if (provider === 'ollama') {
// Check Ollama configuration
const baseUrl = options.get('ollamaBaseUrl');
if (!baseUrl) {
configIssues.push(`Ollama Base URL is missing`);
}
}
// Add checks for other providers as needed
} }
// Anthropic is enabled if API key is set // Fetch embedding stats to check if there are any notes being processed
const anthropicKey = options.get('anthropicApiKey');
if (anthropicKey) {
enabledProviders.push('anthropic');
}
// Ollama is enabled if base URL is set
const ollamaBaseUrl = options.get('ollamaBaseUrl');
if (ollamaBaseUrl) {
enabledProviders.push('ollama');
}
// Local is always available
enabledProviders.push('local');
// Perform validation checks
const allPrecedenceEnabled = precedenceList.every((p: string) => enabledProviders.includes(p));
// Get embedding queue status
const embeddingStats = await getEmbeddingStats() as { const embeddingStats = await getEmbeddingStats() as {
success: boolean, success: boolean,
stats: { stats: {
@ -73,17 +77,18 @@ export async function validateEmbeddingProviders(validationWarning: HTMLElement)
const queuedNotes = embeddingStats?.stats?.queuedNotesCount || 0; const queuedNotes = embeddingStats?.stats?.queuedNotesCount || 0;
const hasEmbeddingsInQueue = queuedNotes > 0; const hasEmbeddingsInQueue = queuedNotes > 0;
// Show warning if there are issues // Show warning if there are configuration issues or embeddings in queue
if (!allPrecedenceEnabled || hasEmbeddingsInQueue) { if (configIssues.length > 0 || hasEmbeddingsInQueue) {
let message = '<i class="bx bx-error-circle me-2"></i><strong>AI Provider Configuration Issues</strong>'; let message = '<i class="bx bx-error-circle me-2"></i><strong>AI Provider Configuration Issues</strong>';
message += '<ul class="mb-1 ps-4">'; message += '<ul class="mb-1 ps-4">';
if (!allPrecedenceEnabled) { // Show configuration issues
const disabledProviders = precedenceList.filter((p: string) => !enabledProviders.includes(p)); for (const issue of configIssues) {
message += `<li>The following providers in your precedence list are not enabled: ${disabledProviders.join(', ')}.</li>`; message += `<li>${issue}</li>`;
} }
// Show warning about embeddings queue if applicable
if (hasEmbeddingsInQueue) { if (hasEmbeddingsInQueue) {
message += `<li>Currently processing embeddings for ${queuedNotes} notes. Some AI features may produce incomplete results until processing completes.</li>`; message += `<li>Currently processing embeddings for ${queuedNotes} notes. Some AI features may produce incomplete results until processing completes.</li>`;
} }

View File

@ -3,12 +3,10 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
import protectedSessionHolder from "../services/protected_session_holder.js"; import protectedSessionHolder from "../services/protected_session_holder.js";
import SpacedUpdate from "../services/spaced_update.js"; import SpacedUpdate from "../services/spaced_update.js";
import server from "../services/server.js"; import server from "../services/server.js";
import libraryLoader from "../services/library_loader.js";
import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js"; import appContext, { type CommandListenerData, type EventData } from "../components/app_context.js";
import keyboardActionsService from "../services/keyboard_actions.js"; import keyboardActionsService from "../services/keyboard_actions.js";
import noteCreateService from "../services/note_create.js"; import noteCreateService from "../services/note_create.js";
import attributeService from "../services/attributes.js"; import attributeService from "../services/attributes.js";
import attributeRenderer from "../services/attribute_renderer.js";
import EmptyTypeWidget from "./type_widgets/empty.js"; import EmptyTypeWidget from "./type_widgets/empty.js";
import EditableTextTypeWidget from "./type_widgets/editable_text.js"; import EditableTextTypeWidget from "./type_widgets/editable_text.js";
@ -30,7 +28,6 @@ import ContentWidgetTypeWidget from "./type_widgets/content_widget.js";
import AttachmentListTypeWidget from "./type_widgets/attachment_list.js"; import AttachmentListTypeWidget from "./type_widgets/attachment_list.js";
import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js"; import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js";
import MindMapWidget from "./type_widgets/mind_map.js"; import MindMapWidget from "./type_widgets/mind_map.js";
import { getStylesheetUrl, isSyntaxHighlightEnabled } from "../services/syntax_highlight.js";
import GeoMapTypeWidget from "./type_widgets/geo_map.js"; import GeoMapTypeWidget from "./type_widgets/geo_map.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import type { NoteType } from "../entities/fnote.js"; import type { NoteType } from "../entities/fnote.js";

View File

@ -1,7 +1,6 @@
import { Dropdown } from "bootstrap"; import { Dropdown } from "bootstrap";
import NoteContextAwareWidget from "./note_context_aware_widget.js"; import NoteContextAwareWidget from "./note_context_aware_widget.js";
import { getAvailableLocales, getLocaleById } from "../services/i18n.js"; import { getAvailableLocales, getLocaleById, t } from "../services/i18n.js";
import { t } from "i18next";
import type { EventData } from "../components/app_context.js"; import type { EventData } from "../components/app_context.js";
import type FNote from "../entities/fnote.js"; import type FNote from "../entities/fnote.js";
import attributes from "../services/attributes.js"; import attributes from "../services/attributes.js";

View File

@ -27,6 +27,11 @@ import type { AttributeRow, BranchRow } from "../services/load_results.js";
import type { SetNoteOpts } from "../components/note_context.js"; import type { SetNoteOpts } from "../components/note_context.js";
import type { TouchBarItem } from "../components/touch_bar.js"; import type { TouchBarItem } from "../components/touch_bar.js";
import type { TreeCommandNames } from "../menus/tree_context_menu.js"; import type { TreeCommandNames } from "../menus/tree_context_menu.js";
import "jquery.fancytree";
import "jquery.fancytree/dist/modules/jquery.fancytree.dnd5.js";
import "jquery.fancytree/dist/modules/jquery.fancytree.clones.js";
import "jquery.fancytree/dist/modules/jquery.fancytree.filter.js";
import "../stylesheets/tree.css";
const TPL = /*html*/` const TPL = /*html*/`
<div class="tree-wrapper"> <div class="tree-wrapper">
@ -345,7 +350,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
}, },
scrollParent: this.$tree, scrollParent: this.$tree,
minExpandLevel: 2, // root can't be collapsed minExpandLevel: 2, // root can't be collapsed
click: (event: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement>, data): boolean => { click: (event, data): boolean => {
this.activityDetected(); this.activityDetected();
const targetType = data.targetType; const targetType = data.targetType;
@ -740,7 +745,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
prepareChildren(parentNote: FNote) { prepareChildren(parentNote: FNote) {
utils.assertArguments(parentNote); utils.assertArguments(parentNote);
const noteList = []; const noteList: Node[] = [];
const hideArchivedNotes = this.hideArchivedNotes; const hideArchivedNotes = this.hideArchivedNotes;
@ -834,7 +839,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
getExtraClasses(note: FNote) { getExtraClasses(note: FNote) {
utils.assertArguments(note); utils.assertArguments(note);
const extraClasses = []; const extraClasses: string[] = [];
if (note.isProtected) { if (note.isProtected) {
extraClasses.push("protected"); extraClasses.push("protected");
@ -1167,16 +1172,19 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
let noneCollapsedYet = true; let noneCollapsedYet = true;
this.tree.getRootNode().visit((node) => { if (!options.is("databaseReadonly")) {
if (node.isExpanded() && !noteIdsToKeepExpanded.has(node.data.noteId)) { // can't change expanded notes when database is readonly
node.setExpanded(false); this.tree.getRootNode().visit((node) => {
if (node.isExpanded() && !noteIdsToKeepExpanded.has(node.data.noteId)) {
node.setExpanded(false);
if (noneCollapsedYet) { if (noneCollapsedYet) {
toastService.showMessage(t("note_tree.auto-collapsing-notes-after-inactivity")); toastService.showMessage(t("note_tree.auto-collapsing-notes-after-inactivity"));
noneCollapsedYet = false; noneCollapsedYet = false;
}
} }
} }, false);
}, false); }
this.filterHoistedBranch(true); this.filterHoistedBranch(true);
}, 600 * 1000); }, 600 * 1000);
@ -1257,8 +1265,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const allBranchesDeleted = branchRows.every((branchRow) => !!branchRow.isDeleted); const allBranchesDeleted = branchRows.every((branchRow) => !!branchRow.isDeleted);
// activeNode is supposed to be moved when we find out activeNode is deleted but not all branches are deleted. save it for fixing activeNodePath after all nodes loaded. // activeNode is supposed to be moved when we find out activeNode is deleted but not all branches are deleted. save it for fixing activeNodePath after all nodes loaded.
let movedActiveNode = null; let movedActiveNode: Fancytree.FancytreeNode | null = null;
let parentsOfAddedNodes = []; let parentsOfAddedNodes: Fancytree.FancytreeNode[] = [];
for (const branchRow of branchRows) { for (const branchRow of branchRows) {
if (branchRow.noteId) { if (branchRow.noteId) {

View File

@ -38,7 +38,6 @@ const NOTE_TYPES: NoteTypeMapping[] = [
// Misc note types // Misc note types
{ type: "render", mime: "", title: t("note_types.render-note"), selectable: true }, { type: "render", mime: "", title: t("note_types.render-note"), selectable: true },
{ type: "webView", mime: "", title: t("note_types.web-view"), selectable: true }, { type: "webView", mime: "", title: t("note_types.web-view"), selectable: true },
{ type: "aiChat", mime: "application/json", title: t("note_types.ai-chat"), selectable: true },
// Code notes // Code notes
{ type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true }, { type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true },
@ -50,7 +49,8 @@ const NOTE_TYPES: NoteTypeMapping[] = [
{ type: "image", title: t("note_types.image"), selectable: false }, { type: "image", title: t("note_types.image"), selectable: false },
{ type: "launcher", mime: "", title: t("note_types.launcher"), selectable: false }, { type: "launcher", mime: "", title: t("note_types.launcher"), selectable: false },
{ type: "noteMap", mime: "", title: t("note_types.note-map"), selectable: false }, { type: "noteMap", mime: "", title: t("note_types.note-map"), selectable: false },
{ type: "search", title: t("note_types.saved-search"), selectable: false } { type: "search", title: t("note_types.saved-search"), selectable: false },
{ type: "aiChat", mime: "application/json", title: t("note_types.ai-chat"), selectable: false }
]; ];
const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => !nt.selectable).map((nt) => nt.type); const NOT_SELECTABLE_NOTE_TYPES = NOTE_TYPES.filter((nt) => !nt.selectable).map((nt) => nt.type);

View File

@ -19,7 +19,7 @@ const TPL = /*html*/`
<div class="no-edited-notes-found">${t("edited_notes.no_edited_notes_found")}</div> <div class="no-edited-notes-found">${t("edited_notes.no_edited_notes_found")}</div>
<div class="edited-notes-list"></div> <div class="edited-notes-list use-tn-links"></div>
</div> </div>
`; `;

View File

@ -13,7 +13,7 @@ const TPL = /*html*/`
height: 300px; height: 300px;
} }
.open-full-button, .collapse-button { .note-map-ribbon-widget .open-full-button, .note-map-ribbon-widget .collapse-button {
position: absolute; position: absolute;
right: 5px; right: 5px;
bottom: 5px; bottom: 5px;

View File

@ -85,7 +85,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
this.$notePathIntro.text(t("note_paths.intro_not_placed")); this.$notePathIntro.text(t("note_paths.intro_not_placed"));
} }
const renderedPaths = []; const renderedPaths: JQuery<HTMLElement>[] = [];
for (const notePathRecord of sortedNotePaths) { for (const notePathRecord of sortedNotePaths) {
const notePath = notePathRecord.notePath; const notePath = notePathRecord.notePath;
@ -100,7 +100,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
const $pathItem = $("<li>"); const $pathItem = $("<li>");
const pathSegments: string[] = []; const pathSegments: string[] = [];
const lastIndex = notePath.length - 1; const lastIndex = notePath.length - 1;
for (let i = 0; i < notePath.length; i++) { for (let i = 0; i < notePath.length; i++) {
const noteId = notePath[i]; const noteId = notePath[i];
pathSegments.push(noteId); pathSegments.push(noteId);
@ -109,13 +109,13 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
$noteLink.find("a").addClass("no-tooltip-preview tn-link"); $noteLink.find("a").addClass("no-tooltip-preview tn-link");
$pathItem.append($noteLink); $pathItem.append($noteLink);
if (i != lastIndex) { if (i != lastIndex) {
$pathItem.append(" / "); $pathItem.append(" / ");
} }
} }
const icons = []; const icons: string[] = [];
if (this.notePath === notePath.join("/")) { if (this.notePath === notePath.join("/")) {
$pathItem.addClass("path-current"); $pathItem.addClass("path-current");

View File

@ -122,7 +122,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
return; return;
} }
const $cells = []; const $cells: JQuery<HTMLElement>[] = [];
for (const definitionAttr of promotedDefAttrs) { for (const definitionAttr of promotedDefAttrs) {
const valueType = definitionAttr.name.startsWith("label:") ? "label" : "relation"; const valueType = definitionAttr.name.startsWith("label:") ? "label" : "relation";

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