Merge branch 'develop' into feat/add-rootless-dockerfiles

This commit is contained in:
perf3ct 2025-05-21 12:58:05 -07:00
commit cbbbae727f
156 changed files with 4906 additions and 6149 deletions

View File

@ -53,7 +53,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Install Playwright Browsers
run: npx playwright install --with-deps
run: pnpx playwright install --with-deps
- name: Run the TypeScript build
run: pnpm run server:build
@ -62,7 +62,7 @@ jobs:
uses: docker/build-push-action@v6
with:
context: apps/server
file: ${{ matrix.dockerfile }}
file: apps/server/${{ matrix.dockerfile }}
load: true
tags: ${{ env.TEST_TAG }}
cache-from: type=gha
@ -70,7 +70,7 @@ jobs:
- name: Validate container run output
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"
- name: Wait for the healthchecks to pass
@ -82,7 +82,7 @@ jobs:
require-healthy: true
- 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
if: ${{ !cancelled() }}
with:
@ -129,7 +129,6 @@ jobs:
- name: Set TEST_TAG to lowercase
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
- name: Checkout repository
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
@ -142,6 +141,9 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run the TypeScript build
run: pnpm run server:build
- name: Update build info
run: pnpm run chore:update-build-info
@ -184,7 +186,7 @@ jobs:
uses: docker/build-push-action@v6
with:
context: apps/server
file: ${{ matrix.dockerfile }}
file: apps/server/${{ matrix.dockerfile }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.meta.outputs.labels }}
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
run: pnpm install --frozen-lockfile
- run: npx playwright install --with-deps
- run: pnpx playwright install --with-deps
- uses: nrwl/nx-set-shas@v4
# Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud
# - 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
# 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

