Merge branch 'master' into next61

# Conflicts:
#	package-lock.json
#	src/public/app/services/note_content_renderer.js
#	src/public/app/widgets/note_tree.js
#	src/routes/routes.js
#	src/services/consistency_checks.js
#	src/services/notes.js
#	src/services/task_context.js
This commit is contained in:
zadam 2023-07-10 18:20:36 +02:00
commit b7f0fd2db3
41 changed files with 253 additions and 192 deletions

View file

@ -16,7 +16,7 @@ noBackup=false
# host=0.0.0.0
# port setting is relevant only for web deployments, desktop builds run on a fixed port (changeable with TRILIUM_PORT environment variable)
port=8080
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).
# true for TLS/SSL/HTTPS (secure), false for HTTP (insecure).
https=false
# path to certificate (run "bash bin/generate-cert.sh" to generate self-signed certificate). Relevant only if https=true
certPath=

View file

@ -4,7 +4,7 @@ const fs = require("fs");
const dataDir = require("./src/services/data_dir");
const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, 'utf-8'));
if (config.https) {
if (config.Network.https) {
// built-in TLS (terminated by trilium) is not supported yet, PRs are welcome
// for reverse proxy terminated TLS this will works since config.https will be false
process.exit(0);

View file

@ -667,7 +667,7 @@ class BNote extends AbstractBeccaEntity {
return this.ownedAttributes.filter(attr => attr.name === name);
}
else {
return this.ownedAttributes.slice();
return this.ownedAttributes;
}
}

View file

@ -367,7 +367,7 @@ async function findSimilarNotes(noteId) {
* We want to improve the standing of notes which have been created in similar time to each other since
* there's a good chance they are related.
*
* But there's an exception - if they were created really close to each other (withing few seconds) then
* But there's an exception - if they were created really close to each other (within few seconds) then
* they are probably part of the import and not created by hand - these OTOH should not benefit.
*/
const {utcDateCreated} = candidateNote;

View file

@ -231,7 +231,7 @@ paths:
schema:
$ref: '#/components/schemas/EntityId'
get:
description: Returns note content idenfied by its ID
description: Returns note content identified by its ID
operationId: getNoteContent
responses:
'200':
@ -241,7 +241,7 @@ paths:
schema:
type: string
put:
description: Updates note content idenfied by its ID
description: Updates note content identified by its ID
operationId: putNoteContentById
requestBody:
description: html content of note

View file

@ -41,8 +41,8 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) {
async function initLabelValueAutocomplete({ $el, open, nameCallback }) {
if ($el.hasClass("aa-input")) {
// we reinit everytime because autocomplete seems to have a bug where it retains state from last
// open even though the value was resetted
// we reinit every time because autocomplete seems to have a bug where it retains state from last
// open even though the value was reset
$el.autocomplete('destroy');
}

View file

@ -133,7 +133,7 @@ function initNoteAutocomplete($el, options) {
showRecentNotes($el);
// this will cause the click not give focus to the "show recent notes" button
// this is important because otherwise input will lose focus immediatelly and not show the results
// this is important because otherwise input will lose focus immediately and not show the results
return false;
});

View file

@ -99,7 +99,7 @@ function parseSelectedHtml(selectedHtml) {
if (dom.length > 0 && dom[0].tagName && dom[0].tagName.match(/h[1-6]/i)) {
const title = $(dom[0]).text();
// remove the title from content (only first occurence)
// remove the title from content (only first occurrence)
const content = selectedHtml.replace(dom[0].outerHTML, "");
return [title, content];

View file

@ -161,7 +161,7 @@ class NoteListRenderer {
constructor($parent, parentNote, noteIds, showNotePath = false) {
this.$noteList = $(TPL);
// note list must be added to the DOM immediatelly, otherwise some functionality scripting (canvas) won't work
// note list must be added to the DOM immediately, otherwise some functionality scripting (canvas) won't work
$parent.empty();
this.parentNote = parentNote;

View file

@ -21,7 +21,7 @@ async function getHeaders(headers) {
}
if (utils.isElectron()) {
// passing it explicitely here because of the electron HTTP bypass
// passing it explicitly here because of the electron HTTP bypass
allHeaders.cookie = document.cookie;
}

View file

@ -1,7 +1,7 @@
/**
* Fetch note with given ID from backend
*
* @param noteId of the given note to be fetched. If falsy, fetches current note.
* @param noteId of the given note to be fetched. If false, fetches current note.
*/
async function fetchNote(noteId = null) {
if (!noteId) {

View file

@ -26,7 +26,7 @@ export default class AbstractBulkAction {
}
}
// to be overriden
// to be overridden
doRender() {}
async saveAction(data) {

View file

@ -50,7 +50,7 @@ export default class RightDropdownButtonWidget extends BasicWidget {
this.$widget.find(".dropdown-menu").append(this.$dropdownContent);
}
// to be overriden
// to be overridden
async dropdownShow() {}
hideDropdown() {

View file

@ -25,7 +25,7 @@ const TPL = `
</div>
<div class="checkbox">
<label title="Normal (soft) deletion only marks the notes as deleted and they can be undeleted (in recent changes dialog) within a period of time. Checking this option will erase the notes immediatelly and it won't be possible to undelete the notes.">
<label title="Normal (soft) deletion only marks the notes as deleted and they can be undeleted (in recent changes dialog) within a period of time. Checking this option will erase the notes immediately and it won't be possible to undelete the notes.">
<input class="erase-notes" value="1" type="checkbox">
erase notes permanently (can't be undone), including all clones. This will force application reload.

View file

@ -10,20 +10,20 @@ import RightPanelWidget from "./right_panel_widget.js";
import options from "../services/options.js";
import OnClickButtonWidget from "./buttons/onclick_button.js";
const TPL = `<div class="highlists-list-widget">
const TPL = `<div class="highlights-list-widget">
<style>
.highlists-list-widget {
.highlights-list-widget {
padding: 10px;
contain: none;
overflow: auto;
position: relative;
}
.highlists-list > ol {
.highlights-list > ol {
padding-left: 20px;
}
.highlists-list li {
.highlights-list li {
cursor: pointer;
margin-bottom: 3px;
text-align: justify;
@ -32,18 +32,18 @@ const TPL = `<div class="highlists-list-widget">
hyphens: auto;
}
.highlists-list li:hover {
.highlights-list li:hover {
font-weight: bold;
}
.close-highlists-list {
.close-highlights-list {
position: absolute;
top: 2px;
right: 0px;
}
</style>
<span class="highlists-list"></span>
<span class="highlights-list"></span>
</div>`;
export default class HighlightsListWidget extends RightPanelWidget {
@ -55,61 +55,61 @@ export default class HighlightsListWidget extends RightPanelWidget {
}
get widgetTitle() {
return "Highlighted Text";
return "Highlights List";
}
isEnabled() {
return super.isEnabled()
&& this.note.type === 'text'
&& !this.noteContext.viewScope.highlightedTextTemporarilyHidden
&& !this.noteContext.viewScope.highlightsListTemporarilyHidden
&& this.noteContext.viewScope.viewMode === 'default';
}
async doRenderBody() {
this.$body.empty().append($(TPL));
this.$highlightsList = this.$body.find('.highlists-list');
this.$body.find('.highlists-list-widget').append(this.closeHltButton.render());
this.$highlightsList = this.$body.find('.highlights-list');
this.$body.find('.highlights-list-widget').append(this.closeHltButton.render());
}
async refreshWithNote(note) {
/* The reason for adding highlightedTextPreviousVisible is to record whether the previous state
of the highlightedText is hidden or displayed, and then let it be displayed/hidden at the initial time.
/* The reason for adding highlightsListPreviousVisible is to record whether the previous state
of the highlightsList is hidden or displayed, and then let it be displayed/hidden at the initial time.
If there is no such value, when the right panel needs to display toc but not highlighttext,
every time the note content is changed, highlighttext Widget will appear and then close immediately,
because getHlt function will consume time */
if (this.noteContext.viewScope.highlightedTextPreviousVisible) {
if (this.noteContext.viewScope.highlightsListPreviousVisible) {
this.toggleInt(true);
} else {
this.toggleInt(false);
}
const optionsHlt = JSON.parse(options.get('highlightedText'));
const optionsHighlightsList = JSON.parse(options.get('highlightsList'));
if (note.isLabelTruthy('hideHighlightWidget') || !optionsHlt) {
if (note.isLabelTruthy('hideHighlightWidget') || !optionsHighlightsList) {
this.toggleInt(false);
this.triggerCommand("reEvaluateRightPaneVisibility");
return;
}
let $highlightsList = "", hltLiCount = -1;
let $highlightsList = "", hlLiCount = -1;
// Check for type text unconditionally in case alwaysShowWidget is set
if (this.note.type === 'text') {
const {content} = await note.getNoteComplement();
({$highlightsList, hltLiCount} = this.getHighlightList(content, optionsHlt));
({$highlightsList, hlLiCount} = this.getHighlightList(content, optionsHighlightsList));
}
this.$highlightsList.empty().append($highlightsList);
if (hltLiCount > 0) {
if (hlLiCount > 0) {
this.toggleInt(true);
this.noteContext.viewScope.highlightedTextPreviousVisible = true;
this.noteContext.viewScope.highlightsListPreviousVisible = true;
} else {
this.toggleInt(false);
this.noteContext.viewScope.highlightedTextPreviousVisible = false;
this.noteContext.viewScope.highlightsListPreviousVisible = false;
}
this.triggerCommand("reEvaluateRightPaneVisibility");
}
getHighlightList(content, optionsHlt) {
getHighlightList(content, optionsHighlightsList) {
// matches a span containing background-color
const regex1 = /<span[^>]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi;
// matches a span containing color
@ -120,27 +120,27 @@ export default class HighlightsListWidget extends RightPanelWidget {
const regex4 = /<strong>[\s\S]*?<\/strong>/gi;
// match underline
const regex5 = /<u>[\s\S]*?<\/u>/g;
// Possible values in optionsHlt '["bold","italic","underline","color","bgColor"]'
// Possible values in optionsHighlightsList '["bold","italic","underline","color","bgColor"]'
// element priority span>i>strong>u
let findSubStr = "", combinedRegexStr = "";
if (optionsHlt.includes("bgColor")) {
findSubStr += `,span[style*="background-color"]`;
if (optionsHighlightsList.includes("bgColor")) {
findSubStr += `,span[style*="background-color"]:not(section.include-note span[style*="background-color"])`;
combinedRegexStr += `|${regex1.source}`;
}
if (optionsHlt.includes("color")) {
findSubStr += `,span[style*="color"]`;
if (optionsHighlightsList.includes("color")) {
findSubStr += `,span[style*="color"]:not(section.include-note span[style*="color"])`;
combinedRegexStr += `|${regex2.source}`;
}
if (optionsHlt.includes("italic")) {
findSubStr += `,i`;
if (optionsHighlightsList.includes("italic")) {
findSubStr += `,i:not(section.include-note i)`;
combinedRegexStr += `|${regex3.source}`;
}
if (optionsHlt.indexOf("bold")) {
findSubStr += `,strong`;
if (optionsHighlightsList.includes("bold")) {
findSubStr += `,strong:not(section.include-note strong)`;
combinedRegexStr += `|${regex4.source}`;
}
if (optionsHlt.includes("underline")) {
findSubStr += `,u`;
if (optionsHighlightsList.includes("underline")) {
findSubStr += `,u:not(section.include-note u)`;
combinedRegexStr += `|${regex5.source}`;
}
@ -148,7 +148,7 @@ export default class HighlightsListWidget extends RightPanelWidget {
combinedRegexStr = `(` + combinedRegexStr.substring(1) + `)`;
const combinedRegex = new RegExp(combinedRegexStr, 'gi');
const $highlightsList = $("<ol>");
let prevEndIndex = -1, hltLiCount = 0;
let prevEndIndex = -1, hlLiCount = 0;
for (let match = null, hltIndex = 0; ((match = combinedRegex.exec(content)) !== null); hltIndex++) {
const subHtml = match[0];
const startIndex = match.index;
@ -158,16 +158,18 @@ export default class HighlightsListWidget extends RightPanelWidget {
$highlightsList.children().last().append(subHtml);
} else {
// TODO: can't be done with $(subHtml).text()?
const hasText = [...subHtml.matchAll(/(?<=^|>)[^><]+?(?=<|$)/g)].map(matchTmp => matchTmp[0]).join('').trim();
//Cant remember why regular expressions are used here, but modified to $(subHtml).text() works as expected
//const hasText = [...subHtml.matchAll(/(?<=^|>)[^><]+?(?=<|$)/g)].map(matchTmp => matchTmp[0]).join('').trim();
const hasText = $(subHtml).text().trim();
if (hasText) {
$highlightsList.append(
$('<li>')
.html(subHtml)
.on("click", () => this.jumpToHighlightedText(findSubStr, hltIndex))
.on("click", () => this.jumpToHighlightsList(findSubStr, hltIndex))
);
hltLiCount++;
hlLiCount++;
} else {
// hide li if its text content is empty
continue;
@ -177,11 +179,11 @@ export default class HighlightsListWidget extends RightPanelWidget {
}
return {
$highlightsList,
hltLiCount
hlLiCount
};
}
async jumpToHighlightedText(findSubStr, itemIndex) {
async jumpToHighlightsList(findSubStr, itemIndex) {
const isReadOnly = await this.noteContext.isReadOnly();
let targetElement;
if (isReadOnly) {
@ -224,7 +226,7 @@ export default class HighlightsListWidget extends RightPanelWidget {
}
async closeHltCommand() {
this.noteContext.viewScope.highlightedTextTemporarilyHidden = true;
this.noteContext.viewScope.highlightsListTemporarilyHidden = true;
await this.refresh();
this.triggerCommand('reEvaluateRightPaneVisibility');
}
@ -245,13 +247,13 @@ class CloseHltButton extends OnClickButtonWidget {
super();
this.icon("bx-x")
.title("Close HighlightedTextWidget")
.title("Close HighlightsListWidget")
.titlePlacement("bottom")
.onClick((widget, e) => {
e.stopPropagation();
widget.triggerCommand("closeHlt");
})
.class("icon-action close-highlists-list");
.class("icon-action close-highlights-list");
}
}

View file

@ -9584,7 +9584,7 @@ const icons = [
"term": [
"honor",
"honour",
"acheivement"
"achievement"
]
},
{
@ -9595,7 +9595,7 @@ const icons = [
"term": [
"honor",
"honour",
"acheivement"
"achievement"
]
},
{

View file

@ -166,7 +166,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
generateColorFromString(str) {
if (this.themeStyle === "dark") {
str = `0${str}`; // magic lightening modifier
str = `0${str}`; // magic lightning modifier
}
let hash = 0;

View file

@ -1116,7 +1116,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
const note = froca.getNoteFromCache(ecAttr.noteId);
if (note && note.getChildNoteIds().includes(ecAttr.value)) {
// there's a new /deleted imageLink betwen note and its image child - which can show/hide
// there's a new /deleted imageLink between note and its image child - which can show/hide
// the image (if there is an imageLink relation between parent and child,
// then it is assumed to be "contained" in the note and thus does not have to be displayed in the tree)
noteIdsToReload.add(ecAttr.noteId);

View file

@ -39,7 +39,7 @@ export default class AbstractSearchOption extends Component {
}
}
// to be overriden
// to be overridden
doRender() {}
async deleteOption() {

View file

@ -2,6 +2,7 @@ import AbstractSearchOption from "./abstract_search_option.js";
import SpacedUpdate from "../../services/spaced_update.js";
import server from "../../services/server.js";
import shortcutService from "../../services/shortcuts.js";
import appContext from "../../components/app_context.js";
const TPL = `
<tr>
@ -56,6 +57,7 @@ export default class SearchString extends AbstractSearchOption {
this.spacedUpdate = new SpacedUpdate(async () => {
const searchString = this.$searchString.val();
appContext.lastSearchString = searchString;
await this.setAttribute('label', 'searchString', searchString);
@ -84,6 +86,7 @@ export default class SearchString extends AbstractSearchOption {
}
focusOnSearchDefinitionEvent() {
this.$searchString.focus();
this.$searchString.val(appContext.lastSearchString).focus().select();
this.spacedUpdate.scheduleUpdate();
}
}

View file

@ -187,7 +187,7 @@ export default class TocWidget extends RightPanelWidget {
if (isReadOnly) {
const $container = await this.noteContext.getContentElement();
const headingElement = $container.find(":header")[headingIndex];
const headingElement = $container.find(":header:not(section.include-note :header)")[headingIndex];
if (headingElement != null) {
headingElement.scrollIntoView({ behavior: "smooth" });
@ -206,7 +206,7 @@ export default class TocWidget extends RightPanelWidget {
// navigate (note that the TOC rendering and other TOC
// entries' navigation could be wrong too)
if (headingNode != null) {
$(textEditor.editing.view.domRoots.values().next().value).find(':header')[headingIndex].scrollIntoView({
$(textEditor.editing.view.domRoots.values().next().value).find(':header:not(section.include-note :header)')[headingIndex].scrollIntoView({
behavior: 'smooth'
});
}

View file

@ -77,7 +77,7 @@ const TPL = `
*
* Discussion of storing svg in the note:
* - Pro: we will combat bit-rot. Showing the SVG will be very fast and easy, since it is already there.
* - Con: The note will get bigger (~40-50%?), we will generate more bandwith. However, using trilium
* - Con: The note will get bigger (~40-50%?), we will generate more bandwidth. However, using trilium
* desktop instance mitigates that issue.
*
* Roadmap:

View file

@ -7,7 +7,7 @@ import MaxContentWidthOptions from "./options/appearance/max_content_width.js";
import KeyboardShortcutsOptions from "./options/shortcuts.js";
import HeadingStyleOptions from "./options/text_notes/heading_style.js";
import TableOfContentsOptions from "./options/text_notes/table_of_contents.js";
import HighlightedTextOptions from "./options/text_notes/highlighted_text.js";
import HighlightsListOptions from "./options/text_notes/highlights_list.js";
import TextAutoReadOnlySizeOptions from "./options/text_notes/text_auto_read_only_size.js";
import VimKeyBindingsOptions from "./options/code_notes/vim_key_bindings.js";
import WrapLinesOptions from "./options/code_notes/wrap_lines.js";
@ -63,7 +63,7 @@ const CONTENT_WIDGETS = {
_optionsTextNotes: [
HeadingStyleOptions,
TableOfContentsOptions,
HighlightedTextOptions,
HighlightsListOptions,
TextAutoReadOnlySizeOptions
],
_optionsCodeNotes: [

View file

@ -96,7 +96,7 @@ export default class EtapiOptions extends OptionsWidget {
.append($("<td>").append(
$('<span class="bx bx-pen token-table-button" title="Rename this token"></span>')
.on("click", () => this.renameToken(token.etapiTokenId, token.name)),
$('<span class="bx bx-trash token-table-button" title="Delete / deactive this token"></span>')
$('<span class="bx bx-trash token-table-button" title="Delete / deactivate this token"></span>')
.on("click", () => this.deleteToken(token.etapiTokenId, token.name))
))
);

View file

@ -1,40 +0,0 @@
import OptionsWidget from "../options_widget.js";
const TPL = `
<div class="options-section">
<h4>Highlighted Text</h4>
<p>You can customize the highlighted text displayed in the right panel:</p>
</div>
<label><input type="checkbox" class="highlighted-text-check" value="bold"> Bold font &nbsp;</label>
<label><input type="checkbox" class="highlighted-text-check" value="italic"> Italic font &nbsp;</label>
<label><input type="checkbox" class="highlighted-text-check" value="underline"> Underlined font &nbsp;</label>
<label><input type="checkbox" class="highlighted-text-check" value="color"> Font with color &nbsp;</label>
<label><input type="checkbox" class="highlighted-text-check" value="bgColor"> Font with background color &nbsp;</label>
</div>
</div>`;
export default class HighlightedTextOptions extends OptionsWidget {
doRender() {
this.$widget = $(TPL);
this.$hlt = this.$widget.find("input.highlighted-text-check");
this.$hlt.on('change', () => {
const hltVals = this.$widget.find('input.highlighted-text-check[type="checkbox"]:checked').map(function () {
return this.value;
}).get();
this.updateOption('highlightedText', JSON.stringify(hltVals));
});
}
async optionsLoaded(options) {
const hltVals = JSON.parse(options.highlightedText);
this.$widget.find('input.highlighted-text-check[type="checkbox"]').each(function () {
if ($.inArray($(this).val(), hltVals) !== -1) {
$(this).prop("checked", true);
} else {
$(this).prop("checked", false);
}
});
}
}

View file

@ -0,0 +1,40 @@
import OptionsWidget from "../options_widget.js";
const TPL = `
<div class="options-section">
<h4>Highlights List</h4>
<p>You can customize the highlights list displayed in the right panel:</p>
</div>
<label><input type="checkbox" class="highlights-list-check" value="bold"> Bold font &nbsp;</label>
<label><input type="checkbox" class="highlights-list-check" value="italic"> Italic font &nbsp;</label>
<label><input type="checkbox" class="highlights-list-check" value="underline"> Underlined font &nbsp;</label>
<label><input type="checkbox" class="highlights-list-check" value="color"> Font with color &nbsp;</label>
<label><input type="checkbox" class="highlights-list-check" value="bgColor"> Font with background color &nbsp;</label>
</div>
</div>`;
export default class HighlightsListOptions extends OptionsWidget {
doRender() {
this.$widget = $(TPL);
this.$hlt = this.$widget.find("input.highlights-list-check");
this.$hlt.on('change', () => {
const hltVals = this.$widget.find('input.highlights-list-check[type="checkbox"]:checked').map(function () {
return this.value;
}).get();
this.updateOption('highlightsList', JSON.stringify(hltVals));
});
}
async optionsLoaded(options) {
const hltVals = JSON.parse(options.highlightsList);
this.$widget.find('input.highlights-list-check[type="checkbox"]').each(function () {
if ($.inArray($(this).val(), hltVals) !== -1) {
$(this).prop("checked", true);
} else {
$(this).prop("checked", false);
}
});
}
}

View file

@ -412,7 +412,7 @@ export default class RelationMapTypeWidget extends TypeWidget {
}
});
// if there's no event, then this has been triggered programatically
// if there's no event, then this has been triggered programmatically
if (!originalEvent) {
return;
}

View file

@ -1,6 +1,7 @@
"use strict";
const attributeService = require("../../services/attributes");
const cloneService = require("../../services/cloning");
const noteService = require('../../services/notes');
const dateNoteService = require('../../services/date_notes');
const dateUtils = require('../../services/date_utils');
@ -13,46 +14,25 @@ const path = require('path');
const BAttribute = require('../../becca/entities/battribute');
const htmlSanitizer = require('../../services/html_sanitizer');
const {formatAttrForSearch} = require("../../services/attribute_formatter");
function findClippingNote(clipperInboxNote, pageUrl) {
const notes = clipperInboxNote.searchNotesInSubtree(
formatAttrForSearch({
type: 'label',
name: "pageUrl",
value: pageUrl
}, true)
);
for (const note of notes) {
if (note.getOwnedLabelValue('clipType') === 'clippings') {
return note;
}
}
return null;
}
function getClipperInboxNote() {
let clipperInbox = attributeService.getNoteWithLabel('clipperInbox');
if (!clipperInbox) {
clipperInbox = dateNoteService.getDayNote(dateUtils.localNowDate());
}
return clipperInbox;
}
const jsdom = require("jsdom");
const { JSDOM } = jsdom;
function addClipping(req) {
// if a note under the clipperInbox as the same 'pageUrl' attribute,
// add the content to that note and clone it under today's inbox
// otherwise just create a new note under today's inbox
let {title, content, pageUrl, images} = req.body;
const clipType = 'clippings';
const clipperInbox = getClipperInboxNote();
const dailyNote = dateNoteService.getDayNote(dateUtils.localNowDate());
pageUrl = htmlSanitizer.sanitizeUrl(pageUrl);
let clippingNote = findClippingNote(clipperInbox, pageUrl);
let clippingNote = findClippingNote(clipperInbox, pageUrl, clipType);
if (!clippingNote) {
clippingNote = noteService.createNewNote({
parentNoteId: clipperInbox.noteId,
parentNoteId: dailyNote.noteId,
title: title,
content: '',
type: 'text'
@ -67,13 +47,45 @@ function addClipping(req) {
const existingContent = clippingNote.getContent();
clippingNote.setContent(`${existingContent}${existingContent.trim() ? "<br/>" : ""}${rewrittenContent}`);
clippingNote.setContent(`${existingContent}${existingContent.trim() ? "<br>" : ""}${rewrittenContent}`);
if (clippingNote.parentNoteId !== dailyNote.noteId) {
cloneService.cloneNoteToParentNote(clippingNote.noteId, dailyNote.noteId);
}
return {
noteId: clippingNote.noteId
};
}
function findClippingNote(clipperInboxNote, pageUrl, clipType) {
if (!pageUrl) {
return null;
}
const notes = clipperInboxNote.searchNotesInSubtree(
formatAttrForSearch({
type: 'label',
name: "pageUrl",
value: pageUrl
}, true)
);
return clipType
? notes.find(note => note.getOwnedLabelValue('clipType') === clipType)
: notes[0];
}
function getClipperInboxNote() {
let clipperInbox = attributeService.getNoteWithLabel('clipperInbox');
if (!clipperInbox) {
clipperInbox = dateNoteService.getRootCalendarNote();
}
return clipperInbox;
}
function createNote(req) {
let {title, content, pageUrl, images, clipType, labels} = req.body;
@ -81,26 +93,31 @@ function createNote(req) {
title = `Clipped note from ${pageUrl}`;
}
const clipperInbox = getClipperInboxNote();
const {note} = noteService.createNewNote({
parentNoteId: clipperInbox.noteId,
title,
content,
type: 'text'
});
clipType = htmlSanitizer.sanitize(clipType);
note.setLabel('clipType', clipType);
const clipperInbox = getClipperInboxNote();
const dailyNote = dateNoteService.getDayNote(dateUtils.localNowDate());
pageUrl = htmlSanitizer.sanitizeUrl(pageUrl);
let note = findClippingNote(clipperInbox, pageUrl, clipType);
if (pageUrl) {
pageUrl = htmlSanitizer.sanitizeUrl(pageUrl);
if (!note) {
note = noteService.createNewNote({
parentNoteId: dailyNote.noteId,
title,
content: '',
type: 'text'
}).note;
note.setLabel('pageUrl', pageUrl);
note.setLabel('iconClass', 'bx bx-globe');
note.setLabel('clipType', clipType);
if (pageUrl) {
pageUrl = htmlSanitizer.sanitizeUrl(pageUrl);
note.setLabel('pageUrl', pageUrl);
note.setLabel('iconClass', 'bx bx-globe');
}
}
if (labels) {
for (const labelName in labels) {
const labelValue = htmlSanitizer.sanitize(labels[labelName]);
@ -108,9 +125,9 @@ function createNote(req) {
}
}
const existingContent = note.getContent();
const rewrittenContent = processContent(images, note, content);
note.setContent(rewrittenContent);
note.setContent(`${existingContent}${existingContent.trim() ? "<br/>" : ""}${rewrittenContent}`);
return {
noteId: note.noteId
@ -158,6 +175,15 @@ function processContent(images, note, content) {
// fallback if parsing/downloading images fails for some reason on the extension side (
rewrittenContent = noteService.downloadImages(note.noteId, rewrittenContent);
// Check if rewrittenContent contains at least one HTML tag
if (!/<.+?>/.test(rewrittenContent)) {
rewrittenContent = `<p>${rewrittenContent}</p>`;
}
// Create a JSDOM object from the existing HTML content
const dom = new JSDOM(rewrittenContent);
// Get the content inside the body tag and serialize it
rewrittenContent = dom.window.document.body.innerHTML;
return rewrittenContent;
}
@ -187,9 +213,19 @@ function handshake() {
}
}
function findNotesByUrl(req){
let pageUrl = req.params.noteUrl;
const clipperInbox = getClipperInboxNote();
let foundPage = findClippingNote(clipperInbox, pageUrl, null);
return {
noteId: foundPage ? foundPage.noteId : null
}
}
module.exports = {
createNote,
addClipping,
openNote,
handshake
handshake,
findNotesByUrl
};

View file

@ -49,7 +49,7 @@ const ALLOWED_OPTIONS = new Set([
'compressImages',
'downloadImagesAutomatically',
'minTocHeadings',
'highlightedText',
'highlightsList',
'checkForUpdates',
'disableTray',
'eraseUnusedAttachmentsAfterSeconds',

View file

@ -11,7 +11,7 @@ function addRecentNote(req) {
}).save();
if (Math.random() < 0.05) {
// it's not necessary to run this everytime ...
// it's not necessary to run this every time ...
const cutOffDate = dateUtils.utcDateTimeStr(new Date(Date.now() - 24 * 3600 * 1000));
sql.execute(`DELETE FROM recent_notes WHERE utcDateCreated < ?`, [cutOffDate]);

View file

@ -28,6 +28,12 @@ function execute(req) {
for (let query of queries) {
query = query.trim();
while (query.startsWith('-- ')) {
// Query starts with one or more SQL comments, discard these before we execute.
const pivot = query.indexOf('\n');
query = pivot > 0 ? query.substr(pivot + 1).trim() : "";
}
if (!query) {
continue;
}

View file

@ -62,7 +62,7 @@ function checkSync() {
function syncNow() {
log.info("Received request to trigger sync now.");
// when explicitly asked for set in progress status immediatelly for faster user feedback
// when explicitly asked for set in progress status immediately for faster user feedback
ws.syncPullInProgress();
return syncService.sync();

View file

@ -269,6 +269,7 @@ function register(app) {
route(PST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler);
route(PST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler);
route(PST, '/api/clipper/open/:noteId', clipperMiddleware, clipperRoute.openNote, apiResultHandler);
route(GET, '/api/clipper/notes-by-url/:noteUrl', clipperMiddleware, clipperRoute.findNotesByUrl, apiResultHandler);
apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote);
apiRoute(GET, '/api/special-notes/days/:date', specialNotesRoute.getDayNote);

View file

@ -395,7 +395,7 @@ class ConsistencyChecks {
({noteId, isProtected, type, mime}) => {
if (this.autoFix) {
// it might be possible that the blob is not available only because of the interrupted
// sync, and it will come later. It's therefore important to guarantee that this artifical
// sync, and it will come later. It's therefore important to guarantee that this artificial
// record won't overwrite the real one coming from the sync.
const fakeDate = "2000-01-01 00:00:00Z";

View file

@ -57,5 +57,7 @@ function sanitize(dirtyHtml) {
module.exports = {
sanitize,
sanitizeUrl
sanitizeUrl: url => {
return sanitizeUrl(url).trim();
}
};

View file

@ -83,7 +83,7 @@ const defaultOptions = [
{ name: 'compressImages', value: 'true', isSynced: true },
{ name: 'downloadImagesAutomatically', value: 'true', isSynced: true },
{ name: 'minTocHeadings', value: '5', isSynced: true },
{ name: 'highlightedText', value: '["bold","italic","underline","color","bgColor"]', isSynced: true },
{ name: 'highlightsList', value: '["bold","italic","underline","color","bgColor"]', isSynced: true },
{ name: 'checkForUpdates', value: 'true', isSynced: true },
{ name: 'disableTray', value: 'false', isSynced: false },
{ name: 'eraseUnusedAttachmentsAfterSeconds', value: '2592000', isSynced: true },

View file

@ -55,7 +55,7 @@ ${bundle.script}\r
}
/**
* THIS METHOD CANT BE ASYNC, OTHERWISE TRANSACTION WRAPPER WON'T BE EFFECTIVE AND WE WILL BE LOSING THE
* THIS METHOD CAN'T BE ASYNC, OTHERWISE TRANSACTION WRAPPER WON'T BE EFFECTIVE AND WE WILL BE LOSING THE
* ENTITY CHANGES IN CLS.
*
* This method preserves frontend startNode - that's why we start execution from currentNote and override

View file

@ -19,20 +19,22 @@ class NoteFlatTextExp extends Expression {
/**
* @param {BNote} note
* @param {string[]} tokens
* @param {string[]} path
* @param {string[]} remainingTokens - tokens still needed to be found in the path towards root
* @param {string[]} takenPath - path so far taken towards from candidate note towards the root.
* It contains the suffix fragment of the full note path.
*/
const searchDownThePath = (note, tokens, path) => {
if (tokens.length === 0) {
const retPath = this.getNotePath(note, path);
const searchPathTowardsRoot = (note, remainingTokens, takenPath) => {
if (remainingTokens.length === 0) {
// we're done, just build the result
const resultPath = this.getNotePath(note, takenPath);
if (retPath) {
const noteId = retPath[retPath.length - 1];
if (resultPath) {
const noteId = resultPath[resultPath.length - 1];
if (!resultNoteSet.hasNoteId(noteId)) {
// we could get here from multiple paths, the first one wins because the paths
// are sorted by importance
executionContext.noteIdToNotePath[noteId] = retPath;
executionContext.noteIdToNotePath[noteId] = resultPath;
resultNoteSet.add(becca.notes[noteId]);
}
@ -42,22 +44,23 @@ class NoteFlatTextExp extends Expression {
}
if (note.parents.length === 0 || note.noteId === 'root') {
// we've reached root, but there are still remaining tokens -> this candidate note produced no result
return;
}
const foundAttrTokens = [];
for (const token of tokens) {
for (const token of remainingTokens) {
if (note.type.includes(token) || note.mime.includes(token)) {
foundAttrTokens.push(token);
}
}
for (const attribute of note.ownedAttributes) {
for (const attribute of note.getOwnedAttributes()) {
const normalizedName = utils.normalize(attribute.name);
const normalizedValue = utils.normalize(attribute.value);
for (const token of tokens) {
for (const token of remainingTokens) {
if (normalizedName.includes(token) || normalizedValue.includes(token)) {
foundAttrTokens.push(token);
}
@ -68,19 +71,19 @@ class NoteFlatTextExp extends Expression {
const title = utils.normalize(beccaService.getNoteTitle(note.noteId, parentNote.noteId));
const foundTokens = foundAttrTokens.slice();
for (const token of tokens) {
for (const token of remainingTokens) {
if (title.includes(token)) {
foundTokens.push(token);
}
}
if (foundTokens.length > 0) {
const remainingTokens = tokens.filter(token => !foundTokens.includes(token));
const newRemainingTokens = remainingTokens.filter(token => !foundTokens.includes(token));
searchDownThePath(parentNote, remainingTokens, [...path, note.noteId]);
searchPathTowardsRoot(parentNote, newRemainingTokens, [note.noteId, ...takenPath]);
}
else {
searchDownThePath(parentNote, tokens, [...path, note.noteId]);
searchPathTowardsRoot(parentNote, remainingTokens, [note.noteId, ...takenPath]);
}
}
}
@ -90,7 +93,7 @@ class NoteFlatTextExp extends Expression {
for (const note of candidateNotes) {
// autocomplete should be able to find notes by their noteIds as well (only leafs)
if (this.tokens.length === 1 && note.noteId.toLowerCase() === this.tokens[0]) {
searchDownThePath(note, [], []);
searchPathTowardsRoot(note, [], [note.noteId]);
continue;
}
@ -123,7 +126,7 @@ class NoteFlatTextExp extends Expression {
if (foundTokens.length > 0) {
const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token));
searchDownThePath(parentNote, remainingTokens, [note.noteId]);
searchPathTowardsRoot(parentNote, remainingTokens, [note.noteId]);
}
}
}
@ -131,14 +134,22 @@ class NoteFlatTextExp extends Expression {
return resultNoteSet;
}
getNotePath(note, path) {
if (path.length === 0) {
/**
* @param {BNote} note
* @param {string[]} takenPath
* @returns {string[]}
*/
getNotePath(note, takenPath) {
if (takenPath.length === 0) {
throw new Error("Path is not expected to be empty.");
} else if (takenPath.length === 1 && takenPath[0] === note.noteId) {
return note.getBestNotePath();
} else {
const closestNoteId = path[0];
const closestNoteBestNotePath = becca.getNote(closestNoteId).getBestNotePath();
// this note is the closest to root containing the last matching token(s), thus completing the requirements
// what's in this note's predecessors does not matter, thus we'll choose the best note path
const topMostMatchingTokenNotePath = becca.getNote(takenPath[0]).getBestNotePath();
return [...closestNoteBestNotePath, ...path.slice(1)];
return [...topMostMatchingTokenNotePath, ...takenPath.slice(1)];
}
}

View file

@ -10,7 +10,6 @@ const becca = require('../../../becca/becca');
const beccaService = require('../../../becca/becca_service');
const utils = require('../../utils');
const log = require('../../log');
const scriptService = require("../../script");
const hoistedNoteService = require("../../hoisted_note");
function searchFromNote(note) {
@ -73,6 +72,7 @@ function searchFromRelation(note, relationName) {
return [];
}
const scriptService = require("../../script"); // to avoid circular dependency
const result = scriptService.executeNote(scriptNote, {originEntity: note});
if (!Array.isArray(result)) {

View file

@ -13,7 +13,7 @@ class TaskContext {
this.noteDeletionHandlerTriggered = false;
// progressCount is meant to represent just some progress - to indicate the task is not stuck
this.progressCount = -1; // we're incrementing immediatelly
this.progressCount = -1; // we're incrementing immediately
this.lastSentCountTs = 0; // 0 will guarantee the first message will be sent
// just the fact this has been initialized is a progress which should be sent to clients

View file

@ -96,7 +96,7 @@
<li>From the Trilium Menu, click Options.</li>
<li>Click on Sync tab.</li>
<li>Change server instance address to: <span id="current-host"></span> and click save.</li>
<li>Click "Test sync" button to verify connection is successfull.</li>
<li>Click "Test sync" button to verify connection is successful.</li>
<li>Once you've completed these steps, click <a href="/">here</a>.</li>
</ol>