Merge remote-tracking branch 'origin/develop' into find_replace

This commit is contained in:
Elian Doran
2025-05-10 15:26:52 +03:00
311 changed files with 25334 additions and 2475 deletions

View File

@@ -10,7 +10,7 @@
"url": "https://github.com/TriliumNext/Notes"
},
"dependencies": {
"@eslint/js": "9.25.0",
"@eslint/js": "9.26.0",
"@excalidraw/excalidraw": "0.18.0",
"@fullcalendar/core": "6.1.17",
"@fullcalendar/daygrid": "6.1.17",
@@ -21,16 +21,17 @@
"@mermaid-js/layout-elk": "0.1.7",
"@mind-elixir/node-menu": "1.0.5",
"@popperjs/core": "2.11.8",
"@triliumnext/ckeditor5": "workspace:*",
"@triliumnext/commons": "workspace:*",
"bootstrap": "5.3.5",
"bootstrap": "5.3.6",
"dayjs": "1.11.13",
"dayjs-plugin-utc": "0.1.2",
"debounce": "2.2.0",
"draggabilly": "3.0.0",
"eslint-linter-browserify": "9.26.0",
"force-graph": "1.49.5",
"globals": "16.0.0",
"i18next": "25.0.2",
"globals": "16.1.0",
"i18next": "25.1.2",
"i18next-http-backend": "3.0.2",
"jquery": "3.7.1",
"jquery-hotkeys": "0.2.2",
@@ -44,19 +45,21 @@
"mermaid": "11.6.0",
"mind-elixir": "4.5.2",
"panzoom": "9.4.3",
"react": "18.3.1",
"react-dom": "18.3.1",
"react": "19.1.0",
"react-dom": "19.1.0",
"split.js": "1.6.5",
"svg-pan-zoom": "3.6.2",
"vanilla-js-wheel-zoom": "9.0.4"
},
"devDependencies": {
"@ckeditor/ckeditor5-inspector": "4.1.0",
"@types/bootstrap": "5.2.10",
"@types/jquery": "3.5.32",
"@types/leaflet": "1.9.17",
"@types/leaflet-gpx": "1.3.7",
"@types/react": "18.3.20",
"@types/react-dom": "18.3.6",
"@types/react": "19.1.3",
"@types/react-dom": "19.1.3",
"copy-webpack-plugin": "13.0.0",
"happy-dom": "17.4.6",
"script-loader": "0.7.2"
},

View File