104
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)
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:
<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?
[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?
@ -20,7 +52,7 @@ 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.
## Documentation
## 📖 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.
@ -29,55 +61,40 @@ Below are some quick links for your convenience to navigate the documentation:
- [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
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)
- [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)
## 🎁 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.
- [Github Discussions](https://github.com/TriliumNext/Notes/discussions) (For asynchronous discussions.)
- [Github Issues](https://github.com/TriliumNext/Notes/issues) (For bug reports and feature requests.)
## 🏗 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.
* Access TriliumNext via the web interface of a server installation (see below)
* Currently only the latest versions of Chrome & Firefox are supported (and tested).
* TriliumNext is also provided as a Flatpak, but not yet published on FlatHub.
### Linux
If your distribution is listed in the table below, use your distribution's package.
[![Packaging status](https://repology.org/badge/vertical-allrepos/trilium-next-desktop.svg)](https://repology.org/project/trilium-next-desktop/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
@ -91,11 +108,6 @@ 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).
## 📝 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
@ -150,4 +162,6 @@ Support for the TriliumNext organization will be possible in the near future. Fo
## 🔑 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.

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

@ -38,10 +38,10 @@
"@playwright/test": "1.52.0",
"@stylistic/eslint-plugin": "4.2.0",
"@types/express": "5.0.1",
"@types/node": "22.15.17",
"@types/node": "22.15.21",
"@types/yargs": "17.0.33",
"@vitest/coverage-v8": "3.1.3",
"eslint": "9.26.0",
"@vitest/coverage-v8": "3.1.4",
"eslint": "9.27.0",
"eslint-plugin-simple-import-sort": "12.1.1",
"esm": "3.2.25",
"jsdoc": "4.0.4",

View File

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

View File

@ -283,6 +283,9 @@ export type CommandMappings = {
type EventMappings = {
initialRenderComplete: {};
frocaReloaded: {};
setLeftPaneVisibility: {
leftPaneVisible: boolean | null;
}
protectedSessionStarted: {};
notesReloaded: {
noteIds: string[];

View File

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

View File

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

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 glob from "./services/glob.js";
import "./stylesheets/bootstrap.scss";
import "boxicons/css/boxicons.min.css";
import "autocomplete.js/index_jquery.js";
glob.setupGlobs();

View File

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

View File

@ -1,7 +1,6 @@
import renderService from "./render.js";
import protectedSessionService from "./protected_session.js";
import protectedSessionHolder from "./protected_session_holder.js";
import libraryLoader from "./library_loader.js";
import openService from "./open.js";
import froca from "./froca.js";
import utils from "./utils.js";
@ -12,10 +11,11 @@ import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.js";
import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js";
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
import { normalizeMimeTypeForCKEditor } from "./mime_type_definitions.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 { renderMathInElement } from "./math.js";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
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));
if ($renderedContent.find("span.math-tex").length > 0) {
await libraryLoader.requireLibrary(libraryLoader.KATEX);
renderMathInElement($renderedContent[0], { trust: true });
}

View File

@ -48,5 +48,6 @@ function getUrl(docNameValue: string, language: string) {
// Cannot have spaces in the URL due to how JQuery.load works.
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

@ -1,7 +1,6 @@
import utils from "./utils.js";
import appContext from "../components/app_context.js";
import server from "./server.js";
import libraryLoader from "./library_loader.js";
import ws from "./ws.js";
import froca from "./froca.js";
import linkService from "./link.js";
@ -17,7 +16,6 @@ function setupGlobs() {
// required for ESLint plugin and CKEditor
window.glob.getActiveContextNote = () => appContext.tabManager.getActiveContextNote();
window.glob.requireLibrary = libraryLoader.requireLibrary;
window.glob.appContext = appContext; // for debugging
window.glob.froca = froca;
window.glob.treeCache = froca; // compatibility for CKEditor builds for a while
@ -64,7 +62,7 @@ function setupGlobs() {
});
for (const appCssNoteId of glob.appCssNoteIds || []) {
libraryLoader.requireCss(`api/notes/download/${appCssNoteId}`, false);
requireCss(`api/notes/download/${appCssNoteId}`, false);
}
utils.initHelpButtons($(window));
@ -76,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 {
setupGlobs
};

View File

@ -1,126 +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 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,
KATEX,
HIGHLIGHT_JS
};

View File

@ -58,6 +58,7 @@ export interface ViewScope {
* toc will appear and then close immediately, because getToc(html) function will consume time
*/
tocPreviousVisible?: boolean;
tocCollapsedHeadings?: Set<string>;
}
interface CreateLinkOptions {

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,223 +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: "GDScript (Godot)", mime: "text/x-gdscript" },
{ 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: "Nix", mime: "text/x-nix", highlightJs: "nix" },
{ 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";
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;
function loadMimeTypes() {
@ -45,8 +38,8 @@ export function getHighlightJsNameForMime(mimeType: string) {
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.
const normalizedMime = normalizeMimeTypeForCKEditor(mimeType.mime);
if (mimeType.highlightJs) {
mimeToHighlightJsMapping[normalizedMime] = mimeType.highlightJs;
if (mimeType.mdLanguageCode) {
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 { t } from "./i18n.js";
// Track all elements that open tooltips
let openTooltipElements: JQuery<HTMLElement>[] = [];
let dismissTimer: ReturnType<typeof setTimeout>;
function setupGlobalTooltip() {
$(document).on("mouseenter", "a", mouseEnterHandler);
@ -23,7 +27,12 @@ function setupGlobalTooltip() {
}
function dismissAllTooltips() {
$(".note-tooltip").remove();
clearTimeout(dismissTimer);
openTooltipElements.forEach($el => {
$el.tooltip("dispose");
$el.removeAttr("aria-describedby");
});
openTooltipElements = [];
}
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
// 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
if ($(this).filter(":hover").length > 0) {
$(this).tooltip({
if ($link.filter(":hover").length > 0) {
$link.tooltip({
container: "body",
// https://github.com/zadam/trilium/issues/2794 https://github.com/zadam/trilium/issues/2988
// with bottom this flickering happens a bit less
@ -103,7 +112,9 @@ async function mouseEnterHandler(this: HTMLElement) {
});
dismissAllTooltips();
$(this).tooltip("show");
$link.tooltip("show");
openTooltipElements.push($link);
// Dismiss the tooltip immediately if a link was clicked inside the tooltip.
$(`.${tooltipClass} a`).on("click", (e) => {
@ -115,15 +126,16 @@ async function mouseEnterHandler(this: HTMLElement) {
// 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
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
dismissAllTooltips();
} 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
.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 {

View File

@ -3,7 +3,11 @@ import Split from "split.js"
export const DEFAULT_GUTTER_SIZE = 5;
let leftPaneWidth: number;
let reservedPx: number;
let layoutOrientation: string;
let leftInstance: ReturnType<typeof Split> | null;
let rightPaneWidth: number;
let rightInstance: ReturnType<typeof Split> | null;
function setupLeftPaneResizer(leftPaneVisible: boolean) {
@ -14,27 +18,34 @@ function setupLeftPaneResizer(leftPaneVisible: boolean) {
$("#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) {
$("#rest-pane").css("width", "100%");
$("#rest-pane").css("width", layoutOrientation === "vertical" ? `${100 - reservedWidth}%` : "100%");
return;
}
let leftPaneWidth = options.getInt("leftPaneWidth");
leftPaneWidth = leftPaneWidth ?? (options.getInt("leftPaneWidth") ?? 0);
if (!leftPaneWidth || leftPaneWidth < 5) {
leftPaneWidth = 5;
}
const restPaneWidth = 100 - leftPaneWidth - reservedWidth;
if (leftPaneVisible) {
// 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,
// which would cause the minSize setting to have no effect.
requestAnimationFrame(() => {
leftInstance = Split(["#left-pane", "#rest-pane"], {
sizes: [leftPaneWidth, 100 - leftPaneWidth],
sizes: [leftPaneWidth, restPaneWidth],
gutterSize: DEFAULT_GUTTER_SIZE,
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;
}
let rightPaneWidth = options.getInt("rightPaneWidth");
rightPaneWidth = rightPaneWidth ?? (options.getInt("rightPaneWidth") ?? 0);
if (!rightPaneWidth || rightPaneWidth < 5) {
rightPaneWidth = 5;
}
@ -63,8 +74,11 @@ function setupRightPaneResizer() {
rightInstance = Split(["#center-pane", "#right-pane"], {
sizes: [100 - rightPaneWidth, rightPaneWidth],
gutterSize: DEFAULT_GUTTER_SIZE,
minSize: [ 300, 180 ],
onDragEnd: (sizes) => options.save("rightPaneWidth", Math.round(sizes[1]))
minSize: [300, 180],
onDragEnd: (sizes) => {
rightPaneWidth = Math.round(sizes[1]);
options.save("rightPaneWidth", Math.round(sizes[1]));
}
});
}
}

View File

@ -1,19 +1,8 @@
import library_loader from "./library_loader.js";
import { ensureMimeTypes, highlight, highlightAuto, loadTheme, Themes } from "@triliumnext/highlightjs";
import mime_types from "./mime_types.js";
import options from "./options.js";
export function getStylesheetUrl(theme: string) {
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;
}
let highlightingLoaded = false;
/**
* 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.
@ -25,6 +14,8 @@ export async function applySyntaxHighlight($container: JQuery<HTMLElement>) {
return;
}
await ensureMimeTypesForHighlighting();
const codeBlocks = $container.find("pre code");
for (const codeBlock of codeBlocks) {
const normalizedMimeType = extractLanguageFromClassList(codeBlock);
@ -43,20 +34,13 @@ export async function applySingleBlockSyntaxHighlight($codeBlock: JQuery<HTMLEle
$codeBlock.parent().toggleClass("hljs");
const text = $codeBlock.text();
if (!window.hljs) {
await library_loader.requireLibrary(library_loader.HIGHLIGHT_JS);
}
let highlightedText = null;
if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) {
highlightedText = hljs.highlightAuto(text);
await ensureMimeTypesForHighlighting();
highlightedText = highlightAuto(text);
} else if (normalizedMimeType) {
const language = mime_types.getHighlightJsNameForMime(normalizedMimeType);
if (language) {
highlightedText = hljs.highlight(text, { language });
} else {
console.warn(`Unknown mime type: ${normalizedMimeType}.`);
}
await ensureMimeTypesForHighlighting();
highlightedText = highlight(text, { language: normalizedMimeType });
}
if (highlightedText) {
@ -64,6 +48,35 @@ 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 = 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.
* @returns whether syntax highlighting should be enabled for code blocks.

View File

@ -1,3 +1,5 @@
import "jquery";
import "jquery-hotkeys";
import utils from "./services/utils.js";
import ko from "knockout";
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

View File

@ -15,6 +15,14 @@
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;
}
.table {
--bs-table-bg: transparent !important;
}
@ -590,11 +598,6 @@ table.promoted-attributes-in-tooltip th {
}
.tooltip-trigger {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: transparent;
pointer-events: none;
}
@ -1213,6 +1216,10 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
background-color: inherit;
}
::selection {
background-color: var(--selection-background-color);
}
[data-bs-toggle="tooltip"]:not(.button-widget) span {
padding-bottom: 0;
border-bottom: 1px dotted;
@ -1769,7 +1776,7 @@ body.zen .title-row {
height: unset !important;
-webkit-app-region: drag;
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 {
@ -2009,11 +2016,11 @@ footer.file-footer button {
left: 1em;
}
.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; }
.admonition.note { --accent-color: var(--admonition-note-accent-color); }
.admonition.tip { --accent-color: var(--admonition-tip-accent-color); }
.admonition.important { --accent-color: var(--admonition-important-accent-color); }
.admonition.caution { --accent-color: var(--admonition-caution-accent-color); }
.admonition.warning { --accent-color: var(--admonition-warning-accent-color); }
.admonition.note::before { content: "\eb21"; }
.admonition.tip::before { content: "\ea0d"; }

View File

@ -195,6 +195,8 @@
--scrollbar-background-color: transparent;
--scrollbar-border-color: unset; /* Deprecated */
--selection-background-color: #3399FF70;
--link-color: lightskyblue;
--mermaid-theme: dark;

View File

@ -194,6 +194,8 @@
--scrollbar-background-color: transparent;
--scrollbar-border-color: unset; /* Deprecated */
--selection-background-color: #3399FF70;
--link-color: blue;
--mermaid-theme: default;

View File

@ -73,7 +73,8 @@
}
/* 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;
background: transparent;
border-radius: 6px !important;
@ -81,12 +82,11 @@
}
/* Checked list item */
:root ul.ck.ck-list button.ck-button.ck-on:not(:hover) {
background: transparent !important;
}
: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);
color: var(--hover-item-color);
}
@ -99,6 +99,57 @@
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-color-base-border);
background: rgba(0, 0, 0, .025);
}
.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;
}
/* 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 button */
@ -112,12 +163,43 @@
/* Table dropdown */
.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-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-border-radius: 0;
}
/* 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 */
:root .ck-link-actions button.ck-button,

View File

@ -127,10 +127,12 @@ body.layout-horizontal > .horizontal {
--launcher-pane-button-gap: var(--launcher-pane-vert-button-gap);
width: var(--launcher-pane-size) !important;
min-width: var(--launcher-pane-size) !important;
padding-bottom: var(--launcher-pane-button-gap);
}
#launcher-pane.vertical #launcher-container {
width: var(--launcher-pane-size);
height: 100%;
overflow-x: hidden;
overflow-y: auto;
@ -1636,7 +1638,9 @@ div.find-replace-widget div.find-widget-found-wrapper > span {
#right-pane .toc li,
#right-pane .highlights-list li {
padding: 2px 8px;
padding-top: 2px;
padding-right: 8px;
padding-bottom: 2px;
border-radius: 4px;
text-align: unset;
transition:

View File

@ -1621,7 +1621,10 @@
"color-scheme": "颜色方案"
},
"code_block": {
"word_wrapping": "自动换行"
"word_wrapping": "自动换行",
"theme_none": "无语法高亮",
"theme_group_light": "浅色主题",
"theme_group_dark": "深色主题"
},
"classic_editor_toolbar": {
"title": "格式化"

View File

@ -1573,7 +1573,10 @@
"color-scheme": "Farbschema"
},
"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": {
"title": "Format"

View File

@ -1827,7 +1827,10 @@
"color-scheme": "Color Scheme"
},
"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"
},
"classic_editor_toolbar": {
"title": "Formatting"

View File

@ -1589,7 +1589,10 @@
"color-scheme": "Esquema de color"
},
"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": {
"title": "Formato"

View File

@ -1579,7 +1579,10 @@
"color-scheme": "Jeu de couleurs"
},
"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": {
"title": "Mise en forme"

View File

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

View File

@ -1585,7 +1585,10 @@
"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": {
"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": {
"title": "Formatare"

View File

@ -1519,7 +1519,10 @@
"color-scheme": "顏色方案"
},
"code_block": {
"word_wrapping": "自動換行"
"word_wrapping": "自動換行",
"theme_none": "無格式高亮",
"theme_group_light": "淺色主題",
"theme_group_dark": "深色主題"
},
"classic_editor_toolbar": {
"title": "格式化"

View File

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

View File

@ -24,3 +24,10 @@ declare module "draggabilly" {
declare module "@mind-elixir/node-menu" {
export default mindmap;
}
declare module "katex/contrib/auto-render" {
var renderMathInElement: (element: HTMLElement, options: {
trust: boolean;
}) => void;
export default renderMathInElement;
}

View File

@ -22,7 +22,6 @@ interface CustomGlobals {
getReferenceLinkTitle: (href: string) => Promise<string>;
getReferenceLinkTitleSync: (href: string) => string;
getActiveContextNote: () => FNote | null;
requireLibrary: typeof library_loader.requireLibrary;
ESLINT: Library;
appContext: AppContext;
froca: Froca;
@ -123,24 +122,6 @@ declare global {
var require: RequireMethod;
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;
var katex: {
renderToString(text: string, opts: {
throwOnError: boolean
});
}
/*
* Panzoom
*/

View File

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

View File

@ -53,10 +53,6 @@ const TPL = /*html*/`
pointer-events: none;
}
.update-to-latest-version-button {
display: none;
}
.global-menu .zoom-container {
display: flex;
flex-direction: row;
@ -235,7 +231,7 @@ const TPL = /*html*/`
${t("global_menu.about")}
</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="version-text"></span>

View File

@ -19,10 +19,10 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
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");
@ -32,16 +32,12 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
}
refreshIcon() {
if (document.hasFocus() || this.currentLeftPaneVisible === true) {
super.refreshIcon();
splitService.setupLeftPaneResizer(this.currentLeftPaneVisible);
}
super.refreshIcon();
splitService.setupLeftPaneResizer(this.currentLeftPaneVisible);
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("leftPaneVisible") && document.hasFocus()) {
this.currentLeftPaneVisible = options.is("leftPaneVisible");
this.refreshIcon();
}
setLeftPaneVisibilityEvent({ leftPaneVisible }: EventData<"setLeftPaneVisibility">) {
this.currentLeftPaneVisible = leftPaneVisible ?? !this.currentLeftPaneVisible;
this.refreshIcon();
}
}

View File

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

View File

@ -3,7 +3,6 @@ import utils from "../../services/utils.js";
import server from "../../services/server.js";
import toastService from "../../services/toast.js";
import appContext from "../../components/app_context.js";
import libraryLoader from "../../services/library_loader.js";
import openService from "../../services/open.js";
import protectedSessionHolder from "../../services/protected_session_holder.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 { NoteType } from "../../entities/fnote.js";
import { Dropdown, Modal } from "bootstrap";
import { renderMathInElement } from "../../services/math.js";
const TPL = /*html*/`
<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>`);
if (this.$content.find("span.math-tex").length > 0) {
await libraryLoader.requireLibrary(libraryLoader.KATEX);
renderMathInElement(this.$content[0], { trust: true });
}
} else if (revisionItem.type === "code") {

View File

@ -21,7 +21,7 @@ export default class FindInHtml {
}
async performFind(searchTerm: string, matchCase: boolean, wholeWord: boolean) {
await import("script-loader!mark.js/dist/jquery.mark.min.js");
await import("mark.js");
const $content = await this.parent?.noteContext?.getContentElement();
@ -42,7 +42,7 @@ export default class FindInHtml {
const containerTop = scrollingContainer?.getBoundingClientRect().top ?? 0;
const closestIndex = this.$results.toArray().findIndex(el => el.getBoundingClientRect().top >= containerTop);
this.currentIndex = closestIndex >= 0 ? closestIndex : 0;
await this.jumpTo();
res({

View File

@ -11,8 +11,8 @@ import RightPanelWidget from "./right_panel_widget.js";
import options from "../services/options.js";
import OnClickButtonWidget from "./buttons/onclick_button.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 katex from "../services/math.js";
const TPL = /*html*/`<div class="highlights-list-widget">
<style>
@ -175,7 +175,6 @@ export default class HighlightsListWidget extends RightPanelWidget {
} catch (e) {
if (e instanceof ReferenceError && e.message.includes("katex is not defined")) {
// Load KaTeX if it is not already loaded
await libraryLoader.requireLibrary(libraryLoader.KATEX);
try {
rendered = katex.renderToString(latexCode, {
throwOnError: false

View File

@ -3,12 +3,10 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
import protectedSessionHolder from "../services/protected_session_holder.js";
import SpacedUpdate from "../services/spaced_update.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 keyboardActionsService from "../services/keyboard_actions.js";
import noteCreateService from "../services/note_create.js";
import attributeService from "../services/attributes.js";
import attributeRenderer from "../services/attribute_renderer.js";
import EmptyTypeWidget from "./type_widgets/empty.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 AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.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 utils from "../services/utils.js";
import type { NoteType } from "../entities/fnote.js";

View File

@ -1,7 +1,6 @@
import { Dropdown } from "bootstrap";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import { getAvailableLocales, getLocaleById } from "../services/i18n.js";
import { t } from "i18next";
import { getAvailableLocales, getLocaleById, t } from "../services/i18n.js";
import type { EventData } from "../components/app_context.js";
import type FNote from "../entities/fnote.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 { TouchBarItem } from "../components/touch_bar.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*/`
<div class="tree-wrapper">

View File

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

View File

@ -19,7 +19,7 @@ import RightPanelWidget from "./right_panel_widget.js";
import options from "../services/options.js";
import OnClickButtonWidget from "./buttons/onclick_button.js";
import appContext, { type EventData } from "../components/app_context.js";
import libraryLoader from "../services/library_loader.js";
import katex from "../services/math.js";
import type FNote from "../entities/fnote.js";
const TPL = /*html*/`<div class="toc-widget">
@ -29,23 +29,88 @@ const TPL = /*html*/`<div class="toc-widget">
contain: none;
overflow: auto;
position: relative;
padding-left:0px !important;
}
.toc ol {
padding-left: 25px;
position: relative;
overflow: hidden;
padding-left: 0px;
transition: max-height 0.3s ease;
}
.toc > ol {
padding-left: 20px;
.toc li.collapsed + ol {
display:none;
}
.toc li + ol:before {
content: "";
position: absolute;
height: 100%;
border-left: 1px solid var(--main-border-color);
z-index: 10;
}
.toc li {
display: flex;
position: relative;
list-style: none;
align-items: center;
padding-left: 7px;
cursor: pointer;
text-align: justify;
word-wrap: break-word;
hyphens: auto;
}
.toc > ol {
--toc-depth-level: 1;
}
.toc > ol > ol {
--toc-depth-level: 2;
}
.toc > ol > ol > ol {
--toc-depth-level: 3;
}
.toc > ol > ol > ol > ol {
--toc-depth-level: 4;
}
.toc > ol > ol > ol > ol > ol {
--toc-depth-level: 5;
}
.toc > ol ol::before {
left: calc((var(--toc-depth-level) - 2) * 20px + 14px);
}
.toc li {
padding-left: calc((var(--toc-depth-level) - 1) * 20px + 4px);
}
.toc li .collapse-button {
display: flex;
position: relative;
width: 21px;
height: 21px;
flex-shrink: 0;
align-items: center;
justify-content: center;
transition: transform 0.3s ease;
}
.toc li.collapsed .collapse-button {
transform: rotate(-90deg);
}
.toc li .item-content {
margin-left: 25px;
flex: 1;
}
.toc li .collapse-button + .item-content {
margin-left: 4px;
}
.toc li:hover {
font-weight: bold;
}
@ -189,7 +254,6 @@ export default class TocWidget extends RightPanelWidget {
} catch (e) {
if (e instanceof ReferenceError && e.message.includes("katex is not defined")) {
// Load KaTeX if it is not already loaded
await libraryLoader.requireLibrary(libraryLoader.KATEX);
try {
rendered = katex.renderToString(latexCode, {
throwOnError: false
@ -231,6 +295,14 @@ export default class TocWidget extends RightPanelWidget {
// Note heading 2 is the first level Trilium makes available to the note
let curLevel = 2;
const $ols = [$toc];
let $previousLi: JQuery<HTMLElement> | undefined;
if (!(this.noteContext?.viewScope?.tocCollapsedHeadings instanceof Set)) {
this.noteContext!.viewScope!.tocCollapsedHeadings = new Set<string>();
}
const tocCollapsedHeadings = this.noteContext!.viewScope!.tocCollapsedHeadings as Set<string>;
const validHeadingKeys = new Set<string>(); // Used to clean up obsolete entries in tocCollapsedHeadings
let headingCount = 0;
for (let m = null, headingIndex = 0; (m = headingTagsRegex.exec(html)) !== null; headingIndex++) {
//
@ -244,6 +316,11 @@ export default class TocWidget extends RightPanelWidget {
const $ol = $("<ol>");
$ols[$ols.length - 1].append($ol);
$ols.push($ol);
if ($previousLi) {
const headingKey = `h${newLevel}_${headingIndex}_${$previousLi?.text().trim()}`;
this.setupCollapsibleHeading($ol, $previousLi, headingKey, tocCollapsedHeadings, validHeadingKeys);
}
}
} else if (levelDelta < 0) {
// Close as many lists as curLevel - newLevel
@ -259,10 +336,19 @@ export default class TocWidget extends RightPanelWidget {
//
const headingText = await this.replaceMathTextWithKatax(m[2]);
const $li = $("<li>").html(headingText);
$li.on("click", () => this.jumpToHeading(headingIndex));
const $itemContent = $('<div class="item-content">').html(headingText);
const $li = $("<li>").append($itemContent)
.on("click", () => this.jumpToHeading(headingIndex));
$ols[$ols.length - 1].append($li);
headingCount = headingIndex;
$previousLi = $li;
}
// Clean up unused entries in tocCollapsedHeadings
for (const key of tocCollapsedHeadings) {
if (!validHeadingKeys.has(key)) {
tocCollapsedHeadings.delete(key);
}
}
$toc = this.pullLeft($toc);
@ -286,7 +372,7 @@ export default class TocWidget extends RightPanelWidget {
const $first = $toc.children(":first");
if ($first[0].tagName !== "OL") {
if ($first[0].tagName.toLowerCase() !== "ol") {
break;
}
@ -320,6 +406,60 @@ export default class TocWidget extends RightPanelWidget {
headingElement?.scrollIntoView({ behavior: "smooth" });
}
async setupCollapsibleHeading($ol: JQuery<HTMLElement>, $previousLi: JQuery<HTMLElement>, headingKey: string, tocCollapsedHeadings: Set<string>, validHeadingKeys: Set<string>) {
if ($previousLi && $previousLi.find(".collapse-button").length === 0) {
const $collapseButton = $('<div class="collapse-button bx bx-chevron-down"></div>');
$previousLi.prepend($collapseButton);
// Restore the previous collapsed state
if (tocCollapsedHeadings?.has(headingKey)) {
$previousLi.addClass("collapsed");
validHeadingKeys.add(headingKey);
} else {
$previousLi.removeClass("collapsed");
}
$collapseButton.on("click", (event) => {
event.stopPropagation();
if ($previousLi.hasClass("animating")) return;
const willCollapse = !$previousLi.hasClass("collapsed");
$previousLi.addClass("animating");
if (willCollapse) { // Collapse
$ol.css("maxHeight", `${$ol.prop("scrollHeight")}px`);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
$ol.css("maxHeight", "0px");
$collapseButton.css("transform", "rotate(-90deg)");
});
});
setTimeout(() => {
$ol.css("maxHeight", "");
$previousLi.addClass("collapsed");
$previousLi.removeClass("animating");
}, 300);
} else { // Expand
$previousLi.removeClass("collapsed");
$ol.css("maxHeight", "0px");
requestAnimationFrame(() => {
$ol.css("maxHeight", `${$ol.prop("scrollHeight")}px`);
$collapseButton.css("transform", "");
});
setTimeout(() => {
$ol.css("maxHeight", "");
$previousLi.removeClass("animating");
}, 300);
}
if (willCollapse) { // Store collapsed headings
tocCollapsedHeadings!.add(headingKey);
} else {
tocCollapsedHeadings!.delete(headingKey);
}
});
}
}
async closeTocCommand() {
if (this.noteContext?.viewScope) {
this.noteContext.viewScope.tocTemporarilyHidden = true;

View File

@ -72,7 +72,7 @@ export default class AbstractCodeTypeWidget extends TypeWidget {
* @param the note that was changed.
* @param new content of the note.
*/
_update(note: FNote, content: string) {
_update(note: { mime: string }, content: string) {
this.codeEditor.setText(content);
this.codeEditor.setMimeType(note.mime);
this.codeEditor.clearHistory();

View File

@ -132,7 +132,7 @@ export default class ExcalidrawTypeWidget extends TypeWidget {
// currently required by excalidraw, in order to allows self-hosting fonts locally.
// this avoids making excalidraw load the fonts from an external CDN.
(window as any).EXCALIDRAW_ASSET_PATH = `${window.location.origin}/${asset_path}/app-dist/excalidraw/`;
(window as any).EXCALIDRAW_ASSET_PATH = `${new URL(import.meta.url).origin}/node_modules/@excalidraw/excalidraw/dist/prod`;
// temporary vars
this.currentNoteId = "";

View File

@ -1,11 +1,10 @@
import library_loader from "../../../services/library_loader.js";
import { ALLOWED_PROTOCOLS } from "../../../services/link.js";
import { MIME_TYPE_AUTO } from "../../../services/mime_type_definitions.js";
import { MIME_TYPE_AUTO } from "@triliumnext/commons";
import { getHighlightJsNameForMime } from "../../../services/mime_types.js";
import options from "../../../services/options.js";
import { isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
import { ensureMimeTypesForHighlighting, isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
import utils from "../../../services/utils.js";
import emojiDefinitionsUrl from "@triliumnext/ckeditor5/emoji_definitions/en.json?external";
import emojiDefinitionsUrl from "@triliumnext/ckeditor5/emoji_definitions/en.json?url";
const TEXT_FORMATTING_GROUP = {
label: "Text formatting",
@ -101,12 +100,12 @@ export function buildConfig() {
allowedProtocols: ALLOWED_PROTOCOLS
},
emoji: {
definitionsUrl: emojiDefinitionsUrl
definitionsUrl: new URL(import.meta.url).origin + emojiDefinitionsUrl
},
syntaxHighlighting: {
async loadHighlightJs() {
await library_loader.requireLibrary(library_loader.HIGHLIGHT_JS);
return hljs;
loadHighlightJs: async () => {
await ensureMimeTypesForHighlighting();
return await import("@triliumnext/highlightjs");
},
mapLanguageName: getHighlightJsNameForMime,
defaultMimeType: MIME_TYPE_AUTO,

View File

@ -1,6 +1,7 @@
import server from "../../../services/server.js";
import AbstractCodeTypeWidget from "../abstract_code_type_widget.js";
import type { EventData } from "../../../components/app_context.js";
import type { EditorConfig } from "@triliumnext/codemirror";
const TPL = /*html*/`<div style="height: 100%; display: flex; flex-direction: column;">
<style>
@ -21,9 +22,9 @@ export default class BackendLogWidget extends AbstractCodeTypeWidget {
private $refreshBackendLog!: JQuery<HTMLElement>;
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$editor = this.$widget.find(".backend-log-editor");
super.doRender();
}
async refresh() {
@ -38,9 +39,10 @@ export default class BackendLogWidget extends AbstractCodeTypeWidget {
this.refresh();
}
getExtraOpts(): Partial<CodeMirrorOpts> {
getExtraOpts(): Partial<EditorConfig> {
return {
readOnly: true
readOnly: true,
preferPerformance: true
};
}

View File

@ -40,7 +40,7 @@ import ShareSettingsOptions from "./options/other/share_settings.js";
import AiSettingsOptions from "./options/ai_settings.js";
import type FNote from "../../entities/fnote.js";
import type NoteContextAwareWidget from "../note_context_aware_widget.js";
import { t } from "i18next";
import { t } from "../../services/i18n.js";
import LanguageOptions from "./options/i18n/language.js";
import type BasicWidget from "../basic_widget.js";
import CodeTheme from "./options/code_notes/code_theme.js";

View File

@ -1,5 +1,4 @@
import { t } from "../../services/i18n.js";
import libraryLoader from "../../services/library_loader.js";
import noteAutocompleteService, { type Suggestion } from "../../services/note_autocomplete.js";
import mimeTypesService from "../../services/mime_types.js";
import utils, { hasTouchBar } from "../../services/utils.js";
@ -12,13 +11,13 @@ import appContext, { type CommandListenerData, type EventData } from "../../comp
import dialogService from "../../services/dialog.js";
import options from "../../services/options.js";
import toast from "../../services/toast.js";
import { normalizeMimeTypeForCKEditor } from "../../services/mime_type_definitions.js";
import { buildSelectedBackgroundColor } from "../../components/touch_bar.js";
import { buildConfig, buildToolbarConfig } from "./ckeditor/config.js";
import type FNote from "../../entities/fnote.js";
import { getMermaidConfig } from "../../services/mermaid.js";
import { PopupEditor, ClassicEditor, EditorWatchdog, type CKTextEditor, type MentionFeed, type WatchdogConfig } from "@triliumnext/ckeditor5";
import "@triliumnext/ckeditor5/index.css";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
const ENABLE_INSPECTOR = false;
@ -301,15 +300,18 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
async createEditor() {
await this.watchdog.create(this.$editor[0], {
placeholder: t("editable_text.placeholder"),
//@ts-ignore TODO: FIX TYPES
mention: mentionSetup,
mention: {
feeds: mentionSetup,
},
codeBlock: {
languages: buildListOfLanguages()
},
math: {
engine: "katex",
outputType: "span", // or script
lazyLoad: async () => await libraryLoader.requireLibrary(libraryLoader.KATEX),
lazyLoad: async () => {
(window as any).katex = (await import("../../services/math.js")).default
},
forceOutputType: false, // forces output to use outputType
enablePreview: true // Enable preview view
},

View File

@ -1,7 +1,7 @@
import OptionsWidget from "../options_widget.js";
import type { OptionMap } from "@triliumnext/commons";
import { getAvailableLocales } from "../../../../services/i18n.js";
import { t } from "i18next";
import { t } from "../../../../services/i18n.js";
const TPL = /*html*/`
<div class="options-section">

View File

@ -1,10 +1,11 @@
import type { OptionMap } from "@triliumnext/commons";
import { normalizeMimeTypeForCKEditor, type OptionMap } from "@triliumnext/commons";
import { t } from "../../../../services/i18n.js";
import library_loader from "../../../../services/library_loader.js";
import server from "../../../../services/server.js";
import OptionsWidget from "../options_widget.js";
import { ensureMimeTypesForHighlighting, loadHighlightingTheme } from "../../../../services/syntax_highlight.js";
import { Themes, type Theme } from "@triliumnext/highlightjs";
const SAMPLE_LANGUAGE = "javascript";
const SAMPLE_LANGUAGE = normalizeMimeTypeForCKEditor("application/javascript;env=frontend");
const SAMPLE_CODE = `\
const n = 10;
greet(n); // Print "Hello World" for n times
@ -55,14 +56,6 @@ const TPL = /*html*/`
</div>
`;
// TODO: Deduplicate
interface Theme {
title: string;
val: string;
}
type Response = Record<string, Theme[]>;
/**
* Contains appearance settings for code blocks within text notes, such as the theme for the syntax highlighter.
*/
@ -75,9 +68,31 @@ export default class CodeBlockOptions extends OptionsWidget {
doRender() {
this.$widget = $(TPL);
this.$themeSelect = this.$widget.find(".theme-select");
// Populate the list of themes.
const themeGroups = groupThemesByLightOrDark();
for (const [key, themes] of Object.entries(themeGroups)) {
const $group = key ? $("<optgroup>").attr("label", key) : null;
for (const theme of themes) {
const option = $("<option>")
.attr("value", theme.val)
.text(theme.title);
if ($group) {
$group.append(option);
} else {
this.$themeSelect.append(option);
}
}
if ($group) {
this.$themeSelect.append($group);
}
}
this.$themeSelect.on("change", async () => {
const newTheme = String(this.$themeSelect.val());
library_loader.loadHighlightingTheme(newTheme);
loadHighlightingTheme(newTheme);
await server.put(`options/codeBlockTheme/${newTheme}`);
});
@ -91,11 +106,14 @@ export default class CodeBlockOptions extends OptionsWidget {
#setupPreview(shouldEnableSyntaxHighlight: boolean) {
const text = SAMPLE_CODE;
if (shouldEnableSyntaxHighlight) {
library_loader.requireLibrary(library_loader.HIGHLIGHT_JS).then(() => {
import("@triliumnext/highlightjs").then(async (hljs) => {
await ensureMimeTypesForHighlighting();
const highlightedText = hljs.highlight(text, {
language: SAMPLE_LANGUAGE
});
this.$sampleEl.html(highlightedText.value);
if (highlightedText) {
this.$sampleEl.html(highlightedText.value);
}
});
} else {
this.$sampleEl.text(text);
@ -103,25 +121,6 @@ export default class CodeBlockOptions extends OptionsWidget {
}
async optionsLoaded(options: OptionMap) {
const themeGroups = await server.get<Response>("options/codeblock-themes");
this.$themeSelect.empty();
for (const [key, themes] of Object.entries(themeGroups)) {
const $group = key ? $("<optgroup>").attr("label", key) : null;
for (const theme of themes) {
const option = $("<option>").attr("value", theme.val).text(theme.title);
if ($group) {
$group.append(option);
} else {
this.$themeSelect.append(option);
}
}
if ($group) {
this.$themeSelect.append($group);
}
}
this.$themeSelect.val(options.codeBlockTheme);
this.setCheckboxState(this.$wordWrap, options.codeBlockWordWrap);
this.$widget.closest(".note-detail-printable").toggleClass("word-wrap", options.codeBlockWordWrap === "true");
@ -129,3 +128,38 @@ export default class CodeBlockOptions extends OptionsWidget {
this.#setupPreview(options.codeBlockTheme !== "none");
}
}
interface ThemeData {
val: string;
title: string;
}
function groupThemesByLightOrDark() {
const darkThemes: ThemeData[] = [];
const lightThemes: ThemeData[] = [];
for (const [ id, theme ] of Object.entries(Themes)) {
const data: ThemeData = {
val: "default:" + id,
title: theme.name
};
if (theme.name.includes("Dark")) {
darkThemes.push(data);
} else {
lightThemes.push(data);
}
}
const output: Record<string, ThemeData[]> = {
"": [
{
val: "none",
title: t("code_block.theme_none")
}
]
};
output[t("code_block.theme_group_light")] = lightThemes;
output[t("code_block.theme_group_dark")] = darkThemes;
return output;
}

View File

@ -1,11 +1,11 @@
import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
import libraryLoader from "../../services/library_loader.js";
import { applySyntaxHighlight } from "../../services/syntax_highlight.js";
import type FNote from "../../entities/fnote.js";
import type { CommandListenerData, EventData } from "../../components/app_context.js";
import { getLocaleById } from "../../services/i18n.js";
import appContext from "../../components/app_context.js";
import { getMermaidConfig } from "../../services/mermaid.js";
import { renderMathInElement } from "../../services/math.js";
const TPL = /*html*/`
<div class="note-detail-readonly-text note-detail-printable" tabindex="100">
@ -121,8 +121,6 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
});
if (this.$content.find("span.math-tex").length > 0) {
await libraryLoader.requireLibrary(libraryLoader.KATEX);
renderMathInElement(this.$content[0], { trust: true });
}

View File

@ -215,7 +215,7 @@ class ListOrGridView extends ViewMode {
const highlightedTokens = this.parentNote.highlightedTokens || [];
if (highlightedTokens.length > 0) {
await import("script-loader!mark.js/dist/jquery.mark.min.js");
await import("mark.js");
const regex = highlightedTokens.map((token) => utils.escapeRegExp(token)).join("|");

View File

@ -34,6 +34,9 @@
"src/**/*.ts"
],
"references": [
{
"path": "../../packages/highlightjs/tsconfig.lib.json"
},
{
"path": "../../packages/codemirror/tsconfig.lib.json"
},

View File

@ -3,6 +3,9 @@
"files": [],
"include": [],
"references": [
{
"path": "../../packages/highlightjs"
},
{
"path": "../../packages/codemirror"
},

View File

@ -1,21 +1,94 @@
/// <reference types='vitest' />
import { join, resolve } from 'path';
import { defineConfig } from 'vite';
import { viteStaticCopy } from 'vite-plugin-static-copy'
import asset_path from './src/asset_path';
const assets = [ "assets", "stylesheets", "libraries", "fonts", "translations" ];
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/client',
plugins: [],
test: {
watch: false,
globals: true,
setupFiles: ["./src/test/setup.ts"],
environment: "happy-dom",
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
reporters: ["default"],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
reporter: [ "text", "html" ]
base: "/" + asset_path,
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [
viteStaticCopy({
targets: assets.map((asset) => ({
src: `src/${asset}/**/*`,
dest: asset
}))
}),
viteStaticCopy({
structured: true,
targets: [
{
src: "node_modules/@excalidraw/excalidraw/dist/prod/fonts/*",
dest: "",
}
]
})
],
resolve: {
alias: [
// Force the use of dist in development mode because upstream ESM is broken (some hybrid between CJS and ESM, will be improved in upcoming versions).
{
find: "@triliumnext/highlightjs",
replacement: resolve(__dirname, "node_modules/@triliumnext/highlightjs/dist")
}
]
},
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
target: "esnext",
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
rollupOptions: {
input: {
desktop: join(__dirname, "src", "desktop.ts"),
mobile: join(__dirname, "src", "mobile.ts"),
login: join(__dirname, "src", "login.ts"),
setup: join(__dirname, "src", "setup.ts"),
share: join(__dirname, "src", "share.ts"),
set_password: join(__dirname, "src", "set_password.ts"),
runtime: join(__dirname, "src", "runtime.ts")
},
output: {
entryFileNames: "src/[name].js",
chunkFileNames: "src/[name].js",
assetFileNames: "src/[name].[ext]"
},
onwarn(warning, rollupWarn) {
if (warning.code === "MODULE_LEVEL_DIRECTIVE") {
return;
}
rollupWarn(warning);
}
}
},
optimizeDeps: {
exclude: [
"@triliumnext/highlightjs"
]
},
css: {
preprocessorOptions: {
scss: {
quietDeps: true
}
}
},
commonjsOptions: {
transformMixedEsModules: true,
}
}));

View File

@ -1,116 +0,0 @@
const { composePlugins, withNx, withWeb } = require('@nx/webpack');
const { join } = require('path');
const CopyPlugin = require('copy-webpack-plugin');
module.exports = composePlugins(
withNx({
tsConfig: join(__dirname, './tsconfig.app.json'),
compiler: "tsc",
main: join(__dirname, "./src/index.ts"),
additionalEntryPoints: [
{
entryName: "desktop",
entryPath: join(__dirname, "./src/desktop.ts")
},
{
entryName: "mobile",
entryPath: join(__dirname, "./src/mobile.ts")
},
{
entryName: "login",
entryPath: join(__dirname, "./src/login.ts")
},
{
entryName: "setup",
entryPath: join(__dirname, "./src/setup.ts")
},
{
entryName: "share",
entryPath: join(__dirname, "./src/share.ts")
},
{
// TriliumNextTODO: integrate set_password into setup entry point/view
entryName: "set_password",
entryPath: join(__dirname, "./src/set_password.ts")
}
],
externalDependencies: [
"electron"
],
baseHref: '/',
outputHashing: false,
optimization: process.env['NODE_ENV'] === 'production'
}),
withWeb({
styles: [],
stylePreprocessorOptions: {
sassOptions: {
quietDeps: true
}
},
}),
(config) => {
config.output = {
path: join(__dirname, 'dist')
};
config.devServer = {
port: 4200,
client: {
overlay: {
errors: true,
warnings: false,
runtimeErrors: true
}
}
}
config.resolve.fallback = {
path: false,
fs: false,
util: false
};
const assets = [ "assets", "stylesheets", "libraries", "fonts", "translations" ]
config.plugins.push(new CopyPlugin({
patterns: assets.map((asset) => ({
from: join(__dirname, "src", asset),
to: asset
}))
}));
inlineSvg(config);
externalJson(config);
return config;
}
);
function inlineSvg(config) {
if (!config.module?.rules) {
return;
}
// Alter Nx's asset rule to avoid inlining SVG if they have ?raw prepended.
const existingRule = config.module.rules.find((r) => r.test.toString() === /\.svg$/.toString());
existingRule.resourceQuery = { not: [/raw/] };
// Add a rule for prepending ?raw SVGs.
config.module.rules.push({
resourceQuery: /raw/,
type: 'asset/source',
});
}
function externalJson(config) {
if (!config.module?.rules) {
return;
}
// Add a rule for prepending ?external.
config.module.rules.push({
resourceQuery: /external/,
type: 'asset/resource',
});
}

View File

@ -5,7 +5,7 @@
"description": "Tool to compare content of Trilium databases. Useful for debugging sync problems.",
"dependencies": {
"colors": "1.4.0",
"diff": "7.0.0",
"diff": "8.0.1",
"sqlite": "5.1.1",
"sqlite3": "5.1.7"
},
@ -67,8 +67,5 @@
}
}
}
},
"devDependencies": {
"@types/diff": "^7.0.2"
}
}

View File

@ -11,14 +11,13 @@
"electron-dl": "4.0.0",
"electron-squirrel-startup": "1.0.1",
"jquery.fancytree": "2.38.5",
"jquery-hotkeys": "0.2.2",
"@highlightjs/cdn-assets": "11.11.1"
"jquery-hotkeys": "0.2.2"
},
"devDependencies": {
"@types/electron-squirrel-startup": "1.0.2",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.0",
"electron": "36.2.0",
"electron": "36.2.1",
"@electron-forge/cli": "7.8.1",
"@electron-forge/maker-deb": "7.8.1",
"@electron-forge/maker-dmg": "7.8.1",
@ -32,7 +31,7 @@
"config": {
"forge": "../electron-forge/forge.config.cjs"
},
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39",
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
"scripts": {
"start-prod": "nx build desktop && cross-env TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=dist TRILIUM_PORT=37841 electron dist/main.js"
},
@ -57,7 +56,7 @@
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist"
},
"nixos": {
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist $(nix-shell -p electron_33 --run \"electron --version\")"
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist $(nix-shell -p electron_35 --run \"electron --version\")"
}
}
},
@ -73,7 +72,7 @@
"cwd": "{projectRoot}/dist"
},
"nixos": {
"command": "nix-shell -p electron_33 --run \"electron {projectRoot}/dist/main.js\"",
"command": "nix-shell -p electron_35 --run \"electron {projectRoot}/dist/main.js\"",
"cwd": ".",
"forwardAllArgs": false
}
@ -91,7 +90,7 @@
"cwd": "{projectRoot}/dist"
},
"nixos": {
"command": "nix-shell -p electron_33 --run \"electron {projectRoot}/dist/main.js\"",
"command": "nix-shell -p electron_35 --run \"electron {projectRoot}/dist/main.js\"",
"cwd": ".",
"forwardAllArgs": false
}

View File

@ -8,7 +8,7 @@
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.0",
"electron": "36.2.0",
"electron": "36.2.1",
"fs-extra": "11.3.0"
},
"nx": {

View File

@ -22,12 +22,12 @@ export default defineConfig({
trace: 'on-first-retry',
},
/* Run your local dev server before starting the tests */
webServer: {
webServer: !process.env.TRILIUM_DOCKER ? {
command: 'pnpm server:start-prod',
url: baseURL,
reuseExistingServer: !process.env.CI,
cwd: workspaceRoot
},
} : undefined,
projects: [
{
name: "chromium",

View File

@ -1,2 +1,3 @@
TRILIUM_ENV=production
TRILIUM_DATA_DIR=./apps/server/data
TRILIUM_DATA_DIR=./apps/server/data
TRILIUM_PORT=8082

View File

@ -1,4 +1,4 @@
FROM node:22.15.0-bullseye-slim AS builder
FROM node:22.15.1-bullseye-slim AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@ -7,7 +7,7 @@ FROM node:22.15.0-bullseye-slim AS builder
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:22.15.0-bullseye-slim
FROM node:22.15.1-bullseye-slim
# Install only runtime dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \

View File

@ -1,4 +1,4 @@
FROM node:22.15.0-alpine AS builder
FROM node:22.15.1-alpine AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@ -7,7 +7,7 @@ FROM node:22.15.0-alpine AS builder
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:22.15.0-alpine
FROM node:22.15.1-alpine
# Install runtime dependencies
RUN apk add --no-cache su-exec shadow

View File

@ -4,14 +4,10 @@
"description": "The server-side component of TriliumNext, which exposes the client via the web, allows for sync and provides a REST API for both internal and external use.",
"private": true,
"dependencies": {
"better-sqlite3": "11.10.0",
"jquery.fancytree": "2.38.5",
"jquery-hotkeys": "0.2.2",
"@highlightjs/cdn-assets": "11.11.1"
"better-sqlite3": "11.10.0"
},
"devDependencies": {
"@electron/remote": "2.1.2",
"@excalidraw/excalidraw": "0.18.0",
"@types/archiver": "6.0.3",
"@types/better-sqlite3": "7.6.13",
"@types/cls-hooked": "4.3.9",
@ -42,13 +38,8 @@
"@types/turndown": "5.0.5",
"@types/ws": "8.18.1",
"@types/xml2js": "0.4.14",
"autocomplete.js": "0.38.1",
"boxicons": "2.1.4",
"express-http-proxy": "2.1.1",
"jquery": "3.7.1",
"katex": "0.16.22",
"normalize.css": "8.0.1",
"@anthropic-ai/sdk": "0.50.4",
"@anthropic-ai/sdk": "0.51.0",
"@braintree/sanitize-url": "7.1.1",
"@triliumnext/commons": "workspace:*",
"@triliumnext/express-partial-content": "workspace:*",
@ -68,7 +59,7 @@
"debounce": "2.2.0",
"debug": "4.4.1",
"ejs": "3.1.10",
"electron": "36.2.0",
"electron": "36.2.1",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
@ -83,28 +74,27 @@
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"i18next": "25.1.2",
"i18next": "25.2.0",
"i18next-fs-backend": "2.6.0",
"image-type": "5.2.0",
"image-type": "6.0.0",
"ini": "5.0.0",
"is-animated": "2.0.2",
"is-svg": "6.0.0",
"jimp": "1.6.0",
"js-yaml": "4.1.0",
"jsdom": "26.1.0",
"marked": "15.0.11",
"marked": "15.0.12",
"mime-types": "3.0.1",
"multer": "1.4.5-lts.2",
"multer": "2.0.0",
"normalize-strings": "1.1.1",
"ollama": "0.5.15",
"openai": "4.98.0",
"openai": "4.100.0",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
"sanitize-html": "2.16.0",
"sanitize-html": "2.17.0",
"sax": "1.4.1",
"serve-favicon": "2.5.0",
"session-file-store": "1.5.0",
"stream-throttle": "0.1.3",
"strip-bom": "5.0.0",
"striptags": "3.2.0",
@ -115,7 +105,7 @@
"tmp": "0.2.3",
"turndown": "7.2.0",
"unescape": "1.0.1",
"webpack": "5.99.8",
"webpack": "5.99.9",
"ws": "8.18.2",
"xml2js": "0.6.2",
"yauzl": "3.2.0",

View File

@ -1,6 +1,7 @@
import { beforeAll } from "vitest";
import i18next from "i18next";
import { join } from "path";
import dayjs from "dayjs";
beforeAll(async () => {
// Initialize the translations manually to avoid any side effects.
@ -15,4 +16,8 @@ beforeAll(async () => {
loadPath: join(__dirname, "../src/assets/translations/{{lng}}/{{ns}}.json")
}
});
// Initialize dayjs
await import("dayjs/locale/en.js");
dayjs.locale("en");
});

View File

@ -4,9 +4,8 @@ import favicon from "serve-favicon";
import cookieParser from "cookie-parser";
import helmet from "helmet";
import compression from "compression";
import sessionParser from "./routes/session_parser.js";
import config from "./services/config.js";
import utils, { getResourceDir } from "./services/utils.js";
import utils, { getResourceDir, isDev } from "./services/utils.js";
import assets from "./routes/assets.js";
import routes from "./routes/routes.js";
import custom from "./routes/custom.js";
@ -64,7 +63,7 @@ export default async function buildApp() {
console.log("Database not initialized yet. LLM features will be initialized after setup.");
}
const publicDir = path.join(getResourceDir(), "public");
const publicDir = isDev ? path.join(getResourceDir(), "../dist/public") : path.join(getResourceDir(), "public");
const publicAssetsDir = path.join(publicDir, "assets");
const assetsDir = RESOURCE_DIR;
@ -111,6 +110,8 @@ export default async function buildApp() {
app.use(`/manifest.webmanifest`, express.static(path.join(publicAssetsDir, "manifest.webmanifest")));
app.use(`/robots.txt`, express.static(path.join(publicAssetsDir, "robots.txt")));
app.use(`/icon.png`, express.static(path.join(publicAssetsDir, "icon.png")));
const sessionParser = (await import("./routes/session_parser.js")).default;
app.use(sessionParser);
app.use(favicon(path.join(assetsDir, "icon.ico")));

View File

@ -0,0 +1,5 @@
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
data TEXT,
expires INTEGER
);

View File

@ -187,3 +187,9 @@ CREATE TABLE IF NOT EXISTS "embedding_providers" (
"dateModified" TEXT NOT NULL,
"utcDateModified" TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
data TEXT,
expires INTEGER
);

View File

@ -1,5 +1,3 @@
<p><strong>Note: This feature has not been merged yet, so it is not available.</strong>
</p>
<p>Multi-factor authentication (MFA) is a security process that requires
users to provide two or more verification factors to gain access to a system,
application, or account. This adds an extra layer of protection beyond
@ -7,80 +5,60 @@
<p>By requiring more than one verification method, MFA helps reduce the risk
of unauthorized access, even if someone has obtained your password. Its
highly recommended for securing sensitive information stored in your notes.</p>
<p>Warning! OpenID and TOTP cannot be both used at the same time!</p>
<h2>Log in with your Google Account with OpenID!</h2>
<p>OpenID is a standardized way to let you log into websites using an account
from another service, like Google, to verify your identity.</p>
<h2>Why Time-based One Time Passwords?</h2>
<p>TOTP (Time-Based One-Time Password) is a security feature that generates
a unique, temporary code on your device, like a smartphone, which changes
every 30 seconds. You use this code, along with your password, to log into
your account, making it much harder for anyone else to access them.</p>
<h2>Setup</h2>
<h3>TOTP</h3>
<ol>
<li>
<p>Start Trilium Notes normally.</p>
</li>
<li>
<p>Go to "Menu" -&gt; "Options" -&gt; "MFA"</p>
</li>
<li>
<p>Click the "Generate TOTP Secret" button</p>
</li>
<li>
<p>Copy the generated secret to your authentication app/extension</p>
</li>
<li>
<p>Set an environment variable "TOTP_SECRET" as the generated secret. Environment
variables can be set with a .env file in the root directory, by defining
them in the command line, or with a docker container.</p><pre><code class="language-text-x-trilium-auto"># .env in the project root directory
TOTP_ENABLED="true"
TOTP_SECRET="secret"</code></pre><pre><code class="language-text-x-trilium-auto"># Terminal/CLI
export TOTP_ENABLED="true"
export TOTP_SECRET="secret"</code></pre><pre><code class="language-text-x-trilium-auto"># Docker
docker run -p 8080:8080 -v ~/trilium-data:/home/node/trilium-data -e TOTP_ENABLED="true" -e TOTP_SECRET="secret" triliumnext/notes:[VERSION]</code></pre>
</li>
<li>
<p>Restart Trilium</p>
</li>
<li>
<p>Go to "Options" -&gt; "MFA"</p>
</li>
<li>
<p>Click the "Generate Recovery Codes" button</p>
</li>
<li>
<p>Save the recovery codes. Recovery codes can be used once in place of the
<aside
class="admonition warning">
<p>OpenID and TOTP cannot be both used at the same time!</p>
</aside>
<h2>Log in with your Google Account with OpenID!</h2>
<p>OpenID is a standardized way to let you log into websites using an account
from another service, like Google, to verify your identity.</p>
<h2>Why Time-based One Time Passwords?</h2>
<p>TOTP (Time-Based One-Time Password) is a security feature that generates
a unique, temporary code on your device, like a smartphone, which changes
every 30 seconds. You use this code, along with your password, to log into
your account, making it much harder for anyone else to access them.</p>
<h2>Setup</h2>
<p>MFA can only be set up on a server instance.</p>
<aside class="admonition note">
<p>When Multi-Factor Authentication (MFA) is enabled on a server instance,
a new desktop instance may fail to sync with it. As a temporary workaround,
you can disable MFA to complete the initial sync, then re-enable MFA afterward.
This issue will be addressed in a future release.</p>
</aside>
<h3>TOTP</h3>
<ol>
<li>Go to "Menu" -&gt; "Options" -&gt; "MFA"</li>
<li>Click the “Enable Multi-Factor Authentication” checkbox if not checked</li>
<li>Choose “Time-Based One-Time Password (TOTP)” under MFA Method</li>
<li>Click the "Generate TOTP Secret" button</li>
<li>Copy the generated secret to your authentication app/extension</li>
<li>Click the "Generate Recovery Codes" button</li>
<li>Save the recovery codes. Recovery codes can be used once in place of the
TOTP if you loose access to your authenticator. After a rerecovery code
is used, it will show the unix timestamp when it was used in the MFA options
tab.</p>
</li>
<li>
<p>Load the secret into an authentication app like google authenticator</p>
</li>
</ol>
<h3>OpenID</h3>
<p><em>Currently only compatible with Google. Other services like Authentik and Auth0 are planned on being added.</em>
</p>
<p>In order to setup OpenID, you will need to setup a authentication provider.
This requires a bit of extra setup. Follow <a href="https://developers.google.com/identity/openid-connect/openid-connect">these instructions</a> to
setup an OpenID service through google.</p>
<p>Set an environment variable "SSO_ENABLED" to true and add the client ID
and secret you obtained from google. Environment variables can be set with
a .env file in the root directory, by defining them in the command line,
or with a docker container.</p>
<h4>.env File</h4><pre><code class="language-text-x-trilium-auto"># .env in the project root directory
SSO_ENABLED="true"
BASE_URL="http://localhost:8080"
CLIENT_ID=
SECRET=</code></pre>
<h4>Environment variable (linux)</h4><pre><code class="language-text-x-trilium-auto">export SSO_ENABLED="true"
export BASE_URL="http://localhost:8080"
export CLIENT_ID=
export SECRET=</code></pre>
<h4>Docker</h4><pre><code class="language-text-x-trilium-auto">docker run -d -p 8080:8080 -v ~/trilium-data:/home/node/trilium-data -e SSO_ENABLED="true" -e BASE_URL="http://localhost:8080" -e CLIENT_ID= -e SECRET= triliumnext/notes:[VERSION]</code></pre>
<p>After you restart Trilium Notes, you will be redirected to Google's account
selection page. Login to an account and Trilium Next will bind to that
account, allowing you to login with it.</p>
<p>You can now login using your google account.</p>
tab.</li>
<li>Re-login will be required after TOTP setup is finished (After you refreshing
the page).</li>
</ol>
<h3>OpenID</h3>
<aside class="admonition note">
<p>Currently only compatible with Google. Other services like Authentik and
Auth0 are planned on being added.</p>
</aside>
<p>In order to setup OpenID, you will need to setup a authentication provider.
This requires a bit of extra setup. Follow <a href="https://developers.google.com/identity/openid-connect/openid-connect">these instructions</a> to
setup an OpenID service through google.</p>
<ol>
<li>Set the <code>oauthBaseUrl</code>, <code>oauthClientId</code> and <code>oauthClientSecret</code> in
the <code>config.ini</code> file (check&nbsp;<a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a>&nbsp;for
more information).
<ol>
<li>You can also setup through environment variables (<code>TRILIUM_OAUTH_BASE_URL</code>, <code>TRILIUM_OAUTH_CLIENT_ID</code> and <code>TRILIUM_OAUTH_CLIENT_SECRET</code>).</li>
</ol>
</li>
<li>Restart the server</li>
<li>Go to "Menu" -&gt; "Options" -&gt; "MFA"</li>
<li>Click the “Enable Multi-Factor Authentication” checkbox if not checked</li>
<li>Choose “OAuth/OpenID” under MFA Method</li>
<li>Refresh the page and login through OpenID provider</li>
</ol>

View File

@ -40,7 +40,7 @@
<h2>Color schemes</h2>
<p>Since Trilium 0.94.0 the colors of code notes can be customized by going&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_4TIF1oA4VQRO">Options</a>&nbsp;→ Code Notes and looking for the <em>Appearance</em> section.</p>
class="reference-link" href="#root/_help_4TIF1oA4VQRO">Options</a>&nbsp;→ Code Notes and looking for the <em>Appearance</em> section.</p>
<aside
class="admonition note">
<p><strong>Why are there only a few themes whereas the code block themes for text notes have a lot?</strong>

View File

@ -193,11 +193,6 @@
"special_notes": {
"search_prefix": "搜索:"
},
"code_block": {
"theme_none": "无语法高亮",
"theme_group_light": "浅色主题",
"theme_group_dark": "深色主题"
},
"test_sync": {
"not-configured": "同步服务器主机未配置。请先配置同步。",
"successful": "同步服务器握手成功,同步已开始。"

View File

@ -185,11 +185,6 @@
"special_notes": {
"search_prefix": "Suche:"
},
"code_block": {
"theme_none": "Keine Syntax-Hervorhebung",
"theme_group_light": "Helle Themen",
"theme_group_dark": "Dunkle Themen"
},
"test_sync": {
"not-configured": "Der Synchronisations-Server-Host ist nicht konfiguriert. Bitte konfiguriere zuerst die Synchronisation.",
"successful": "Die Server-Verbindung wurde erfolgreich hergestellt, die Synchronisation wurde gestartet."

View File

@ -193,11 +193,6 @@
"special_notes": {
"search_prefix": "Search:"
},
"code_block": {
"theme_none": "No syntax highlighting",
"theme_group_light": "Light themes",
"theme_group_dark": "Dark themes"
},
"test_sync": {
"not-configured": "Sync server host is not configured. Please configure sync first.",
"successful": "Sync server handshake has been successful, sync has been started."

View File

@ -189,11 +189,6 @@
"special_notes": {
"search_prefix": "Buscar:"
},
"code_block": {
"theme_none": "Sin resaltado de sintaxis",
"theme_group_light": "Temas claros",
"theme_group_dark": "Temas oscuros"
},
"test_sync": {
"not-configured": "El servidor de sincronización no está configurado. Por favor configure primero la sincronización.",
"successful": "El protocolo de enlace del servidor de sincronización ha sido exitoso, la sincronización ha comenzado."

View File

@ -189,11 +189,6 @@
"special_notes": {
"search_prefix": "Recherche :"
},
"code_block": {
"theme_none": "Pas de coloration syntaxique",
"theme_group_light": "Thèmes clairs",
"theme_group_dark": "Thèmes sombres"
},
"test_sync": {
"not-configured": "L'hôte du serveur de synchronisation n'est pas configuré. Veuillez d'abord configurer la synchronisation.",
"successful": "L'établissement de liaison du serveur de synchronisation a été réussi, la synchronisation a été démarrée."

View File

@ -186,11 +186,6 @@
"special_notes": {
"search_prefix": "Pesquisar:"
},
"code_block": {
"theme_none": "Sem destaque de sintaxe",
"theme_group_light": "Temas claros",
"theme_group_dark": "Temas escuros"
},
"test_sync": {
"not-configured": "O host do servidor de sincronização não está configurado. Por favor, configure a sincronização primeiro.",
"successful": "A comunicação com o servidor de sincronização foi bem-sucedida, a sincronização foi iniciada."

View File

@ -189,11 +189,6 @@
"special_notes": {
"search_prefix": "Căutare:"
},
"code_block": {
"theme_none": "Fără evidențiere de sintaxă",
"theme_group_dark": "Teme întunecate",
"theme_group_light": "Teme luminoase"
},
"test_sync": {
"not-configured": "Calea către serverul de sincronizare nu este configurată. Configurați sincronizarea înainte.",
"successful": "Comunicarea cu serverul de sincronizare a avut loc cu succes, s-a început sincronizarea."

View File

@ -185,11 +185,6 @@
"special_notes": {
"search_prefix": "搜尋:"
},
"code_block": {
"theme_none": "無格式高亮",
"theme_group_light": "淺色主題",
"theme_group_dark": "深色主題"
},
"test_sync": {
"not-configured": "並未設定同步伺服器主機,請先設定同步",
"successful": "成功與同步伺服器握手,現在開始同步"

View File

@ -7,8 +7,6 @@
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<% // TriliumNextTODO: move the css file to ${assetPath}/stylesheets/ %>
<link rel="stylesheet" href="<%= appPath %>/desktop.css">
<title>TriliumNext Notes</title>
</head>
<body class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %> <%= hasBackgroundEffects ? 'background-effects' : '' %>">
@ -34,17 +32,7 @@
<!-- Required for correct loading of scripts in Electron -->
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script src="<%= assetPath %>/node_modules/jquery/dist/jquery.min.js"></script>
<!-- Include Fancytree library and skip -->
<link href="<%= assetPath %>/stylesheets/tree.css" rel="stylesheet">
<script src="<%= assetPath %>/node_modules/jquery.fancytree/dist/jquery.fancytree-all-deps.min.js"></script>
<script src="<%= assetPath %>/node_modules/jquery-hotkeys/jquery-hotkeys.js"></script>
<script src="<%= assetPath %>/node_modules/autocomplete.js/dist/autocomplete.jquery.min.js"></script>
<link href="<%= appPath %>/bootstrap.css" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/ckeditor-theme.css" rel="stylesheet">
<link href="api/fonts" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet">
@ -64,14 +52,8 @@
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/print.css" rel="stylesheet" media="print">
<script>
$("body").show();
</script>
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
<script src="<%= appPath %>/desktop.js" crossorigin type="module"></script>
<link rel="stylesheet" type="text/css" href="<%= assetPath %>/node_modules/boxicons/css/boxicons.min.css">
</body>
</html>

View File

@ -13,7 +13,7 @@
}
</style>
<% // TriliumNextTODO: move the css file to ${assetPath}/stylesheets/ %>
<link rel="stylesheet" href="<%= appPath %>/login.css">
<link rel="stylesheet" href="<%= appPath %>/bootstrap.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-light.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-next.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/style.css">

View File

@ -10,9 +10,6 @@
<title>TriliumNext Notes</title>
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
<% // TriliumNextTODO: move the css file to ${assetPath}/stylesheets/ %>
<link rel="stylesheet" href="<%= appPath %>/mobile.css">
<style>
.lds-roller {
display: inline-block;
@ -111,17 +108,11 @@
<%- include("./partials/windowGlobal.ejs", locals) %>
<script src="<%= assetPath %>/node_modules/jquery/dist/jquery.min.js"></script>
<script src="<%= assetPath %>/node_modules/autocomplete.js/dist/autocomplete.jquery.min.js"></script>
<link href="<%= assetPath %>/stylesheets/tree.css" rel="stylesheet">
<script src="<%= assetPath %>/node_modules/jquery.fancytree/dist/jquery.fancytree-all-deps.min.js"></script>
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
<script src="<%= appPath %>/mobile.js" crossorigin type="module"></script>
<link href="api/fonts" rel="stylesheet">
<link rel="stylesheet" href="<%= appPath %>/bootstrap.css">
<link href="<%= assetPath %>/stylesheets/ckeditor-theme.css" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet">
<% if (themeCssUrl) { %>
@ -130,7 +121,5 @@
<link href="<%= assetPath %>/stylesheets/style.css" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/print.css" rel="stylesheet" media="print">
<link rel="stylesheet" type="text/css" href="<%= assetPath %>/node_modules/boxicons/css/boxicons.min.css">
</body>
</html>

View File

@ -7,7 +7,7 @@
<link rel="apple-touch-icon" sizes="180x180" href="<%= assetPath %>/images/app-icons/ios/apple-touch-icon.png">
<link rel="shortcut icon" href="favicon.ico">
<% // TriliumNextTODO: move the css file to ${assetPath}/stylesheets/ %>
<link rel="stylesheet" href="<%= appPath %>/set_password.css">
<link rel="stylesheet" href="<%= appPath %>/bootstrap.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-light.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/theme-next.css">
<link rel="stylesheet" href="<%= assetPath %>/stylesheets/style.css">

View File

@ -7,7 +7,7 @@
<title><%= t("setup.title") %></title>
<% // TriliumNextTODO: move the css file to ${assetPath}/stylesheets/ %>
<link rel="stylesheet" href="<%= appPath %>/setup.css">
<link rel="stylesheet" href="<%= appPath %>/bootstrap.css">
<style>
.lds-ring {
@ -168,9 +168,6 @@
<!-- Required for correct loading of scripts in Electron -->
<script>if (typeof module === 'object') {window.module = module; module = undefined;}</script>
<script src="<%= assetPath %>/node_modules/jquery/dist/jquery.min.js"></script>
<script src="<%= assetPath %>/node_modules/jquery-hotkeys/jquery-hotkeys.js"></script>
<script src="<%= appPath %>/runtime.js" crossorigin type="module"></script>
<script src="<%= appPath %>/setup.js" crossorigin type="module"></script>
<link href="<%= assetPath %>/stylesheets/theme-light.css" rel="stylesheet" />

View File

@ -12,13 +12,9 @@
<% } else { %>
<link rel="shortcut icon" href="../favicon.ico">
<% } %>
<script src="../<%= appPath %>/share.js"></script>
<script src="<%= appPath %>/share.js" type="module"></script>
<% if (!note.isLabelTruthy("shareOmitDefaultCss")) { %>
<link href="../<%= assetPath %>/node_modules/normalize.css/normalize.css" rel="stylesheet">
<link href="../<%= assetPath %>/stylesheets/share.css" rel="stylesheet">
<% } %>
<% if (note.type === 'text' || note.type === 'book') { %>
<link href="../<%= assetPath %>/libraries/ckeditor/ckeditor-content.css" rel="stylesheet">
<link href="<%= assetPath %>/stylesheets/share.css" rel="stylesheet">
<% } %>
<% for (const cssRelation of note.getRelations("shareCss")) { %>
<link href="api/notes/<%= cssRelation.value %>/download" rel="stylesheet">

View File

@ -101,7 +101,7 @@ async function importNotesToBranch(req: Request) {
return note.getPojo();
}
async function importAttachmentsToNote(req: Request) {
function importAttachmentsToNote(req: Request) {
const { parentNoteId } = req.params;
const { taskId, last } = req.body;
@ -121,7 +121,7 @@ async function importAttachmentsToNote(req: Request) {
// unlike in note import, we let the events run, because a huge number of attachments is not likely
try {
await singleImportService.importAttachment(taskContext, file, parentNote);
singleImportService.importAttachment(taskContext, file, parentNote);
} catch (e: unknown) {
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);

View File

@ -6,7 +6,6 @@ import searchService from "../../services/search/services/search.js";
import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express";
import { changeLanguage, getLocales } from "../../services/i18n.js";
import { listSyntaxHighlightingThemes } from "../../services/code_block_theme.js";
import type { OptionNames } from "@triliumnext/commons";
// options allowed to be updated directly in the Options dialog
@ -190,10 +189,6 @@ function getUserThemes() {
return ret;
}
function getSyntaxHighlightingThemes() {
return listSyntaxHighlightingThemes();
}
function getSupportedLocales() {
return getLocales();
}
@ -210,6 +205,5 @@ export default {
updateOption,
updateOptions,
getUserThemes,
getSyntaxHighlightingThemes,
getSupportedLocales
};

View File

@ -1,11 +1,10 @@
import assetPath from "../services/asset_path.js";
import { assetUrlFragment } from "../services/asset_path.js";
import path from "path";
import { fileURLToPath } from "url";
import express from "express";
import { getResourceDir, isDev } from "../services/utils.js";
import type serveStatic from "serve-static";
import proxy from "express-http-proxy";
import contentCss from "@triliumnext/ckeditor5/content.css";
const persistentCacheStatic = (root: string, options?: serveStatic.ServeStaticOptions<express.Response<unknown, Record<string, unknown>>>) => {
if (!isDev) {
@ -21,77 +20,29 @@ async function register(app: express.Application) {
const srcRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..");
const resourceDir = getResourceDir();
app.use(`/${assetPath}/libraries/ckeditor/ckeditor-content.css`, (req, res) => res.contentType("text/css").send(contentCss));
if (isDev) {
const publicUrl = process.env.TRILIUM_PUBLIC_SERVER;
if (!publicUrl) {
throw new Error("Missing TRILIUM_PUBLIC_SERVER");
}
const clientProxy = proxy(publicUrl);
app.use(`/${assetPath}/app/doc_notes`, persistentCacheStatic(path.join(srcRoot, "assets", "doc_notes")));
app.use(`/${assetPath}/app`, clientProxy);
app.use(`/${assetPath}/app-dist`, clientProxy);
app.use(`/${assetPath}/stylesheets`, proxy(publicUrl, {
proxyReqPathResolver: (req) => "/stylesheets" + req.url
app.use("/" + assetUrlFragment + `/@fs`, proxy(publicUrl, {
proxyReqPathResolver: (req) => "/" + assetUrlFragment + `/@fs` + req.url
}));
app.use(`/${assetPath}/libraries`, proxy(publicUrl, {
proxyReqPathResolver: (req) => "/libraries" + req.url
}));
app.use(`/${assetPath}/fonts`, proxy(publicUrl, {
proxyReqPathResolver: (req) => "/fonts" + req.url
}));
app.use(`/${assetPath}/translations`, proxy(publicUrl, {
proxyReqPathResolver: (req) => "/translations" + req.url
}));
app.use(`/${assetPath}/images`, persistentCacheStatic(path.join(srcRoot, "assets", "images")));
} else {
const clientStaticCache = persistentCacheStatic(path.join(resourceDir, "public"));
app.use(`/${assetPath}/app`, clientStaticCache);
app.use(`/${assetPath}/app-dist`, clientStaticCache);
app.use(`/${assetPath}/stylesheets`, persistentCacheStatic(path.join(resourceDir, "public", "stylesheets")));
app.use(`/${assetPath}/libraries`, persistentCacheStatic(path.join(resourceDir, "public", "libraries")));
app.use(`/${assetPath}/fonts`, persistentCacheStatic(path.join(resourceDir, "public", "fonts")));
app.use(`/${assetPath}/translations/`, persistentCacheStatic(path.join(resourceDir, "public", "translations")));
app.use(`/${assetPath}/images`, persistentCacheStatic(path.join(resourceDir, "assets", "images")));
app.use(`/${assetPath}/app-dist/doc_notes`, persistentCacheStatic(path.join(resourceDir, "assets", "doc_notes")));
app.use(`/${assetPath}/app-dist/excalidraw/fonts`, persistentCacheStatic(path.join(resourceDir, "node_modules/@excalidraw/excalidraw/dist/prod/fonts")));
app.use(`/${assetUrlFragment}/src`, persistentCacheStatic(path.join(resourceDir, "public", "src")));
app.use(`/${assetUrlFragment}/stylesheets`, persistentCacheStatic(path.join(resourceDir, "public", "stylesheets")));
app.use(`/${assetUrlFragment}/libraries`, persistentCacheStatic(path.join(resourceDir, "public", "libraries")));
app.use(`/${assetUrlFragment}/fonts`, persistentCacheStatic(path.join(resourceDir, "public", "fonts")));
app.use(`/${assetUrlFragment}/translations/`, persistentCacheStatic(path.join(resourceDir, "public", "translations")));
app.use(`/${assetUrlFragment}/images`, persistentCacheStatic(path.join(resourceDir, "assets", "images")));
app.use(`/node_modules/`, persistentCacheStatic(path.join(resourceDir, "public/node_modules")));
}
app.use(`/${assetUrlFragment}/doc_notes`, persistentCacheStatic(path.join(resourceDir, "assets", "doc_notes")));
app.use(`/assets/vX/fonts`, express.static(path.join(srcRoot, "public/fonts")));
app.use(`/assets/vX/images`, express.static(path.join(srcRoot, "..", "images")));
app.use(`/assets/vX/stylesheets`, express.static(path.join(srcRoot, "public/stylesheets")));
app.use(`/${assetPath}/libraries`, persistentCacheStatic(path.join(srcRoot, "public/libraries")));
app.use(`/${assetUrlFragment}/libraries`, persistentCacheStatic(path.join(srcRoot, "public/libraries")));
app.use(`/assets/vX/libraries`, express.static(path.join(srcRoot, "..", "libraries")));
const nodeModulesDir = isDev ? path.join(srcRoot, "..", "node_modules") : path.join(resourceDir, "node_modules");
app.use(`/node_modules/@excalidraw/excalidraw/dist/fonts/`, express.static(path.join(nodeModulesDir, "@excalidraw/excalidraw/dist/prod/fonts/")));
app.use(`/${assetPath}/node_modules/@excalidraw/excalidraw/dist/fonts/`, persistentCacheStatic(path.join(nodeModulesDir, "@excalidraw/excalidraw/dist/prod/fonts/")));
// KaTeX
app.use(`/${assetPath}/node_modules/katex/dist/katex.min.js`, persistentCacheStatic(path.join(nodeModulesDir, "katex/dist/katex.min.js")));
app.use(`/${assetPath}/node_modules/katex/dist/contrib/mhchem.min.js`, persistentCacheStatic(path.join(nodeModulesDir, "katex/dist/contrib/mhchem.min.js")));
app.use(`/${assetPath}/node_modules/katex/dist/contrib/auto-render.min.js`, persistentCacheStatic(path.join(nodeModulesDir, "katex/dist/contrib/auto-render.min.js")));
// expose the whole dist folder
app.use(`/node_modules/katex/dist/`, express.static(path.join(nodeModulesDir, "katex/dist/")));
app.use(`/${assetPath}/node_modules/katex/dist/`, persistentCacheStatic(path.join(nodeModulesDir, "katex/dist/")));
app.use(`/${assetPath}/node_modules/boxicons/css/`, persistentCacheStatic(path.join(nodeModulesDir, "boxicons/css/")));
app.use(`/${assetPath}/node_modules/boxicons/fonts/`, persistentCacheStatic(path.join(nodeModulesDir, "boxicons/fonts/")));
app.use(`/${assetPath}/node_modules/jquery/dist/`, persistentCacheStatic(path.join(nodeModulesDir, "jquery/dist/")));
app.use(`/${assetPath}/node_modules/jquery-hotkeys/`, persistentCacheStatic(path.join(nodeModulesDir, "jquery-hotkeys/")));
// Deprecated, https://www.npmjs.com/package/autocomplete.js?activeTab=readme
app.use(`/${assetPath}/node_modules/autocomplete.js/dist/`, persistentCacheStatic(path.join(nodeModulesDir, "autocomplete.js/dist/")));
app.use(`/${assetPath}/node_modules/normalize.css/`, persistentCacheStatic(path.join(nodeModulesDir, "normalize.css/")));
app.use(`/${assetPath}/node_modules/jquery.fancytree/dist/`, persistentCacheStatic(path.join(nodeModulesDir, "jquery.fancytree/dist/")));
app.use(`/${assetPath}/node_modules/@highlightjs/cdn-assets/`, persistentCacheStatic(path.join(nodeModulesDir, "@highlightjs/cdn-assets/")));
}
export default {

View File

@ -14,15 +14,12 @@ describe("Login Route test", () => {
it("should return the login page, when using a GET request", async () => {
// RegExp for login page specific string in HTML: e.g. "assets/v0.92.7/app/login.css"
const loginCssRegexp = /assets\/v[0-9.a-z]+\/app\/login\.css/;
// RegExp for login page specific string in HTML
const res = await supertest(app)
.get("/login")
.expect(200)
expect(loginCssRegexp.test(res.text)).toBe(true);
expect(res.text).toMatch(/assets\/v[0-9.a-z]+\/src\/login\.js/);
});

View File

@ -0,0 +1,198 @@
import express from "express";
import multer from "multer";
import log from "../services/log.js";
import cls from "../services/cls.js";
import sql from "../services/sql.js";
import entityChangesService from "../services/entity_changes.js";
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
import NotFoundError from "../errors/not_found_error.js";
import ValidationError from "../errors/validation_error.js";
import auth from "../services/auth.js";
import { doubleCsrfProtection as csrfMiddleware } from "./csrf_protection.js";
import { safeExtractMessageAndStackFromError } from "../services/utils.js";
const MAX_ALLOWED_FILE_SIZE_MB = 250;
export const router = express.Router();
// TODO: Deduplicate with etapi_utils.ts afterwards.
type HttpMethod = "all" | "get" | "post" | "put" | "delete" | "patch" | "options" | "head";
export type ApiResultHandler = (req: express.Request, res: express.Response, result: unknown) => number;
type NotAPromise<T> = T & { then?: void };
export type ApiRequestHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => unknown;
export type SyncRouteRequestHandler = (req: express.Request, res: express.Response, next: express.NextFunction) => NotAPromise<object> | number | string | void | null;
/** Handling common patterns. If entity is not caught, serialization to JSON will fail */
function convertEntitiesToPojo(result: unknown) {
if (result instanceof AbstractBeccaEntity) {
result = result.getPojo();
} else if (Array.isArray(result)) {
for (const idx in result) {
if (result[idx] instanceof AbstractBeccaEntity) {
result[idx] = result[idx].getPojo();
}
}
} else if (result && typeof result === "object") {
if ("note" in result && result.note instanceof AbstractBeccaEntity) {
result.note = result.note.getPojo();
}
if ("branch" in result && result.branch instanceof AbstractBeccaEntity) {
result.branch = result.branch.getPojo();
}
}
if (result && typeof result === "object" && "executionResult" in result) {
// from runOnBackend()
result.executionResult = convertEntitiesToPojo(result.executionResult);
}
return result;
}
export function apiResultHandler(req: express.Request, res: express.Response, result: unknown) {
res.setHeader("trilium-max-entity-change-id", entityChangesService.getMaxEntityChangeId());
result = convertEntitiesToPojo(result);
// if it's an array and the first element is integer, then we consider this to be [statusCode, response] format
if (Array.isArray(result) && result.length > 0 && Number.isInteger(result[0])) {
const [statusCode, response] = result;
if (statusCode !== 200 && statusCode !== 201 && statusCode !== 204) {
log.info(`${req.method} ${req.originalUrl} returned ${statusCode} with response ${JSON.stringify(response)}`);
}
return send(res, statusCode, response);
} else if (result === undefined) {
return send(res, 204, "");
} else {
return send(res, 200, result);
}
}
function send(res: express.Response, statusCode: number, response: unknown) {
if (typeof response === "string") {
if (statusCode >= 400) {
res.setHeader("Content-Type", "text/plain");
}
res.status(statusCode).send(response);
return response.length;
} else {
const json = JSON.stringify(response);
res.setHeader("Content-Type", "application/json");
res.status(statusCode).send(json);
return json.length;
}
}
export function apiRoute(method: HttpMethod, path: string, routeHandler: SyncRouteRequestHandler) {
route(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler);
}
export function asyncApiRoute(method: HttpMethod, path: string, routeHandler: ApiRequestHandler) {
asyncRoute(method, path, [auth.checkApiAuth, csrfMiddleware], routeHandler, apiResultHandler);
}
export function route(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: SyncRouteRequestHandler, resultHandler: ApiResultHandler | null = null) {
internalRoute(method, path, middleware, routeHandler, resultHandler, true);
}
export function asyncRoute(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: ApiRequestHandler, resultHandler: ApiResultHandler | null = null) {
internalRoute(method, path, middleware, routeHandler, resultHandler, false);
}
function internalRoute(method: HttpMethod, path: string, middleware: express.Handler[], routeHandler: ApiRequestHandler, resultHandler: ApiResultHandler | null = null, transactional: boolean) {
router[method](path, ...(middleware as express.Handler[]), (req: express.Request, res: express.Response, next: express.NextFunction) => {
const start = Date.now();
try {
cls.namespace.bindEmitter(req);
cls.namespace.bindEmitter(res);
const result = cls.init(() => {
cls.set("componentId", req.headers["trilium-component-id"]);
cls.set("localNowDateTime", req.headers["trilium-local-now-datetime"]);
cls.set("hoistedNoteId", req.headers["trilium-hoisted-note-id"] || "root");
const cb = () => routeHandler(req, res, next);
return transactional ? sql.transactional(cb) : cb();
});
if (!resultHandler) {
return;
}
if (result?.then) {
// promise
result.then((promiseResult: unknown) => handleResponse(resultHandler, req, res, promiseResult, start)).catch((e: unknown) => handleException(e, method, path, res));
} else {
handleResponse(resultHandler, req, res, result, start);
}
} catch (e) {
handleException(e, method, path, res);
}
});
}
function handleResponse(resultHandler: ApiResultHandler, req: express.Request, res: express.Response, result: unknown, start: number) {
// Skip result handling if the response has already been handled
if ((res as any).triliumResponseHandled) {
// Just log the request without additional processing
log.request(req, res, Date.now() - start, 0);
return;
}
const responseLength = resultHandler(req, res, result);
log.request(req, res, Date.now() - start, responseLength);
}
function handleException(e: unknown | Error, method: HttpMethod, path: string, res: express.Response) {
const [errMessage, errStack] = safeExtractMessageAndStackFromError(e);
log.error(`${method} ${path} threw exception: '${errMessage}', stack: ${errStack}`);
const resStatusCode = (e instanceof ValidationError || e instanceof NotFoundError) ? e.statusCode : 500;
res.status(resStatusCode).json({
message: errMessage
});
}
export function createUploadMiddleware() {
const multerOptions: multer.Options = {
fileFilter: (req: express.Request, file, cb) => {
// UTF-8 file names are not well decoded by multer/busboy, so we handle the conversion on our side.
// See https://github.com/expressjs/multer/pull/1102.
file.originalname = Buffer.from(file.originalname, "latin1").toString("utf-8");
cb(null, true);
}
};
if (!process.env.TRILIUM_NO_UPLOAD_LIMIT) {
multerOptions.limits = {
fileSize: MAX_ALLOWED_FILE_SIZE_MB * 1024 * 1024
};
}
return multer(multerOptions).single("upload");
}
const uploadMiddleware = createUploadMiddleware();
export const uploadMiddlewareWithErrorHandling = function (req: express.Request, res: express.Response, next: express.NextFunction) {
uploadMiddleware(req, res, function (err) {
if (err?.code === "LIMIT_FILE_SIZE") {
res.setHeader("Content-Type", "text/plain").status(400).send(`Cannot upload file because it excceeded max allowed file size of ${MAX_ALLOWED_FILE_SIZE_MB} MiB`);
} else {
next();
}
});
};

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