mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 10:02:59 +08:00
Merge branch 'develop' into feat/add-rootless-dockerfiles
This commit is contained in:
commit
cbbbae727f
14
.github/workflows/main-docker.yml
vendored
14
.github/workflows/main-docker.yml
vendored
@ -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
|
||||
|
4
.github/workflows/playwright.yml
vendored
4
.github/workflows/playwright.yml
vendored
@ -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
104
README.md
@ -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.
|
||||
|
||||
[](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.
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -283,6 +283,9 @@ export type CommandMappings = {
|
||||
type EventMappings = {
|
||||
initialRenderComplete: {};
|
||||
frocaReloaded: {};
|
||||
setLeftPaneVisibility: {
|
||||
leftPaneVisible: boolean | null;
|
||||
}
|
||||
protectedSessionStarted: {};
|
||||
notesReloaded: {
|
||||
noteIds: string[];
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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);
|
@ -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();
|
||||
|
||||
|
5
apps/client/src/runtime.ts
Normal file
5
apps/client/src/runtime.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import $ from "jquery";
|
||||
(window as any).$ = $;
|
||||
(window as any).jQuery = $;
|
||||
|
||||
$("body").show();
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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`;
|
||||
}
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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
|
||||
};
|
@ -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 {
|
||||
|
5
apps/client/src/services/math.ts
Normal file
5
apps/client/src/services/math.ts
Normal 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;
|
@ -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];
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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]));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -1,3 +1,5 @@
|
||||
import "jquery";
|
||||
import "jquery-hotkeys";
|
||||
import utils from "./services/utils.js";
|
||||
import ko from "knockout";
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
|
@ -1,4 +1,5 @@
|
||||
import "./stylesheets/bootstrap.scss";
|
||||
import "normalize.css";
|
||||
import "@triliumnext/ckeditor5/content.css";
|
||||
|
||||
/**
|
||||
* Fetch note with given ID from backend
|
||||
|
@ -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"; }
|
||||
|
@ -195,6 +195,8 @@
|
||||
--scrollbar-background-color: transparent;
|
||||
--scrollbar-border-color: unset; /* Deprecated */
|
||||
|
||||
--selection-background-color: #3399FF70;
|
||||
|
||||
--link-color: lightskyblue;
|
||||
|
||||
--mermaid-theme: dark;
|
||||
|
@ -194,6 +194,8 @@
|
||||
--scrollbar-background-color: transparent;
|
||||
--scrollbar-border-color: unset; /* Deprecated */
|
||||
|
||||
--selection-background-color: #3399FF70;
|
||||
|
||||
--link-color: blue;
|
||||
|
||||
--mermaid-theme: default;
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
@ -1621,7 +1621,10 @@
|
||||
"color-scheme": "颜色方案"
|
||||
},
|
||||
"code_block": {
|
||||
"word_wrapping": "自动换行"
|
||||
"word_wrapping": "自动换行",
|
||||
"theme_none": "无语法高亮",
|
||||
"theme_group_light": "浅色主题",
|
||||
"theme_group_dark": "深色主题"
|
||||
},
|
||||
"classic_editor_toolbar": {
|
||||
"title": "格式化"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -1519,7 +1519,10 @@
|
||||
"color-scheme": "顏色方案"
|
||||
},
|
||||
"code_block": {
|
||||
"word_wrapping": "自動換行"
|
||||
"word_wrapping": "自動換行",
|
||||
"theme_none": "無格式高亮",
|
||||
"theme_group_light": "淺色主題",
|
||||
"theme_group_dark": "深色主題"
|
||||
},
|
||||
"classic_editor_toolbar": {
|
||||
"title": "格式化"
|
||||
|
4
apps/client/src/types-assets.d.ts
vendored
4
apps/client/src/types-assets.d.ts
vendored
@ -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";
|
||||
|
7
apps/client/src/types-lib.d.ts
vendored
7
apps/client/src/types-lib.d.ts
vendored
@ -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;
|
||||
}
|
||||
|
19
apps/client/src/types.d.ts
vendored
19
apps/client/src/types.d.ts
vendored
@ -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
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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") {
|
||||
|
@ -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({
|
||||
|
@ -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
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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 = "";
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
},
|
||||
|
@ -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">
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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("|");
|
||||
|
||||
|
@ -34,6 +34,9 @@
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/highlightjs/tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/codemirror/tsconfig.lib.json"
|
||||
},
|
||||
|
@ -3,6 +3,9 @@
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../packages/highlightjs"
|
||||
},
|
||||
{
|
||||
"path": "../../packages/codemirror"
|
||||
},
|
||||
|
@ -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,
|
||||
}
|
||||
}));
|
||||
|
@ -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',
|
||||
});
|
||||
}
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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": {
|
||||
|
@ -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",
|
||||
|
@ -1,2 +1,3 @@
|
||||
TRILIUM_ENV=production
|
||||
TRILIUM_DATA_DIR=./apps/server/data
|
||||
TRILIUM_DATA_DIR=./apps/server/data
|
||||
TRILIUM_PORT=8082
|
@ -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 \
|
||||
|
@ -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
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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");
|
||||
});
|
||||
|
@ -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")));
|
||||
|
||||
|
@ -0,0 +1,5 @@
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
data TEXT,
|
||||
expires INTEGER
|
||||
);
|
@ -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
|
||||
);
|
||||
|
@ -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. It’s
|
||||
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" -> "Options" -> "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" -> "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" -> "Options" -> "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 <a class="reference-link" href="#root/_help_Gzjqa934BdH4">Configuration (config.ini or environment variables)</a> 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" -> "Options" -> "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>
|
@ -40,7 +40,7 @@
|
||||
<h2>Color schemes</h2>
|
||||
<p>Since Trilium 0.94.0 the colors of code notes can be customized by going
|
||||
<a
|
||||
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_4TIF1oA4VQRO">Options</a> → Code Notes and looking for the <em>Appearance</em> section.</p>
|
||||
class="reference-link" href="#root/_help_4TIF1oA4VQRO">Options</a> → 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>
|
||||
|
@ -193,11 +193,6 @@
|
||||
"special_notes": {
|
||||
"search_prefix": "搜索:"
|
||||
},
|
||||
"code_block": {
|
||||
"theme_none": "无语法高亮",
|
||||
"theme_group_light": "浅色主题",
|
||||
"theme_group_dark": "深色主题"
|
||||
},
|
||||
"test_sync": {
|
||||
"not-configured": "同步服务器主机未配置。请先配置同步。",
|
||||
"successful": "同步服务器握手成功,同步已开始。"
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -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."
|
||||
|
@ -185,11 +185,6 @@
|
||||
"special_notes": {
|
||||
"search_prefix": "搜尋:"
|
||||
},
|
||||
"code_block": {
|
||||
"theme_none": "無格式高亮",
|
||||
"theme_group_light": "淺色主題",
|
||||
"theme_group_dark": "深色主題"
|
||||
},
|
||||
"test_sync": {
|
||||
"not-configured": "並未設定同步伺服器主機,請先設定同步",
|
||||
"successful": "成功與同步伺服器握手,現在開始同步"
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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" />
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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/);
|
||||
|
||||
});
|
||||
|
||||
|
198
apps/server/src/routes/route_api.ts
Normal file
198
apps/server/src/routes/route_api.ts
Normal 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
Loading…
x
Reference in New Issue
Block a user