@@ -26,6 +26,7 @@ import type TypeWidget from "../widgets/type_widgets/type_widget.js";
import type EditableTextTypeWidget from "../widgets/type_widgets/editable_text.js";
import type { NativeImage, TouchBar } from "electron";
import TouchBarComponent from "./touch_bar.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5";
interface Layout {
getRootWidget: (appContext: AppContext) => RootWidget;
@@ -187,7 +188,7 @@ export type CommandMappings = {
callback: (value: NoteDetailWidget | PromiseLike<NoteDetailWidget>) => void;
};
executeWithTextEditor: CommandData &
ExecuteCommandData<TextEditor> & {
ExecuteCommandData<CKTextEditor> & {
callback?: GetTextEditorCallback;
};
executeWithCodeEditor: CommandData & ExecuteCommandData<CodeMirrorInstance>;

View File

@@ -10,13 +10,14 @@ import options from "../services/options.js";
import type { ViewScope } from "../services/link.js";
import type FNote from "../entities/fnote.js";
import type TypeWidget from "../widgets/type_widgets/type_widget.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5";
export interface SetNoteOpts {
triggerSwitchEvent?: unknown;
viewScope?: ViewScope;
}
export type GetTextEditorCallback = (editor: TextEditor) => void;
export type GetTextEditorCallback = (editor: CKTextEditor) => void;
class NoteContext extends Component implements EventListener<"entitiesReloaded"> {
ntxId: string | null;
@@ -298,7 +299,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
}
async getTextEditor(callback?: GetTextEditorCallback) {
return this.timeout<TextEditor>(
return this.timeout<CKTextEditor>(
new Promise((resolve) =>
appContext.triggerCommand("executeWithTextEditor", {
callback,

View File

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

View File

@@ -1,49 +0,0 @@
/**
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
*/
import { DecoupledEditor as DecoupledEditorBase } from '@ckeditor/ckeditor5-editor-decoupled';
import { Essentials } from '@ckeditor/ckeditor5-essentials';
import { Alignment } from '@ckeditor/ckeditor5-alignment';
import { FontSize, FontFamily, FontColor, FontBackgroundColor } from '@ckeditor/ckeditor5-font';
import { CKFinderUploadAdapter } from '@ckeditor/ckeditor5-adapter-ckfinder';
import { Autoformat } from '@ckeditor/ckeditor5-autoformat';
import { Bold, Italic, Strikethrough, Underline } from '@ckeditor/ckeditor5-basic-styles';
import { BlockQuote } from '@ckeditor/ckeditor5-block-quote';
import { CKBox } from '@ckeditor/ckeditor5-ckbox';
import { CKFinder } from '@ckeditor/ckeditor5-ckfinder';
import { EasyImage } from '@ckeditor/ckeditor5-easy-image';
import { Heading } from '@ckeditor/ckeditor5-heading';
import { Image, ImageCaption, ImageResize, ImageStyle, ImageToolbar, ImageUpload, PictureEditing } from '@ckeditor/ckeditor5-image';
import { Indent, IndentBlock } from '@ckeditor/ckeditor5-indent';
import { Link } from '@ckeditor/ckeditor5-link';
import { List, ListProperties } from '@ckeditor/ckeditor5-list';
import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed';
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office';
import { Table, TableToolbar } from '@ckeditor/ckeditor5-table';
import { TextTransformation } from '@ckeditor/ckeditor5-typing';
import { CloudServices } from '@ckeditor/ckeditor5-cloud-services';
export default class DecoupledEditor extends DecoupledEditorBase {
static builtinPlugins: (typeof TextTransformation | typeof Essentials | typeof Alignment | typeof FontBackgroundColor | typeof FontColor | typeof FontFamily | typeof FontSize | typeof CKFinderUploadAdapter | typeof Paragraph | typeof Heading | typeof Autoformat | typeof Bold | typeof Italic | typeof Strikethrough | typeof Underline | typeof BlockQuote | typeof Image | typeof ImageCaption | typeof ImageResize | typeof ImageStyle | typeof ImageToolbar | typeof ImageUpload | typeof CloudServices | typeof CKBox | typeof CKFinder | typeof EasyImage | typeof List | typeof ListProperties | typeof Indent | typeof IndentBlock | typeof Link | typeof MediaEmbed | typeof PasteFromOffice | typeof Table | typeof TableToolbar | typeof PictureEditing)[];
static defaultConfig: {
toolbar: {
items: string[];
};
image: {
resizeUnit: "px";
toolbar: string[];
};
table: {
contentToolbar: string[];
};
list: {
properties: {
styles: boolean;
startIndex: boolean;
reversed: boolean;
};
};
language: string;
};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,7 @@ export interface Froca {
branches: Record<string, FBranch>;
attributes: Record<string, FAttribute>;
attachments: Record<string, FAttachment>;
blobPromises: Record<string, Promise<void | FBlob> | null>;
blobPromises: Record<string, Promise<void | FBlob | null> | null>;
getBlob(entityType: string, entityId: string): Promise<FBlob | null>;
getNote(noteId: string, silentNotFoundError?: boolean): Promise<FNote | null>;

View File

@@ -36,7 +36,7 @@ class FrocaImpl implements Froca {
branches!: Record<string, FBranch>;
attributes!: Record<string, FAttribute>;
attachments!: Record<string, FAttachment>;
blobPromises!: Record<string, Promise<FBlob> | null>;
blobPromises!: Record<string, Promise<FBlob | null> | null>;
constructor() {
this.initializedPromise = this.loadInitialTree();

View File

@@ -7,10 +7,6 @@ export interface Library {
css?: string[];
}
const CKEDITOR: Library = {
js: ["libraries/ckeditor/ckeditor.js"]
};
const CODE_MIRROR: Library = {
js: () => {
const scriptsToLoad = [
@@ -156,7 +152,6 @@ export default {
requireCss,
requireLibrary,
loadHighlightingTheme,
CKEDITOR,
CODE_MIRROR,
KATEX,
HIGHLIGHT_JS

View File

@@ -38,7 +38,7 @@ let mimeToHighlightJsMapping: Record<string, string> | null = null;
* @param mimeType The MIME type of the code block, in the CKEditor-normalized format (e.g. `text-c-src` instead of `text/c-src`).
* @returns the corresponding highlight.js tag, for example `c` for `text-c-src`.
*/
function getHighlightJsNameForMime(mimeType: string) {
export function getHighlightJsNameForMime(mimeType: string) {
if (!mimeToHighlightJsMapping) {
const mimeTypes = getMimeTypes();
mimeToHighlightJsMapping = {};

View File

@@ -3,6 +3,7 @@ import appContext from "../components/app_context.js";
import noteCreateService from "./note_create.js";
import froca from "./froca.js";
import { t } from "./i18n.js";
import type { MentionFeedObjectItem } from "@triliumnext/ckeditor5";
// this key needs to have this value, so it's hit by the tooltip
const SELECTED_NOTE_PATH_KEY = "data-note-path";
@@ -43,7 +44,7 @@ interface Options {
}
async function autocompleteSourceForCKEditor(queryText: string) {
return await new Promise<MentionItem[]>((res, rej) => {
return await new Promise<MentionFeedObjectItem[]>((res, rej) => {
autocompleteSource(
queryText,
(rows) => {

View File

@@ -9,6 +9,7 @@ import { t } from "./i18n.js";
import type FNote from "../entities/fnote.js";
import type FBranch from "../entities/fbranch.js";
import type { ChooseNoteTypeResponse } from "../widgets/dialogs/note_type_chooser.js";
import type { CKTextEditor } from "@triliumnext/ckeditor5";
interface CreateNoteOpts {
isProtected?: boolean;
@@ -22,7 +23,7 @@ interface CreateNoteOpts {
focus?: "title" | "content";
target?: string;
targetBranchId?: string;
textEditor?: TextEditor;
textEditor?: CKTextEditor;
}
interface Response {

View File

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

View File

@@ -21,7 +21,7 @@ interface CustomGlobals {
getHeaders: typeof server.getHeaders;
getReferenceLinkTitle: (href: string) => Promise<string>;
getReferenceLinkTitleSync: (href: string) => string;
getActiveContextNote: () => FNote;
getActiveContextNote: () => FNote | null;
requireLibrary: typeof library_loader.requireLibrary;
ESLINT: Library;
appContext: AppContext;
@@ -74,6 +74,9 @@ declare global {
type AutoCompleteCallback = (values: AutoCompleteArg[]) => void;
interface AutoCompleteArg {
name?: string;
value?: string;
notePathTitle?: string;
displayKey?: "name" | "value" | "notePathTitle";
cache?: boolean;
source?: (term: string, cb: AutoCompleteCallback) => void,
@@ -83,7 +86,7 @@ declare global {
}
interface JQuery {
autocomplete: (action?: "close" | "open" | "destroy" | "val" | AutoCompleteConfig, args?: object[] | string) => JQuery<HTMLElement>;
autocomplete: (action?: "close" | "open" | "destroy" | "val" | AutoCompleteConfig, args?: AutoCompleteArg[] | string) => JQuery<HTMLElement>;
getSelectedNotePath(): string | undefined;
getSelectedNoteId(): string | null;
@@ -131,56 +134,6 @@ declare global {
var renderMathInElement: (element: HTMLElement, options: {
trust: boolean;
}) => void;
interface CKCodeBlockLanguage {
language: string;
label: string;
}
interface CKEditorInstance {
create(elementOrData: any, finalConfig: any): TextEditor;
}
class CKWatchdog {
constructor(editorClass: CKEditorInstance, opts: {
minimumNonErrorTimePeriod: number;
crashNumberLimit: number,
saveInterval: number
});
on(event: string, callback: () => void);
state: string;
crashes: unknown[];
editor: TextEditor;
setCreator(callback: (elementOrData, editorConfig) => void);
create(el: HTMLElement, opts: {
placeholder: string,
mention: MentionConfig,
codeBlock: {
languages: CKCodeBlockLanguage[]
},
math: {
engine: string,
outputType: string,
lazyLoad: () => Promise<void>,
forceOutputType: boolean,
enablePreview: boolean
},
mermaid: {
lazyLoad: () => Promise<Mermaid>,
config: MermaidConfig
}
});
destroy();
}
var CKEditor: {
BalloonEditor: CKEditorInstance;
DecoupledEditor: CKEditorInstance;
EditorWatchdog: typeof CKWatchdog;
};
var CKEditorInspector: {
attach(editor: TextEditor);
};
interface CodeMirrorOpts {
value: string;
@@ -256,222 +209,6 @@ declare global {
});
}
interface Range {
toJSON(): object;
getItems(): TextNode[];
}
interface Writer {
setAttribute(name: string, value: string, el: CKNode);
createPositionAt(el: CKNode, opt?: "end" | number);
setSelection(pos: number, pos2?: number);
insertText(text: string, opts: Record<string, unknown> | undefined | TextPosition, position?: TextPosition);
addMarker(name: string, opts: {
range: Range;
usingOperation: boolean;
});
removeMarker(name: string);
createRange(start: number, end: number): Range;
createElement(type: string, opts: Record<string, string | null | undefined>);
}
interface TextNode {
previousSibling?: TextNode;
name: string;
data: string;
startOffset: number;
_attrs: {
get(key: string): {
length: number
}
}
}
interface TextPosition {
textNode: TextNode;
offset: number;
compareWith(pos: TextPosition): string;
}
interface TextRange {
}
interface Marker {
name: string;
}
interface CKNode {
_children: CKNode[];
name: string;
childCount: number;
isEmpty: boolean;
toJSON(): object;
is(type: string, name?: string);
getAttribute(name: string): string;
getChild(index: number): CKNode;
data: string;
startOffset: number;
root: {
document: {
model: {
createRangeIn(el: CKNode): TextRange;
markers: {
getMarkersIntersectingRange(range: TextRange): Marker[];
}
}
}
};
}
interface CKEvent {
stop(): void;
}
interface PluginEventData {
title: string;
message: {
message: string;
};
}
interface TextEditor {
create(el: HTMLElement, config: {
removePlugins?: string[];
toolbar: {
items: any[];
},
placeholder: string;
mention: MentionConfig
});
enableReadOnlyMode(reason: string);
commands: {
get(name: string): {
value: unknown;
on(event: string, callback: () => void): void;
};
}
model: {
document: {
on(event: string, cb: () => void);
getRoot(): CKNode;
registerPostFixer(callback: (writer: Writer) => boolean);
selection: {
getFirstPosition(): undefined | TextPosition;
getLastPosition(): undefined | TextPosition;
getSelectedElement(): CKNode;
hasAttribute(attribute: string): boolean;
getAttribute(attribute: string): string;
getFirstRange(): Range;
isCollapsed: boolean;
};
differ: {
getChanges(): {
type: string;
name: string;
position?: {
nodeAfter?: CKNode;
parent: CKNode;
toJSON(): Object;
}
}[];
}
},
insertContent(modelFragment: any, selection?: any);
change(cb: (writer: Writer) => void)
},
editing: {
view: {
document: {
on(event: string, cb: (event: CKEvent, data: {
preventDefault();
}) => void, opts?: {
priority: "high"
});
getRoot(): CKNode
},
domRoots: {
values: () => {
next: () => {
value: string;
}
};
}
change(cb: (writer: Writer) => void);
scrollToTheSelection(): void;
focus(): void;
}
},
plugins: {
get(command: string)
},
data: {
processor: {
toView(html: string);
};
toModel(viewFeragment: any);
},
ui: {
view: {
toolbar: {
items: any[];
element: HTMLElement;
}
}
}
conversion: {
for(filter: string): {
markerToHighlight(data: {
model: string;
view: (data: {
markerName: string;
}) => void;
})
}
}
getData(): string;
setData(data: string): void;
getSelectedHtml(): string;
removeSelection(): void;
execute<T>(action: string, ...args: unknown[]): T;
focus(): void;
sourceElement: HTMLElement;
}
interface EditingState {
highlightedResult: string;
results: unknown[];
}
interface CKFindResult {
results: {
get(number): {
marker: {
getStart(): TextPosition;
getRange(): number;
};
}
} & [];
}
interface MentionItem {
action?: string;
noteTitle?: string;
id: string;
name: string;
link?: string;
notePath?: string;
highlightedNotePathTitle?: string;
}
interface MentionConfig {
feeds: {
marker: string;
feed: (queryText: string) => MentionItem[] | Promise<MentionItem[]>;
itemRenderer?: (item: {
highlightedNotePathTitle: string
}) => void;
minimumCharacters: number;
}[];
}
/*
* Panzoom
*/

View File

@@ -1,10 +1,10 @@
import { t } from "../../services/i18n.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import noteAutocompleteService from "../../services/note_autocomplete.js";
import noteAutocompleteService, { type Suggestion } from "../../services/note_autocomplete.js";
import server from "../../services/server.js";
import contextMenuService from "../../menus/context_menu.js";
import attributeParser, { type Attribute } from "../../services/attribute_parser.js";
import libraryLoader from "../../services/library_loader.js";
import { AttributeEditor, type EditorConfig, type Element, type MentionFeed, type Node, type Position } from "@triliumnext/ckeditor5";
import froca from "../../services/froca.js";
import attributeRenderer from "../../services/attribute_renderer.js";
import noteCreateService from "../../services/note_create.js";
@@ -15,7 +15,6 @@ import type { CommandData, EventData, EventListener, FilteredCommandNames } from
import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js";
import type FNote from "../../entities/fnote.js";
import { escapeQuotes } from "../../services/utils.js";
import { buildConfig } from "../type_widgets/ckeditor/config.js";
const HELP_TEXT = `
<p>${t("attribute_editor.help_text_body1")}</p>
@@ -85,109 +84,59 @@ const TPL = /*html*/`
</div>
`;
const mentionSetup: MentionConfig = {
feeds: [
{
marker: "@",
feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
itemRenderer: (item) => {
const itemElement = document.createElement("button");
const mentionSetup: MentionFeed[] = [
{
marker: "@",
feed: (queryText) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
itemRenderer: (_item) => {
const item = _item as Suggestion;
const itemElement = document.createElement("button");
itemElement.innerHTML = `${item.highlightedNotePathTitle} `;
itemElement.innerHTML = `${item.highlightedNotePathTitle} `;
return itemElement;
},
minimumCharacters: 0
return itemElement;
},
{
marker: "#",
feed: async (queryText) => {
const names = await server.get<string[]>(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`);
minimumCharacters: 0
},
{
marker: "#",
feed: async (queryText) => {
const names = await server.get<string[]>(`attribute-names/?type=label&query=${encodeURIComponent(queryText)}`);
return names.map((name) => {
return {
id: `#${name}`,
name: name
};
});
},
minimumCharacters: 0
return names.map((name) => {
return {
id: `#${name}`,
name: name
};
});
},
{
marker: "~",
feed: async (queryText) => {
const names = await server.get<string[]>(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`);
minimumCharacters: 0
},
{
marker: "~",
feed: async (queryText) => {
const names = await server.get<string[]>(`attribute-names/?type=relation&query=${encodeURIComponent(queryText)}`);
return names.map((name) => {
return {
id: `~${name}`,
name: name
};
});
},
minimumCharacters: 0
}
]
};
return names.map((name) => {
return {
id: `~${name}`,
name: name
};
});
},
minimumCharacters: 0
}
];
const editorConfig = {
...buildConfig(),
removePlugins: [
"Heading",
"Link",
"Autoformat",
"Bold",
"Italic",
"Underline",
"Strikethrough",
"Code",
"Superscript",
"Subscript",
"BlockQuote",
"Image",
"ImageCaption",
"ImageStyle",
"ImageToolbar",
"ImageUpload",
"ImageResize",
"List",
"TodoList",
"PasteFromOffice",
"Table",
"TableToolbar",
"TableProperties",
"TableCellProperties",
"Indent",
"IndentBlock",
"BlockToolbar",
"ParagraphButtonUI",
"HeadingButtonsUI",
"UploadimagePlugin",
"InternalLinkPlugin",
"MarkdownImportPlugin",
"CuttonotePlugin",
"TextTransformation",
"Font",
"FontColor",
"FontBackgroundColor",
"CodeBlock",
"SelectAll",
"IncludeNote",
"CutToNote",
"Math",
"AutoformatMath",
"indentBlockShortcutPlugin",
"removeFormatLinksPlugin",
"Footnotes",
"Mermaid",
"Kbd",
"Admonition"
],
const editorConfig: EditorConfig = {
toolbar: {
items: []
},
placeholder: t("attribute_editor.placeholder"),
mention: mentionSetup
mention: {
feeds: mentionSetup
},
licenseKey: "GPL"
};
type AttributeCommandNames = FilteredCommandNames<CommandData>;
@@ -199,7 +148,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
private $saveAttributesButton!: JQuery<HTMLElement>;
private $errors!: JQuery<HTMLElement>;
private textEditor!: TextEditor;
private textEditor!: AttributeEditor;
private lastUpdatedNoteId!: string | undefined;
private lastSavedContent!: string;
@@ -369,13 +318,11 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
}
async initEditor() {
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
this.$widget.show();
this.$editor.on("click", (e) => this.handleEditorClick(e));
this.textEditor = await CKEditor.BalloonEditor.create(this.$editor[0], editorConfig);
this.textEditor = await AttributeEditor.create(this.$editor[0], editorConfig);
this.textEditor.model.document.on("change:data", () => this.dataChanged());
this.textEditor.editing.view.document.on(
"enter",
@@ -388,7 +335,10 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
);
// disable spellcheck for attribute editor
this.textEditor.editing.view.change((writer) => writer.setAttribute("spellcheck", "false", this.textEditor.editing.view.document.getRoot()));
const documentRoot = this.textEditor.editing.view.document.getRoot();
if (documentRoot) {
this.textEditor.editing.view.change((writer) => writer.setAttribute("spellcheck", "false", documentRoot));
}
}
dataChanged() {
@@ -465,18 +415,18 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
this.$editor.tooltip("show");
}
getClickIndex(pos: TextPosition) {
let clickIndex = pos.offset - pos.textNode.startOffset;
getClickIndex(pos: Position) {
let clickIndex = pos.offset - (pos.textNode?.startOffset ?? 0);
let curNode = pos.textNode;
let curNode: Node | Text | Element | null = pos.textNode;
while (curNode.previousSibling) {
while (curNode?.previousSibling) {
curNode = curNode.previousSibling;
if (curNode.name === "reference") {
clickIndex += curNode._attrs.get("notePath").length + 1;
} else {
clickIndex += curNode.data.length;
if ((curNode as Element).name === "reference") {
clickIndex += (curNode.getAttribute("notePath") as string).length + 1;
} else if ("data" in curNode) {
clickIndex += (curNode.data as string).length;
}
}
@@ -534,8 +484,12 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget implem
this.$editor.trigger("focus");
this.textEditor.model.change((writer) => {
const positionAt = writer.createPositionAt(this.textEditor.model.document.getRoot(), "end");
const documentRoot = this.textEditor.editing.model.document.getRoot();
if (!documentRoot) {
return;
}
const positionAt = writer.createPositionAt(documentRoot, "end");
writer.setSelection(positionAt);
});
}

View File

@@ -10,7 +10,7 @@ import utils from "../../services/utils.js";
import { Dropdown } from "bootstrap";
import type FAttachment from "../../entities/fattachment.js";
import type AttachmentDetailWidget from "../attachment_detail.js";
import { NoteRow } from "@triliumnext/commons";
import type { NoteRow } from "@triliumnext/commons";
const TPL = /*html*/`
<div class="dropdown attachment-actions">
@@ -83,7 +83,7 @@ const TPL = /*html*/`
// TODO: Deduplicate
interface AttachmentResponse {
note: NoteRow;
note: NoteRow;
}
export default class AttachmentActionsWidget extends BasicWidget {

View File

@@ -11,7 +11,7 @@ import dayjs, { Dayjs } from "dayjs";
import utc from "dayjs/plugin/utc.js";
import isSameOrAfter from "dayjs/plugin/isSameOrAfter.js";
import "../../stylesheets/calendar.css";
import { AttributeRow } from "@triliumnext/commons";
import type { AttributeRow } from "@triliumnext/commons";
dayjs.extend(utc);
dayjs.extend(isSameOrAfter);

View File

@@ -1,3 +1,4 @@
import type { FindAndReplaceState, FindCommandResult } from "@triliumnext/ckeditor5";
import type { FindResult } from "./find.js";
import type FindWidget from "./find.js";
@@ -14,8 +15,8 @@ interface Match {
export default class FindInText {
private parent: FindWidget;
private findResult?: CKFindResult | null;
private editingState?: EditingState;
private findResult?: FindCommandResult | null;
private editingState?: FindAndReplaceState;
constructor(parent: FindWidget) {
this.parent = parent;
@@ -40,7 +41,7 @@ export default class FindInText {
// Clear
const findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing");
findAndReplaceEditing.state.clear(model);
findAndReplaceEditing.state?.clear(model);
findAndReplaceEditing.stop();
this.editingState = findAndReplaceEditing.state;
if (searchTerm !== "") {
@@ -52,7 +53,7 @@ export default class FindInText {
// let m = text.match(re);
// totalFound = m ? m.length : 0;
const options = { matchCase: matchCase, wholeWords: wholeWord };
findResult = textEditor.execute<CKFindResult>("find", searchTerm, options);
findResult = textEditor.execute("find", searchTerm, options);
totalFound = findResult.results.length;
const selection = model.document.selection;
// If text is selected, highlight the corresponding result;
@@ -60,17 +61,17 @@ export default class FindInText {
if (!selection.isCollapsed) {
const cursorPos = selection.getFirstPosition();
for (let i = 0; i < findResult.results.length; ++i) {
const marker = findResult.results.get(i).marker;
const fromPos = marker.getStart();
if (cursorPos && fromPos.compareWith(cursorPos) !== "before") {
const marker = findResult.results.get(i)?.marker;
const fromPos = marker?.getStart();
if (cursorPos && fromPos?.compareWith(cursorPos) !== "before") {
currentFound = i;
break;
}
}
} else {
const editorEl = textEditor?.sourceElement;
const findResultElement = editorEl.querySelectorAll(".ck-find-result");
const scrollingContainer = editorEl.closest('.scrolling-container');
const findResultElement = editorEl?.querySelectorAll(".ck-find-result");
const scrollingContainer = editorEl?.closest('.scrolling-container');
const containerTop = scrollingContainer?.getBoundingClientRect().top ?? 0;
const closestIndex = Array.from(findResultElement ?? []).findIndex((el) => el.getBoundingClientRect().top >= containerTop);
currentFound = closestIndex >= 0 ? closestIndex : 0;
@@ -86,7 +87,7 @@ export default class FindInText {
// XXX Do this accessing the private data?
// See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findnextcommand.js
for (let i = 0; i < currentFound; ++i) {
textEditor?.execute("findNext", searchTerm);
textEditor?.execute("findNext");
}
}
@@ -120,17 +121,17 @@ export default class FindInText {
// Clear the markers and set the caret to the
// current occurrence
const model = textEditor.model;
const range = this.findResult?.results?.get(currentFound).marker.getRange();
const range = this.findResult?.results?.get(currentFound)?.marker?.getRange();
// From
// https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findandreplace.js#L92
// XXX Roll our own since already done for codeEditor and
// will probably allow more refactoring?
let findAndReplaceEditing = textEditor.plugins.get("FindAndReplaceEditing");
findAndReplaceEditing.state.clear(model);
findAndReplaceEditing.state?.clear(model);
findAndReplaceEditing.stop();
if (range) {
model.change((writer) => {
writer.setSelection(range, 0);
writer.setSelection(range);
});
}
textEditor.editing.view.scrollToTheSelection();

View File

@@ -1,6 +1,11 @@
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 { getHighlightJsNameForMime } from "../../../services/mime_types.js";
import options from "../../../services/options.js";
import { isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
import utils from "../../../services/utils.js";
import emojiDefinitionsUrl from "@triliumnext/ckeditor5/emoji_definitions/en.json?external";
const TEXT_FORMATTING_GROUP = {
label: "Text formatting",
@@ -18,7 +23,6 @@ export function buildConfig() {
"alignBlockRight",
"alignLeft",
"alignRight",
"full", // full and side are for BC since the old images have been created with these styles
"side"
]
},
@@ -96,6 +100,18 @@ export function buildConfig() {
defaultProtocol: "https://",
allowedProtocols: ALLOWED_PROTOCOLS
},
emoji: {
definitionsUrl: emojiDefinitionsUrl
},
syntaxHighlighting: {
async loadHighlightJs() {
await library_loader.requireLibrary(library_loader.HIGHLIGHT_JS);
return hljs;
},
mapLanguageName: getHighlightJsNameForMime,
defaultMimeType: MIME_TYPE_AUTO,
enabled: isSyntaxHighlightEnabled
},
// This value must be kept in sync with the language defined in webpack.config.js.
language: "en"
};
@@ -169,7 +185,7 @@ export function buildClassicToolbar(multilineToolbar: boolean) {
{
label: "Insert",
icon: "plus",
items: ["imageUpload", "|", "link", "internallink", "includeNote", "|", "specialCharacters", "math", "mermaid", "horizontalLine", "pageBreak"]
items: ["imageUpload", "|", "link", "bookmark", "internallink", "includeNote", "|", "specialCharacters", "emoji", "math", "mermaid", "horizontalLine", "pageBreak"]
},
"|",
"outdent",
@@ -202,6 +218,7 @@ export function buildFloatingToolbar() {
"|",
"code",
"link",
"bookmark",
"removeFormat",
"internallink",
"cuttonote"
@@ -232,6 +249,7 @@ export function buildFloatingToolbar() {
"imageUpload",
"markdownImport",
"specialCharacters",
"emoji",
"findAndReplace"
]
};

View File

@@ -1,360 +0,0 @@
/*
* This code is an adaptation of https://github.com/antoniotejada/Trilium-SyntaxHighlightWidget with additional improvements, such as:
*
* - support for selecting the language manually;
* - support for determining the language automatically, if a special language is selected ("Auto-detected");
* - limit for highlighting.
*
* TODO: Generally this class can be done directly in the CKEditor repository.
*/
import library_loader from "../../../services/library_loader.js";
import mime_types from "../../../services/mime_types.js";
import { isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
export async function initSyntaxHighlighting(editor: TextEditor) {
if (!isSyntaxHighlightEnabled) {
return;
}
await library_loader.requireLibrary(library_loader.HIGHLIGHT_JS);
initTextEditor(editor);
}
const HIGHLIGHT_MAX_BLOCK_COUNT = 500;
const tag = "SyntaxHighlightWidget";
const debugLevels = ["error", "warn", "info", "log", "debug"];
const debugLevel = debugLevels.indexOf("warn");
let warn = function (...args: unknown[]) {};
if (debugLevel >= debugLevels.indexOf("warn")) {
warn = console.warn.bind(console, tag + ": ");
}
let info = function (...args: unknown[]) {};
if (debugLevel >= debugLevels.indexOf("info")) {
info = console.info.bind(console, tag + ": ");
}
let log = function (...args: unknown[]) {};
if (debugLevel >= debugLevels.indexOf("log")) {
log = console.log.bind(console, tag + ": ");
}
let dbg = function (...args: unknown[]) {};
if (debugLevel >= debugLevels.indexOf("debug")) {
dbg = console.debug.bind(console, tag + ": ");
}
function assert(e: boolean, msg?: string) {
console.assert(e, tag + ": " + msg);
}
// TODO: Should this be scoped to note?
let markerCounter = 0;
function initTextEditor(textEditor: TextEditor) {
log("initTextEditor");
const document = textEditor.model.document;
// Create a conversion from model to view that converts
// hljs:hljsClassName:uniqueId into a span with hljsClassName
// See the list of hljs class names at
// https://github.com/highlightjs/highlight.js/blob/6b8c831f00c4e87ecd2189ebbd0bb3bbdde66c02/docs/css-classes-reference.rst
textEditor.conversion.for("editingDowncast").markerToHighlight({
model: "hljs",
view: ({ markerName }) => {
dbg("markerName " + markerName);
// markerName has the pattern addMarker:cssClassName:uniqueId
const [, cssClassName, id] = markerName.split(":");
// The original code at
// https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-find-and-replace/src/findandreplaceediting.js
// has this comment
// Marker removal from the view has a bug:
// https://github.com/ckeditor/ckeditor5/issues/7499
// A minimal option is to return a new object for each converted marker...
return {
name: "span",
classes: [cssClassName],
attributes: {
// ...however, adding a unique attribute should be future-proof..
"data-syntax-result": id
}
};
}
});
// XXX This is done at BalloonEditor.create time, so it assumes this
// document is always attached to this textEditor, empirically that
// seems to be the case even with two splits showing the same note,
// it's not clear if CKEditor5 has apis to attach and detach
// documents around
document.registerPostFixer(function (writer) {
log("postFixer");
// Postfixers are a simpler way of tracking changes than onchange
// See
// https://github.com/ckeditor/ckeditor5/blob/b53d2a4b49679b072f4ae781ac094e7e831cfb14/packages/ckeditor5-block-quote/src/blockquoteediting.js#L54
const changes = document.differ.getChanges();
let dirtyCodeBlocks = new Set<CKNode>();
function lookForCodeBlocks(node: CKNode) {
for (const child of node._children) {
if (child.is("element", "paragraph")) {
continue;
}
if (child.is("element", "codeBlock")) {
dirtyCodeBlocks.add(child);
} else if (child.childCount > 0) {
lookForCodeBlocks(child);
}
}
}
for (const change of changes) {
dbg("change " + JSON.stringify(change));
if (change.name !== "paragraph" && change.name !== "codeBlock" && change?.position?.nodeAfter && change.position.nodeAfter.childCount > 0) {
/*
* We need to look for code blocks recursively, as they can be placed within a <div> due to
* general HTML support or normally underneath other elements such as tables, blockquotes, etc.
*/
lookForCodeBlocks(change.position.nodeAfter);
} else if (change.type == "insert" && change.name == "codeBlock") {
// A new code block was inserted
const codeBlock = change.position?.nodeAfter;
// Even if it's a new codeblock, it needs dirtying in case
// it already has children, like when pasting one or more
// full codeblocks, undoing a delete, changing the language,
// etc (the postfixer won't get later changes for those).
if (codeBlock) {
log("dirtying inserted codeBlock " + JSON.stringify(codeBlock.toJSON()));
dirtyCodeBlocks.add(codeBlock);
}
} else if (change.type == "remove" && change.name == "codeBlock" && change.position) {
// An existing codeblock was removed, do nothing. Note the
// node is no longer in the editor so the codeblock cannot
// be inspected here. No need to dirty the codeblock since
// it has been removed
log("removing codeBlock at path " + JSON.stringify(change.position.toJSON()));
} else if ((change.type == "remove" || change.type == "insert") && change?.position?.parent.is("element", "codeBlock")) {
// Text was added or removed from the codeblock, force a
// highlight
const codeBlock = change.position.parent;
log("dirtying codeBlock " + JSON.stringify(codeBlock.toJSON()));
dirtyCodeBlocks.add(codeBlock);
}
}
for (let codeBlock of dirtyCodeBlocks) {
highlightCodeBlock(codeBlock, writer);
}
// Adding markers doesn't modify the document data so no need for
// postfixers to run again
return false;
});
// This assumes the document is empty and a explicit call to highlight
// is not necessary here. Empty documents have a single children of type
// paragraph with no text
assert(document.getRoot().childCount == 1 && document.getRoot().getChild(0).name == "paragraph" && document.getRoot().getChild(0).isEmpty);
}
/**
* This implements highlighting via ephemeral markers (not stored in the
* document).
*
* XXX Another option would be to use formatting markers, which would have
* the benefit of making it work for readonly notes. On the flip side,
* the formatting would be stored with the note and it would need a
* way to remove that formatting when editing back the note.
*/
function highlightCodeBlock(codeBlock: CKNode, writer: Writer) {
log("highlighting codeblock " + JSON.stringify(codeBlock.toJSON()));
const model = codeBlock.root.document.model;
// Can't invoke addMarker with an already existing marker name,
// clear all highlight markers first. Marker names follow the
// pattern hljs:cssClassName:uniqueId, eg hljs:hljs-comment:1
const codeBlockRange = model.createRangeIn(codeBlock);
for (const marker of model.markers.getMarkersIntersectingRange(codeBlockRange)) {
dbg("removing marker " + marker.name);
writer.removeMarker(marker.name);
}
// Don't highlight if plaintext (note this needs to remove the markers
// above first, in case this was a switch from non plaintext to
// plaintext)
const mimeType = codeBlock.getAttribute("language");
if (mimeType == "text-plain") {
// XXX There's actually a plaintext language that could be used
// if you wanted the non-highlight formatting of
// highlight.js css applied, see
// https://github.com/highlightjs/highlight.js/issues/700
log("not highlighting plaintext codeblock");
return;
}
// Find the corresponding language for the given mimetype.
const highlightJsLanguage = mime_types.getHighlightJsNameForMime(mimeType);
if (mimeType !== mime_types.MIME_TYPE_AUTO && !highlightJsLanguage) {
console.warn(`Unsupported highlight.js for mime type ${mimeType}.`);
return;
}
// Don't highlight if the code is too big, as the typing performance will be highly degraded.
if (codeBlock.childCount >= HIGHLIGHT_MAX_BLOCK_COUNT) {
return;
}
// highlight.js needs the full text without HTML tags, eg for the
// text
// #include <stdio.h>
// the highlighted html is
// <span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string">&lt;stdio.h&gt;</span></span>
// But CKEditor codeblocks have <br> instead of \n
// Do a two pass algorithm:
// - First pass collect the codeblock children text, change <br> to
// \n
// - invoke highlight.js on the collected text generating html
// - Second pass parse the highlighted html spans and match each
// char to the CodeBlock text. Issue addMarker CKEditor calls for
// each span
// XXX This is brittle and assumes how highlight.js generates html
// (blanks, which characters escapes, etc), a better approach
// would be to use highlight.js beta api TreeTokenizer?
// Collect all the text nodes to pass to the highlighter Text is
// direct children of the codeBlock
let text = "";
for (let i = 0; i < codeBlock.childCount; ++i) {
let child = codeBlock.getChild(i);
// We only expect text and br elements here
if (child.is("$text")) {
dbg("child text " + child.data);
text += child.data;
} else if (child.is("element") && child.name == "softBreak") {
dbg("softBreak");
text += "\n";
} else {
warn("Unkown child " + JSON.stringify(child.toJSON()));
}
}
let highlightRes;
if (mimeType === mime_types.MIME_TYPE_AUTO) {
highlightRes = hljs.highlightAuto(text);
} else {
highlightRes = hljs.highlight(text, { language: highlightJsLanguage });
}
dbg("text\n" + text);
dbg("html\n" + highlightRes.value);
let iHtml = 0;
let html = highlightRes.value;
let spanStack = [];
let iChild = -1;
let childText = "";
let child = null;
let iChildText = 0;
while (iHtml < html.length) {
// Advance the text index and fetch a new child if necessary
if (iChildText >= childText.length) {
iChild++;
if (iChild < codeBlock.childCount) {
dbg("Fetching child " + iChild);
child = codeBlock.getChild(iChild);
if (child.is("$text")) {
dbg("child text " + child.data);
childText = child.data;
iChildText = 0;
} else if (child.is("element", "softBreak")) {
dbg("softBreak");
iChildText = 0;
childText = "\n";
} else {
warn("child unknown!!!");
}
} else {
// Don't bail if beyond the last children, since there's
// still html text, it must be a closing span tag that
// needs to be dealt with below
childText = "";
}
}
// This parsing is made slightly simpler and faster by only
// expecting <span> and </span> tags in the highlighted html
if (html[iHtml] == "<" && html[iHtml + 1] != "/") {
// new span, note they can be nested eg C preprocessor lines
// are inside a hljs-meta span, hljs-title function names
// inside a hljs-function span, etc
let iStartQuot = html.indexOf('"', iHtml + 1);
let iEndQuot = html.indexOf('"', iStartQuot + 1);
let className = html.slice(iStartQuot + 1, iEndQuot);
// XXX highlight js uses scope for Python "title function_",
// etc for now just use the first style only
// See https://highlightjs.readthedocs.io/en/latest/css-classes-reference.html#a-note-on-scopes-with-sub-scopes
let iBlank = className.indexOf(" ");
if (iBlank > 0) {
className = className.slice(0, iBlank);
}
dbg("Found span start " + className);
iHtml = html.indexOf(">", iHtml) + 1;
// push the span
let posStart = writer.createPositionAt(codeBlock, (child?.startOffset ?? 0) + iChildText);
spanStack.push({ className: className, posStart: posStart });
} else if (html[iHtml] == "<" && html[iHtml + 1] == "/") {
// Done with this span, pop the span and mark the range
iHtml = html.indexOf(">", iHtml + 1) + 1;
let stackTop = spanStack.pop();
let posStart = stackTop?.posStart;
let className = stackTop?.className;
let posEnd = writer.createPositionAt(codeBlock, (child?.startOffset ?? 0) + iChildText);
let range = writer.createRange(posStart, posEnd);
let markerName = "hljs:" + className + ":" + markerCounter;
// Use an incrementing number for the uniqueId, random of
// 10000000 is known to cause collisions with a few
// codeblocks of 10s of lines on real notes (each line is
// one or more marker).
// Wrap-around for good measure so all numbers are positive
// XXX Another option is to catch the exception and retry or
// go through the markers and get the largest + 1
markerCounter = (markerCounter + 1) & 0xffffff;
dbg("Found span end " + className);
dbg("Adding marker " + markerName + ": " + JSON.stringify(range.toJSON()));
writer.addMarker(markerName, { range: range, usingOperation: false });
} else {
// Text, we should also have text in the children
assert(iChild < codeBlock.childCount && iChildText < childText.length, "Found text in html with no corresponding child text!!!!");
if (html[iHtml] == "&") {
// highlight.js only encodes
// .replace(/&/g, '&amp;')
// .replace(/</g, '&lt;')
// .replace(/>/g, '&gt;')
// .replace(/"/g, '&quot;')
// .replace(/'/g, '&#x27;');
// see https://github.com/highlightjs/highlight.js/blob/7addd66c19036eccd7c602af61f1ed84d215c77d/src/lib/utils.js#L5
let iAmpEnd = html.indexOf(";", iHtml);
dbg(html.slice(iHtml, iAmpEnd));
iHtml = iAmpEnd + 1;
} else {
// regular text
dbg(html[iHtml]);
iHtml++;
}
iChildText++;
}
}
}

View File

@@ -1,6 +1,6 @@
import { t } from "../../services/i18n.js";
import libraryLoader from "../../services/library_loader.js";
import noteAutocompleteService from "../../services/note_autocomplete.js";
import noteAutocompleteService, { type Suggestion } from "../../services/note_autocomplete.js";
import mimeTypesService from "../../services/mime_types.js";
import utils, { hasTouchBar } from "../../services/utils.js";
import keyboardActionService from "../../services/keyboard_actions.js";
@@ -10,7 +10,6 @@ import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
import link from "../../services/link.js";
import appContext, { type CommandListenerData, type EventData } from "../../components/app_context.js";
import dialogService from "../../services/dialog.js";
import { initSyntaxHighlighting } from "./ckeditor/syntax_highlight.js";
import options from "../../services/options.js";
import toast from "../../services/toast.js";
import { normalizeMimeTypeForCKEditor } from "../../services/mime_type_definitions.js";
@@ -18,25 +17,25 @@ 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";
const ENABLE_INSPECTOR = false;
const mentionSetup: MentionConfig = {
feeds: [
{
marker: "@",
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
itemRenderer: (item) => {
const itemElement = document.createElement("button");
const mentionSetup: MentionFeed[] = [
{
marker: "@",
feed: (queryText: string) => noteAutocompleteService.autocompleteSourceForCKEditor(queryText),
itemRenderer: (item) => {
const itemElement = document.createElement("button");
itemElement.innerHTML = `${item.highlightedNotePathTitle} `;
itemElement.innerHTML = `${(item as Suggestion).highlightedNotePathTitle} `;
return itemElement;
},
minimumCharacters: 0
}
]
};
return itemElement;
},
minimumCharacters: 0
}
];
const TPL = /*html*/`
<div class="note-detail-editable-text note-detail-printable">
@@ -127,7 +126,7 @@ function buildListOfLanguages() {
export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
private contentLanguage?: string | null;
private watchdog!: CKWatchdog;
private watchdog!: EditorWatchdog<ClassicEditor | PopupEditor>;
private $editor!: JQuery<HTMLElement>;
@@ -149,16 +148,15 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
}
async initEditor() {
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
const isClassicEditor = utils.isMobile() || options.get("textNoteEditorType") === "ckeditor-classic";
const editorClass = isClassicEditor ? CKEditor.DecoupledEditor : CKEditor.BalloonEditor;
const editorClass = isClassicEditor ? ClassicEditor : PopupEditor;
// CKEditor since version 12 needs the element to be visible before initialization. At the same time,
// we want to avoid flicker - i.e., show editor only once everything is ready. That's why we have separate
// display of $widget in both branches.
this.$widget.show();
this.watchdog = new CKEditor.EditorWatchdog(editorClass, {
const config: WatchdogConfig = {
// An average number of milliseconds between the last editor errors (defaults to 5000).
// When the period of time between errors is lower than that and the crashNumberLimit
// is also reached, the watchdog changes its state to crashedPermanently, and it stops
@@ -173,7 +171,8 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
// A minimum number of milliseconds between saving the editor data internally (defaults to 5000).
// Note that for large documents, this might impact the editor performance.
saveInterval: 5000
});
};
this.watchdog = isClassicEditor ? new EditorWatchdog(ClassicEditor, config) : new EditorWatchdog(PopupEditor, config);
this.watchdog.on("stateChange", () => {
const currentState = this.watchdog.state;
@@ -189,7 +188,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
if (currentState === "crashedPermanently") {
dialogService.info(`Editing component keeps crashing. Please try restarting Trilium. If problem persists, consider creating a bug report.`);
this.watchdog.editor.enableReadOnlyMode("crashed-editor");
this.watchdog.editor?.enableReadOnlyMode("crashed-editor");
}
});
@@ -205,11 +204,14 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
styles: true,
classes: true,
attributes: true
}
},
licenseKey: "GPL"
};
const contentLanguage = this.note?.getLabelValue("language");
if (contentLanguage) {
// TODO: Wrong type?
//@ts-ignore
finalConfig.language = {
ui: (typeof finalConfig.language === "string" ? finalConfig.language : "en"),
content: contentLanguage
@@ -219,10 +221,11 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.contentLanguage = null;
}
//@ts-ignore
const editor = await editorClass.create(elementOrData, finalConfig);
const notificationsPlugin = editor.plugins.get("Notification");
notificationsPlugin.on("show:warning", (evt: CKEvent, data: PluginEventData) => {
notificationsPlugin.on("show:warning", (evt, data) => {
const title = data.title;
const message = data.message.message;
@@ -235,8 +238,6 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
evt.stop();
});
await initSyntaxHighlighting(editor);
if (isClassicEditor) {
let $classicToolbarWidget;
if (!utils.isMobile()) {
@@ -248,7 +249,10 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
$classicToolbarWidget.empty();
if ($classicToolbarWidget.length) {
$classicToolbarWidget[0].appendChild(editor.ui.view.toolbar.element);
const toolbarView = (editor as ClassicEditor).ui.view.toolbar;
if (toolbarView.element) {
$classicToolbarWidget[0].appendChild(toolbarView.element);
}
}
if (utils.isMobile()) {
@@ -256,17 +260,18 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
// Reposition all dropdowns to point upwards instead of downwards.
// See https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for more info.
const toolbarView = editor.ui.view.toolbar;
const toolbarView = (editor as ClassicEditor).ui.view.toolbar;
for (const item of toolbarView.items) {
if (!("panelView" in item)) {
continue;
}
item.on("change:isOpen", () => {
if ( !item.isOpen ) {
if (!("isOpen" in item) || !item.isOpen ) {
return;
}
// @ts-ignore
item.panelView.position = item.panelView.position.replace("s", "n");
});
}
@@ -276,15 +281,14 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
editor.model.document.on("change:data", () => this.spacedUpdate.scheduleUpdate());
if (glob.isDev && ENABLE_INSPECTOR) {
// TODO: Check if this still works.
await import(/* webpackIgnore: true */ "../../../libraries/ckeditor/inspector.js");
const CKEditorInspector = (await import("@ckeditor/ckeditor5-inspector")).default;
CKEditorInspector.attach(editor);
}
// Touch bar integration
if (hasTouchBar) {
for (const event of [ "bold", "italic", "underline", "paragraph", "heading" ]) {
editor.commands.get(event).on("change", () => this.triggerCommand("refreshTouchBar"));
editor.commands.get(event)?.on("change", () => this.triggerCommand("refreshTouchBar"));
}
}
@@ -297,6 +301,7 @@ 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,
codeBlock: {
languages: buildListOfLanguages()
@@ -324,13 +329,13 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
if (this.contentLanguage !== newContentLanguage) {
await this.reinitialize(data);
} else {
this.watchdog.editor.setData(data);
this.watchdog.editor?.setData(data);
}
});
}
getData() {
const content = this.watchdog.editor.getData();
const content = this.watchdog.editor?.getData() ?? "";
// if content is only tags/whitespace (typically <p>&nbsp;</p>), then just make it empty,
// this is important when setting a new note to code
@@ -344,11 +349,14 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
}
scrollToEnd() {
this.watchdog?.editor.model.change((writer) => {
writer.setSelection(writer.createPositionAt(this.watchdog?.editor.model.document.getRoot(), "end"));
this.watchdog?.editor?.model.change((writer) => {
const rootItem = this.watchdog?.editor?.model.document.getRoot();
if (rootItem) {
writer.setSelection(writer.createPositionAt(rootItem, "end"));
}
});
this.watchdog?.editor.editing.view.focus();
this.watchdog?.editor?.editing.view.focus();
}
show() { }
@@ -360,7 +368,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
cleanup() {
if (this.watchdog?.editor) {
this.spacedUpdate.allowUpdateWithoutChange(() => {
this.watchdog.editor.setData("");
this.watchdog.editor?.setData("");
});
}
}
@@ -375,18 +383,22 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
async addLinkToEditor(linkHref: string, linkTitle: string) {
await this.initialized;
this.watchdog.editor.model.change((writer) => {
const insertPosition = this.watchdog.editor.model.document.selection.getFirstPosition();
writer.insertText(linkTitle, { linkHref: linkHref }, insertPosition);
this.watchdog.editor?.model.change((writer) => {
const insertPosition = this.watchdog.editor?.model.document.selection.getFirstPosition();
if (insertPosition) {
writer.insertText(linkTitle, { linkHref: linkHref }, insertPosition);
}
});
}
async addTextToEditor(text: string) {
await this.initialized;
this.watchdog.editor.model.change((writer) => {
const insertPosition = this.watchdog.editor.model.document.selection.getLastPosition();
writer.insertText(text, insertPosition);
this.watchdog.editor?.model.change((writer) => {
const insertPosition = this.watchdog.editor?.model.document.selection.getLastPosition();
if (insertPosition) {
writer.insertText(text, insertPosition);
}
});
}
@@ -403,23 +415,23 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
if (linkTitle) {
if (this.hasSelection()) {
this.watchdog.editor.execute("link", externalLink ? `${notePath}` : `#${notePath}`);
this.watchdog.editor?.execute("link", externalLink ? `${notePath}` : `#${notePath}`);
} else {
await this.addLinkToEditor(externalLink ? `${notePath}` : `#${notePath}`, linkTitle);
}
} else {
this.watchdog.editor.execute("referenceLink", { href: "#" + notePath });
this.watchdog.editor?.execute("referenceLink", { href: "#" + notePath });
}
this.watchdog.editor.editing.view.focus();
this.watchdog.editor?.editing.view.focus();
}
// returns true if user selected some text, false if there's no selection
hasSelection() {
const model = this.watchdog.editor.model;
const selection = model.document.selection;
const model = this.watchdog.editor?.model;
const selection = model?.document.selection;
return !selection.isCollapsed;
return !selection?.isCollapsed;
}
async executeWithTextEditorEvent({ callback, resolve, ntxId }: EventData<"executeWithTextEditor">) {
@@ -429,11 +441,15 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
await this.initialized;
if (callback) {
callback(this.watchdog.editor);
if (!this.watchdog.editor) {
return;
}
resolve(this.watchdog.editor);
if (callback) {
callback(this.watchdog.editor as CKTextEditor);
}
resolve(this.watchdog.editor as CKTextEditor);
}
addLinkToTextCommand() {
@@ -443,11 +459,15 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
}
getSelectedText() {
const range = this.watchdog.editor.model.document.selection.getFirstRange();
const range = this.watchdog.editor?.model.document.selection.getFirstRange();
let text = "";
if (!range) {
return text;
}
for (const item of range.getItems()) {
if (item.data) {
if ("data" in item && item.data) {
text += item.data;
}
}
@@ -458,12 +478,12 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
async followLinkUnderCursorCommand() {
await this.initialized;
const selection = this.watchdog.editor.model.document.selection;
const selectedElement = selection.getSelectedElement();
const selection = this.watchdog.editor?.model.document.selection;
const selectedElement = selection?.getSelectedElement();
if (selectedElement?.name === "reference") {
// reference link
const notePath = selectedElement.getAttribute("notePath");
const notePath = selectedElement.getAttribute("notePath") as string | undefined;
if (notePath) {
await appContext.tabManager.getActiveContext()?.setNote(notePath);
@@ -471,11 +491,11 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
}
}
if (!selection.hasAttribute("linkHref")) {
if (!selection?.hasAttribute("linkHref")) {
return;
}
const selectedLinkUrl = selection.getAttribute("linkHref");
const selectedLinkUrl = selection.getAttribute("linkHref") as string;
const notePath = link.getNotePathFromUrl(selectedLinkUrl);
if (notePath) {
@@ -490,10 +510,10 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
}
addIncludeNote(noteId: string, boxSize?: string) {
this.watchdog.editor.model.change((writer) => {
this.watchdog.editor?.model.change((writer) => {
// Insert <includeNote>*</includeNote> at the current selection position
// in a way that will result in creating a valid model structure
this.watchdog.editor.model.insertContent(
this.watchdog.editor?.model.insertContent(
writer.createElement("includeNote", {
noteId: noteId,
boxSize: boxSize
@@ -504,7 +524,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
async addImage(noteId: string) {
const note = await froca.getNote(noteId);
if (!note) {
if (!note || !this.watchdog.editor) {
return;
}
@@ -512,7 +532,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
const encodedTitle = encodeURIComponent(note.title);
const src = `api/images/${note.noteId}/${encodedTitle}`;
this.watchdog.editor.execute("insertImage", { source: src });
this.watchdog.editor?.execute("insertImage", { source: src });
});
}
@@ -544,12 +564,12 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
this.watchdog.destroy();
await this.createEditor();
this.watchdog.editor.setData(data);
this.watchdog.editor?.setData(data);
}
async onLanguageChanged() {
const data = this.watchdog.editor.getData();
await this.reinitialize(data);
const data = this.watchdog.editor?.getData();
await this.reinitialize(data ?? "");
}
buildTouchBarCommand(data: CommandListenerData<"buildTouchBar">) {
@@ -557,20 +577,24 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
const { TouchBarSegmentedControl, TouchBarGroup, TouchBarButton } = TouchBar;
const { editor } = this.watchdog;
if (!editor) {
return;
}
const commandButton = (icon: string, command: string) => new TouchBarButton({
icon: buildIcon(icon),
click: () => editor.execute(command),
backgroundColor: buildSelectedBackgroundColor(editor.commands.get(command).value as boolean)
backgroundColor: buildSelectedBackgroundColor(editor.commands.get(command)?.value as boolean)
});
let headingSelectedIndex = undefined;
const headingCommand = editor.commands.get("heading");
const paragraphCommand = editor.commands.get("paragraph");
if (paragraphCommand.value) {
if (paragraphCommand?.value) {
headingSelectedIndex = 0;
} else if (headingCommand.value === "heading2") {
} else if (headingCommand?.value === "heading2") {
headingSelectedIndex = 1;
} else if (headingCommand.value === "heading3") {
} else if (headingCommand?.value === "heading3") {
headingSelectedIndex = 2;
}
@@ -581,7 +605,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
{ label: "H2" },
{ label: "H3" }
],
change(selectedIndex, isSelected) {
change(selectedIndex: number, isSelected: boolean) {
switch (selectedIndex) {
case 0:
editor.execute("paragraph")

View File

@@ -100,7 +100,7 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
// we load CKEditor also for read only notes because they contain content styles required for correct rendering of even read only notes
// we could load just ckeditor-content.css but that causes CSS conflicts when both build CSS and this content CSS is loaded at the same time
// (see https://github.com/zadam/trilium/issues/1590 for example of such conflict)
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
await import("@triliumnext/ckeditor5");
this.onLanguageChanged();

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
TRILIUM_PORT=37743

View File

@@ -18,16 +18,15 @@
"@types/electron-squirrel-startup": "1.0.2",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.0",
"electron": "35.2.2",
"@electron/rebuild": "4.0.1",
"@electron-forge/cli": "7.8.0",
"@electron-forge/maker-deb": "7.8.0",
"@electron-forge/maker-dmg": "7.8.0",
"@electron-forge/maker-flatpak": "7.8.0",
"@electron-forge/maker-rpm": "7.8.0",
"@electron-forge/maker-squirrel": "7.8.0",
"@electron-forge/maker-zip": "7.8.0",
"@electron-forge/plugin-auto-unpack-natives": "7.8.0",
"electron": "36.2.0",
"@electron-forge/cli": "7.8.1",
"@electron-forge/maker-deb": "7.8.1",
"@electron-forge/maker-dmg": "7.8.1",
"@electron-forge/maker-flatpak": "7.8.1",
"@electron-forge/maker-rpm": "7.8.1",
"@electron-forge/maker-squirrel": "7.8.1",
"@electron-forge/maker-zip": "7.8.1",
"@electron-forge/plugin-auto-unpack-natives": "7.8.1",
"prebuild-install": "^7.1.1"
},
"config": {
@@ -55,12 +54,10 @@
"cache": true,
"configurations": {
"default": {
"command": "cross-env DEBUG=* tsx scripts/rebuild.mts",
"cwd": "{projectRoot}"
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist"
},
"nixos": {
"command": "electron-rebuild -f -v $(nix-shell -p electron_35 --run \"electron --version\") dist/main.js -m dist",
"cwd": "{projectRoot}"
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist $(nix-shell -p electron_33 --run \"electron --version\")"
}
}
},
@@ -76,7 +73,25 @@
"cwd": "{projectRoot}/dist"
},
"nixos": {
"command": "nix-shell -p electron_35 --run \"electron {projectRoot}/dist/main.js\"",
"command": "nix-shell -p electron_33 --run \"electron {projectRoot}/dist/main.js\"",
"cwd": ".",
"forwardAllArgs": false
}
}
},
"serve-nodir": {
"executor": "nx:run-commands",
"dependsOn": [
"rebuild-deps"
],
"defaultConfiguration": "default",
"configurations": {
"default": {
"command": "electron .",
"cwd": "{projectRoot}/dist"
},
"nixos": {
"command": "nix-shell -p electron_33 --run \"electron {projectRoot}/dist/main.js\"",
"cwd": ".",
"forwardAllArgs": false
}

View File

@@ -1,35 +0,0 @@
/**
* @module
*
* This script is used internally by the `rebuild-deps` target of the `desktop`. Normally we could use
* `electron-rebuild` CLI directly, but it would rebuild the monorepo-level dependencies and breaks
* the server build (and it doesn't expose a CLI option to override this).
*/
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import { rebuild } from "@electron/rebuild"
import { readFileSync } from "fs";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const rootDir = join(scriptDir, "..");
function getElectronVersion() {
const packageJsonPath = join(rootDir, "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
return packageJson.devDependencies.electron;
}
function main() {
const distDir = join(rootDir, "dist");
rebuild({
// We force the project root path to avoid electron-rebuild from rebuilding the monorepo-level dependency and breaking the server.
projectRootPath: distDir,
buildPath: distDir,
force: true,
electronVersion: getElectronVersion(),
});
}
main();

View File

@@ -8,6 +8,14 @@ module.exports = {
output: {
path: outputDir,
},
module: {
rules: [
{
test: /\.css$/i,
type: "asset/source"
}
]
},
target: [ "node" ],
plugins: [
new NxAppWebpackPlugin({

View File

@@ -4,32 +4,34 @@
"private": true,
"description": "Desktop version of Trilium which imports the demo database (presented to new users at start-up) or the user guide and other documentation and saves the modifications for committing.",
"devDependencies": {
"@electron/rebuild": "4.0.1",
"@triliumnext/client": "workspace:*",
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.0",
"electron": "35.2.2",
"electron": "36.2.0",
"fs-extra": "11.3.0"
},
"nx": {
"name": "edit-docs",
"implicitDependencies": [
"server"
],
"targets": {
"rebuild-deps": {
"executor": "nx:run-commands",
"dependsOn": [ "build" ],
"dependsOn": [
"build"
],
"defaultConfiguration": "default",
"cache": true,
"configurations": {
"default": {
"command": "cross-env DEBUG=* tsx scripts/rebuild.mts",
"cwd": "{projectRoot}"
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist"
},
"nixos": {
"command": "electron-rebuild -f -v $(nix-shell -p electron_35 --run \"electron --version\") dist/main.js -m dist",
"cwd": "{projectRoot}"
"command": "cross-env DEBUG=* tsx scripts/electron-rebuild.mts {projectRoot}/dist $(nix-shell -p electron_33 --run \"electron --version\")"
}
}
}
},
"serve": {
"executor": "nx:run-commands",

View File

@@ -1,37 +0,0 @@
/**
* @module
*
* This script is used internally by the `rebuild-deps` target of the `desktop`. Normally we could use
* `electron-rebuild` CLI directly, but it would rebuild the monorepo-level dependencies and breaks
* the server build (and it doesn't expose a CLI option to override this).
*/
// TODO: Deduplicate with apps/desktop/scripts/rebuild.ts.
import { fileURLToPath } from "url";
import { dirname, join } from "path";
import { rebuild } from "@electron/rebuild"
import { readFileSync } from "fs";
const scriptDir = dirname(fileURLToPath(import.meta.url));
const rootDir = join(scriptDir, "..");
function getElectronVersion() {
const packageJsonPath = join(rootDir, "package.json");
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
return packageJson.devDependencies.electron;
}
function main() {
const distDir = join(rootDir, "dist");
rebuild({
// We force the project root path to avoid electron-rebuild from rebuilding the monorepo-level dependency and breaking the server.
projectRootPath: distDir,
buildPath: distDir,
force: true,
electronVersion: getElectronVersion(),
});
}
main();

View File

@@ -8,6 +8,14 @@ module.exports = {
output: {
path: join(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.css$/i,
type: "asset/source"
}
]
},
plugins: [
new NxAppWebpackPlugin({
target: 'node',

View File

@@ -7,7 +7,14 @@
"implicitDependencies": [
"client",
"server"
]
],
"targets": {
"e2e": {
"dependsOn": [
"server:build"
]
}
}
},
"devDependencies": {
"dotenv": "16.5.0"

View File

@@ -55,6 +55,8 @@ test("Displays math popup", async ({ page, context }) => {
await app.goto();
await app.goToNoteInNewTab("Empty text");
const noteContent = app.currentNoteSplit.locator(".note-detail-editable-text-editor");
await expect(noteContent.locator("p")).toBeVisible();
await noteContent.focus();
await noteContent.fill("Hello world");
await noteContent.press("ControlOrMeta+M");

View File

@@ -4,7 +4,8 @@ FROM node:22.15.0-bullseye-slim AS builder
# Install native dependencies since we might be building cross-platform.
WORKDIR /usr/src/app/build
COPY ./dist/package.json ./dist/pnpm-lock.yaml ./docker/pnpm-workspace.yaml /usr/src/app/
RUN pnpm install --frozen-lockfile --prod && pnpm rebuild
# 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
# Install only runtime dependencies

View File

@@ -4,7 +4,8 @@ FROM node:22.15.0-alpine AS builder
# Install native dependencies since we might be building cross-platform.
WORKDIR /usr/src/app
COPY ./dist/package.json ./dist/pnpm-lock.yaml ./docker/pnpm-workspace.yaml /usr/src/app/
RUN pnpm install --frozen-lockfile --prod && pnpm rebuild
# 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
# Install runtime dependencies

View File

@@ -4,7 +4,7 @@
"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.9.1",
"better-sqlite3": "11.10.0",
"jquery.fancytree": "2.38.5",
"jquery-hotkeys": "0.2.2",
"@highlightjs/cdn-assets": "11.11.1"
@@ -14,7 +14,6 @@
"@excalidraw/excalidraw": "0.18.0",
"@types/archiver": "6.0.3",
"@types/better-sqlite3": "7.6.13",
"@types/cheerio": "0.22.35",
"@types/cls-hooked": "4.3.9",
"@types/compression": "1.7.5",
"@types/cookie-parser": "1.4.8",
@@ -31,7 +30,7 @@
"@types/mime-types": "2.1.4",
"@types/multer": "1.4.12",
"@types/safe-compare": "1.1.2",
"@types/sanitize-html": "2.15.0",
"@types/sanitize-html": "2.16.0",
"@types/sax": "1.2.7",
"@types/serve-favicon": "2.5.7",
"@types/serve-static": "1.15.7",
@@ -50,7 +49,7 @@
"jquery": "3.7.1",
"katex": "0.16.22",
"normalize.css": "8.0.1",
"@anthropic-ai/sdk": "0.40.1",
"@anthropic-ai/sdk": "0.50.3",
"@braintree/sanitize-url": "7.1.1",
"@triliumnext/commons": "workspace:*",
"@triliumnext/express-partial-content": "workspace:*",
@@ -70,7 +69,7 @@
"debounce": "2.2.0",
"debug": "4.4.0",
"ejs": "3.1.10",
"electron": "35.2.2",
"electron": "36.2.0",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",
@@ -85,7 +84,7 @@
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.6",
"i18next": "25.0.2",
"i18next": "25.1.2",
"i18next-fs-backend": "2.6.0",
"image-type": "5.2.0",
"ini": "5.0.0",
@@ -99,7 +98,7 @@
"multer": "1.4.5-lts.2",
"normalize-strings": "1.1.1",
"ollama": "0.5.15",
"openai": "4.97.0",
"openai": "4.98.0",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",
"sanitize-filename": "1.6.3",
@@ -117,7 +116,7 @@
"tmp": "0.2.3",
"turndown": "7.2.0",
"unescape": "1.0.1",
"webpack": "5.99.7",
"webpack": "5.99.8",
"ws": "8.18.2",
"xml2js": "0.6.2",
"yauzl": "3.2.0",
@@ -125,10 +124,13 @@
},
"nx": {
"name": "server",
"implicitDependencies": [
"client"
],
"targets": {
"build": {
"dependsOn": [
"^build",
"client:build"
]
},
"serve": {
"executor": "@nx/js:node",
"defaultConfiguration": "development",

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 703 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,29 @@
<p>Bookmarks allows creating <a href="#root/_help_QEAPj01N5f7w">links</a> to
a certain part of a note, such as referencing a particular heading.</p>
<p>Technically, bookmarks are HTML anchors.</p>
<p>This feature was introduced in TriliumNext 0.94.0.</p>
<h2>Interaction</h2>
<ul>
<li>To create a bookmark:
<ul>
<li>Place the cursor at the desired position where to place the bookmark.</li>
<li>Look for the
<img src="Bookmarks_plus.png" width="15" height="16">button in the&nbsp;<a class="reference-link" href="#root/_help_nRhnJkTT8cPs">Formatting toolbar</a>,
and then press the
<img src="1_Bookmarks_plus.png" width="12" height="15">button.</li>
</ul>
</li>
<li>To place a link to a bookmark:
<ul>
<li>Place the cursor at the desired position of the link.</li>
<li>From the <a href="#root/_help_QEAPj01N5f7w">link</a> pane, select the <em>Bookmarks</em> section
and select the desired bookmark.</li>
</ul>
</li>
</ul>
<h2>Limitations</h2>
<ul>
<li>Currently it's not possible to create a link to a bookmark from a different
note. This functionality will be added after the internal links feature
is enhanced to support bookmarks.</li>
</ul>

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

View File

@@ -2,59 +2,76 @@
<img src="4_Insert buttons_image.png" width="34" height="16">button in the&nbsp;<a class="reference-link" href="#root/_help_nRhnJkTT8cPs">Formatting toolbar</a>&nbsp;to
reveal special inserable items and blocks such as symbols, Math expressions
and separators.</p>
<h2>Symbols</h2>
<figure class="image image-style-align-right">
<img style="aspect-ratio:346/322;" src="1_Insert buttons_image.png" width="346"
height="322">
<h2>Bookmarks</h2>
<p>See the dedicated&nbsp;<a class="reference-link" href="#root/_help_oSuaNgyyKnhu">Bookmarks</a>&nbsp;section.</p>
<h2>Emoji</h2>
<figure class="image image-style-align-right image_resized" style="width:42.4%;">
<img style="aspect-ratio:366/410;" src="Insert buttons_plus.png" width="366"
height="410">
</figure>
<p>Pressing the
<img src="7_Insert buttons_image.png" width="18" height="15">button will reveal a popup window displaying a list of characters that
are generally more difficult to insert directly from the keyboard, such
as a subset of emojis, quotation characters, etc.</p>
<p>Interaction:</p>
<ul>
<li>Click on a character to insert it at the current cursor position.</li>
<li>The window can be dragged around by the top bar where the title is, to
avoid it getting in the way of the text.</li>
<li>Click on the <em>Category</em> selector to filter the characters.</li>
</ul>
<h2>Math equations</h2>
<p>See the dedicated&nbsp;<a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>&nbsp;page.</p>
<h2>Mermaid diagram</h2>
<p>Press the
<img src="2_Insert buttons_image.png" width="16" height="17">button to create an inline Mermaid diagram.</p>
<p>This feature is quite similar to the&nbsp;<a class="reference-link" href="#root/_help_s1aBHPd79XYj">Mermaid Diagrams</a>&nbsp;note
types and is meant as an alternative to it for simple diagrams. For more
complex diagrams, use the&nbsp;<a class="reference-link" href="#root/_help_nBAXQFj20hS1">Include Note</a>&nbsp;feature
for a dedicated Mermaid note.</p>
<figure class="image">
<img style="aspect-ratio:1174/358;" src="6_Insert buttons_image.png" width="1174"
height="358">
</figure>
<h2>Horizontal ruler</h2>
<p>This feature will display a horizontal line, generally useful to separate
different sections of the text. To do so, press the
<img src="5_Insert buttons_image.png"
width="18" height="16">button in the&nbsp;<a class="reference-link" href="#root/_help_nRhnJkTT8cPs">Formatting toolbar</a>.</p>
<p>This feature allows inserting Unicode emoji characters. Simply select
a category and a desired emoji to insert it.</p>
<p>Emojis can also be searched by their English name and the skin tone can
be selected via a combo box to the right.</p>
<p>There is also the possibility of inserting emojis directly by typing <code>:</code> followed
by a name of an emoji, triggering the display of a list of emojis. Simply
use the arrow keys to select one and press <kbd>Enter</kbd> to insert it.</p>
<img
src="3_Insert buttons_image.png" width="502" height="95">
<p>Alternatively, it's possible to insert a horizontal ruler by typing <code>---</code>.</p>
<h2>Page break</h2>
src="1_Insert buttons_plus.png" width="272" height="187">
<h2>Symbols</h2>
<figure class="image image-style-align-right">
<img style="aspect-ratio:371/79;" src="8_Insert buttons_image.png" width="371"
height="79">
<img style="aspect-ratio:346/322;" src="1_Insert buttons_image.png" width="346"
height="322">
</figure>
<p>Page breaks provide a way to force the next paragraph or block (table,
image, etc.) to be displayed onto the next page when printing (either to
a real printer to <a href="#root/_help_NRnIZmSMc5sj">when exporting to PDF</a>).</p>
<p>Page breaks are marked in the editor with the words <em>Page break</em>,
but they will not actually be shown when printed.</p>
<p>Pressing the
<img src="7_Insert buttons_image.png" width="18" height="15">button will reveal a popup window displaying a list of characters that
are generally more difficult to insert directly from the keyboard, such
as a subset of emojis, quotation characters, etc.</p>
<p>Interaction:</p>
<ul>
<li>To insert a page break, press the
<img src="Insert buttons_image.png" width="20"
height="19">in the formatting toolbar.</li>
<li>To insert many page breaks at once, insert a page break first, click on
it and press <kbd>Ctrl</kbd>+<kbd>C</kbd>. Then use <kbd>Ctrl</kbd>+<kbd>V</kbd>,
to paste as many times as needed.</li>
</ul>
<li>Click on a character to insert it at the current cursor position.</li>
<li>The window can be dragged around by the top bar where the title is, to
avoid it getting in the way of the text.</li>
<li>Click on the <em>Category</em> selector to filter the characters.</li>
</ul>
<h2>Math equations</h2>
<p>See the dedicated&nbsp;<a class="reference-link" href="#root/_help_YfYAtQBcfo5V">Math Equations</a>&nbsp;page.</p>
<h2>Mermaid diagram</h2>
<p>Press the
<img src="2_Insert buttons_image.png" width="16" height="17">button to create an inline Mermaid diagram.</p>
<p>This feature is quite similar to the&nbsp;<a class="reference-link" href="#root/_help_s1aBHPd79XYj">Mermaid Diagrams</a>&nbsp;note
types and is meant as an alternative to it for simple diagrams. For more
complex diagrams, use the&nbsp;<a class="reference-link" href="#root/_help_nBAXQFj20hS1">Include Note</a>&nbsp;feature
for a dedicated Mermaid note.</p>
<figure class="image">
<img style="aspect-ratio:1174/358;" src="6_Insert buttons_image.png" width="1174"
height="358">
</figure>
<h2>Horizontal ruler</h2>
<p>This feature will display a horizontal line, generally useful to separate
different sections of the text. To do so, press the
<img src="5_Insert buttons_image.png"
width="18" height="16">button in the&nbsp;<a class="reference-link" href="#root/_help_nRhnJkTT8cPs">Formatting toolbar</a>.</p>
<img
src="3_Insert buttons_image.png" width="502" height="95">
<p>Alternatively, it's possible to insert a horizontal ruler by typing <code>---</code>.</p>
<h2>Page break</h2>
<figure class="image image-style-align-right">
<img style="aspect-ratio:371/79;" src="8_Insert buttons_image.png" width="371"
height="79">
</figure>
<p>Page breaks provide a way to force the next paragraph or block (table,
image, etc.) to be displayed onto the next page when printing (either to
a real printer to <a href="#root/_help_NRnIZmSMc5sj">when exporting to PDF</a>).</p>
<p>Page breaks are marked in the editor with the words <em>Page break</em>,
but they will not actually be shown when printed.</p>
<ul>
<li>To insert a page break, press the
<img src="Insert buttons_image.png" width="20"
height="19">in the formatting toolbar.</li>
<li>To insert many page breaks at once, insert a page break first, click on
it and press <kbd>Ctrl</kbd>+<kbd>C</kbd>. Then use <kbd>Ctrl</kbd>+<kbd>V</kbd>,
to paste as many times as needed.</li>
</ul>

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -59,6 +59,8 @@
with the text inside of it.</li>
</ul>
</li>
<li>For <a href="#root/_help_CohkqWQC1iBv">emojis</a>, type <code>:</code> followed
by an emoji name to trigger an auto-completion.</li>
</ul>
<p>If auto-formatting is not desirable, press <kbd>Ctrl</kbd> + <kbd>Z</kbd> to
revert the text to its original form.</p>

View File

@@ -7,7 +7,8 @@
<img src="1_Math Equations_image.png" width="20" height="15">button from the&nbsp;<a class="reference-link" href="#root/_help_nRhnJkTT8cPs">Formatting toolbar</a>&nbsp;(generally
found under the&nbsp;<a class="reference-link" href="#root/_help_CohkqWQC1iBv">Insert buttons</a>).</p>
<p>If inserting equations frequently, using the <kbd>Ctrl</kbd>+<kbd>M</kbd> keyboard
shortcut can be more comfortable.</p>
shortcut can be more comfortable. Alternatively, type <code>$$</code> or <code>\[</code> to
trigger the popup directly.</p>
<p>There is currently no quick way to insert an equation, such as surrounding
it with <code>$</code> or pressing <kbd>Ctrl</kbd>+<kbd>M</kbd> on an already
typed-out equation.</p>

View File

@@ -5,6 +5,7 @@ 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) {
@@ -20,6 +21,8 @@ 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) {

View File

@@ -1,11 +1,12 @@
import { describe, expect, it } from "vitest";
import { getStylesDirectory, readThemesFromFileSystem } from "./code_block_theme.js";
import { readThemesFromFileSystem } from "./code_block_theme.js";
import themeNames from "./code_block_theme_names.json" with { type: "json" };
import path = require("path");
describe("Code block theme", () => {
it("all themes are mapped", () => {
const themes = readThemesFromFileSystem(getStylesDirectory());
const themes = readThemesFromFileSystem(path.join(__dirname, "../../node_modules/@highlightjs/cdn-assets/styles"));
const mappedThemeNames = new Set(Object.values(themeNames));
const unmappedThemeNames = new Set<string>();

View File

@@ -30,7 +30,7 @@ interface ColorTheme {
* @returns the supported themes, grouped.
*/
export function listSyntaxHighlightingThemes() {
const path = join(getResourceDir(), getStylesDirectory());
const path = getStylesDirectory();
const systemThemes = readThemesFromFileSystem(path);
return {
@@ -46,11 +46,11 @@ export function listSyntaxHighlightingThemes() {
export function getStylesDirectory() {
if (isElectron && !isDev) {
return "styles";
return join(getResourceDir(), "styles");
} else if (!isDev) {
return "node_modules/@highlightjs/cdn-assets/styles";
return join(getResourceDir(), "node_modules/@highlightjs/cdn-assets/styles");
} else {
return join(__dirname, "../../node_modules/@highlightjs/cdn-assets/styles");
return join(__dirname, "../node_modules/@highlightjs/cdn-assets/styles");
}
}

View File

@@ -21,6 +21,7 @@ import type AttributeMeta from "../meta/attribute_meta.js";
import type BBranch from "../../becca/entities/bbranch.js";
import type { Response } from "express";
import type { NoteMetaFile } from "../meta/note_meta.js";
import cssContent from "@triliumnext/ckeditor5/content.css";
type RewriteLinksFn = (content: string, noteMeta: NoteMeta) => string;
@@ -511,8 +512,6 @@ ${markdownContent}`;
return;
}
const cssContent = fs.readFileSync(`${getResourceDir()}/public/libraries/ckeditor/ckeditor-content.css`);
archive.append(cssContent, { name: cssMeta.dataFileName });
}

View File

@@ -22,3 +22,8 @@ declare module "is-animated" {
function isAnimated(buffer: Buffer): boolean;
export default isAnimated;
}
declare module "@triliumnext/ckeditor5/content.css" {
const content: string;
export default content;
}

View File

@@ -33,6 +33,9 @@
"src/**/*.spec.jsx"
],
"references": [
{
"path": "../../packages/ckeditor5/tsconfig.lib.json"
},
{
"path": "../../packages/turndown-plugin-gfm/tsconfig.lib.json"
},

View File

@@ -3,6 +3,9 @@
"files": [],
"include": [],
"references": [
{
"path": "../../packages/ckeditor5"
},
{
"path": "../../packages/turndown-plugin-gfm"
},

View File

@@ -1,6 +1,6 @@
const { NxAppWebpackPlugin } = require('@nx/webpack/app-plugin');
const CopyPlugin = require('copy-webpack-plugin');
const { join, default: path } = require('path');
const { join } = require('path');
const outputDir = join(__dirname, 'dist');
@@ -48,6 +48,14 @@ module.exports = {
output: {
path: outputDir
},
module: {
rules: [
{
test: /\.css$/i,
type: "asset/source"
}
]
},
plugins: [
new NxAppWebpackPlugin({
target: 'node',