mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	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:
		| @@ -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= | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -667,7 +667,7 @@ class BNote extends AbstractBeccaEntity { | ||||
|             return this.ownedAttributes.filter(attr => attr.name === name); | ||||
|         } | ||||
|         else { | ||||
|             return this.ownedAttributes.slice(); | ||||
|             return this.ownedAttributes; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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'); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|     }); | ||||
|  | ||||
|   | ||||
| @@ -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]; | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -26,7 +26,7 @@ export default class AbstractBulkAction { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // to be overriden | ||||
|     // to be overridden | ||||
|     doRender() {} | ||||
|  | ||||
|     async saveAction(data) { | ||||
|   | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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(); | ||||
|                 //Can’t 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"); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -9584,7 +9584,7 @@ const icons = [ | ||||
|         "term": [ | ||||
|             "honor", | ||||
|             "honour", | ||||
|             "acheivement" | ||||
|             "achievement" | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
| @@ -9595,7 +9595,7 @@ const icons = [ | ||||
|         "term": [ | ||||
|             "honor", | ||||
|             "honour", | ||||
|             "acheivement" | ||||
|             "achievement" | ||||
|         ] | ||||
|     }, | ||||
|     { | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -39,7 +39,7 @@ export default class AbstractSearchOption extends Component { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     // to be overriden | ||||
|     // to be overridden | ||||
|     doRender() {} | ||||
|  | ||||
|     async deleteOption() { | ||||
|   | ||||
| @@ -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(); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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' | ||||
|                 }); | ||||
|             } | ||||
|   | ||||
| @@ -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: | ||||
|   | ||||
| @@ -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: [ | ||||
|   | ||||
| @@ -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)) | ||||
|                     )) | ||||
|             ); | ||||
|   | ||||
| @@ -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  </label> | ||||
|     <label><input type="checkbox" class="highlighted-text-check" value="italic"> Italic font  </label> | ||||
|     <label><input type="checkbox" class="highlighted-text-check" value="underline"> Underlined font  </label> | ||||
|     <label><input type="checkbox" class="highlighted-text-check" value="color"> Font with color  </label> | ||||
|     <label><input type="checkbox" class="highlighted-text-check" value="bgColor"> Font with background color  </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); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @@ -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  </label> | ||||
|     <label><input type="checkbox" class="highlights-list-check" value="italic"> Italic font  </label> | ||||
|     <label><input type="checkbox" class="highlights-list-check" value="underline"> Underlined font  </label> | ||||
|     <label><input type="checkbox" class="highlights-list-check" value="color"> Font with color  </label> | ||||
|     <label><input type="checkbox" class="highlights-list-check" value="bgColor"> Font with background color  </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); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| } | ||||
| @@ -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; | ||||
|         } | ||||
|   | ||||
| @@ -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,17 +93,21 @@ 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); | ||||
|  | ||||
|     const clipperInbox = getClipperInboxNote(); | ||||
|     const dailyNote = dateNoteService.getDayNote(dateUtils.localNowDate()); | ||||
|     pageUrl = htmlSanitizer.sanitizeUrl(pageUrl); | ||||
|     let note = findClippingNote(clipperInbox, pageUrl, clipType); | ||||
|  | ||||
|     if (!note) { | ||||
|         note = noteService.createNewNote({ | ||||
|             parentNoteId: dailyNote.noteId, | ||||
|             title, | ||||
|             content: '', | ||||
|             type: 'text' | ||||
|         }).note; | ||||
|  | ||||
|         note.setLabel('clipType', clipType); | ||||
|  | ||||
|         if (pageUrl) { | ||||
| @@ -100,6 +116,7 @@ function createNote(req) { | ||||
|             note.setLabel('pageUrl', pageUrl); | ||||
|             note.setLabel('iconClass', 'bx bx-globe'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (labels) { | ||||
|         for (const labelName in labels) { | ||||
| @@ -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 | ||||
| }; | ||||
|   | ||||
| @@ -49,7 +49,7 @@ const ALLOWED_OPTIONS = new Set([ | ||||
|     'compressImages', | ||||
|     'downloadImagesAutomatically', | ||||
|     'minTocHeadings', | ||||
|     'highlightedText', | ||||
|     'highlightsList', | ||||
|     'checkForUpdates', | ||||
|     'disableTray', | ||||
|     'eraseUnusedAttachmentsAfterSeconds', | ||||
|   | ||||
| @@ -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]); | ||||
|   | ||||
| @@ -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; | ||||
|             } | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -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"; | ||||
|  | ||||
|   | ||||
| @@ -57,5 +57,7 @@ function sanitize(dirtyHtml) { | ||||
|  | ||||
| module.exports = { | ||||
|     sanitize, | ||||
|     sanitizeUrl | ||||
|     sanitizeUrl: url => { | ||||
|         return sanitizeUrl(url).trim(); | ||||
|     } | ||||
| }; | ||||
|   | ||||
| @@ -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 }, | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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)]; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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)) { | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user