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 | # 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 setting is relevant only for web deployments, desktop builds run on a fixed port (changeable with TRILIUM_PORT environment variable) | ||||||
| port=8080 | port=8080 | ||||||
| # true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure). | # true for TLS/SSL/HTTPS (secure), false for HTTP (insecure). | ||||||
| https=false | https=false | ||||||
| # path to certificate (run "bash bin/generate-cert.sh" to generate self-signed certificate). Relevant only if https=true | # path to certificate (run "bash bin/generate-cert.sh" to generate self-signed certificate). Relevant only if https=true | ||||||
| certPath= | certPath= | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ const fs = require("fs"); | |||||||
| const dataDir = require("./src/services/data_dir"); | const dataDir = require("./src/services/data_dir"); | ||||||
| const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, 'utf-8')); | 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 |     // 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 |     // for reverse proxy terminated TLS this will works since config.https will be false | ||||||
|     process.exit(0); |     process.exit(0); | ||||||
|   | |||||||
| @@ -667,7 +667,7 @@ class BNote extends AbstractBeccaEntity { | |||||||
|             return this.ownedAttributes.filter(attr => attr.name === name); |             return this.ownedAttributes.filter(attr => attr.name === name); | ||||||
|         } |         } | ||||||
|         else { |         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 |          * 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. |          * 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. |          * they are probably part of the import and not created by hand - these OTOH should not benefit. | ||||||
|          */ |          */ | ||||||
|         const {utcDateCreated} = candidateNote; |         const {utcDateCreated} = candidateNote; | ||||||
|   | |||||||
| @@ -231,7 +231,7 @@ paths: | |||||||
|         schema: |         schema: | ||||||
|           $ref: '#/components/schemas/EntityId' |           $ref: '#/components/schemas/EntityId' | ||||||
|     get: |     get: | ||||||
|       description: Returns note content idenfied by its ID |       description: Returns note content identified by its ID | ||||||
|       operationId: getNoteContent |       operationId: getNoteContent | ||||||
|       responses: |       responses: | ||||||
|         '200': |         '200': | ||||||
| @@ -241,7 +241,7 @@ paths: | |||||||
|               schema: |               schema: | ||||||
|                 type: string |                 type: string | ||||||
|     put: |     put: | ||||||
|       description: Updates note content idenfied by its ID |       description: Updates note content identified by its ID | ||||||
|       operationId: putNoteContentById |       operationId: putNoteContentById | ||||||
|       requestBody: |       requestBody: | ||||||
|         description: html content of note |         description: html content of note | ||||||
|   | |||||||
| @@ -41,8 +41,8 @@ function initAttributeNameAutocomplete({ $el, attributeType, open }) { | |||||||
|  |  | ||||||
| async function initLabelValueAutocomplete({ $el, open, nameCallback }) { | async function initLabelValueAutocomplete({ $el, open, nameCallback }) { | ||||||
|     if ($el.hasClass("aa-input")) { |     if ($el.hasClass("aa-input")) { | ||||||
|         // we reinit everytime because autocomplete seems to have a bug where it retains state from last |         // we reinit every time because autocomplete seems to have a bug where it retains state from last | ||||||
|         // open even though the value was resetted |         // open even though the value was reset | ||||||
|         $el.autocomplete('destroy'); |         $el.autocomplete('destroy'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -133,7 +133,7 @@ function initNoteAutocomplete($el, options) { | |||||||
|         showRecentNotes($el); |         showRecentNotes($el); | ||||||
|  |  | ||||||
|         // this will cause the click not give focus to the "show recent notes" button |         // 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; |         return false; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -99,7 +99,7 @@ function parseSelectedHtml(selectedHtml) { | |||||||
|  |  | ||||||
|     if (dom.length > 0 && dom[0].tagName && dom[0].tagName.match(/h[1-6]/i)) { |     if (dom.length > 0 && dom[0].tagName && dom[0].tagName.match(/h[1-6]/i)) { | ||||||
|         const title = $(dom[0]).text(); |         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, ""); |         const content = selectedHtml.replace(dom[0].outerHTML, ""); | ||||||
|  |  | ||||||
|         return [title, content]; |         return [title, content]; | ||||||
|   | |||||||
| @@ -161,7 +161,7 @@ class NoteListRenderer { | |||||||
|     constructor($parent, parentNote, noteIds, showNotePath = false) { |     constructor($parent, parentNote, noteIds, showNotePath = false) { | ||||||
|         this.$noteList = $(TPL); |         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(); |         $parent.empty(); | ||||||
|  |  | ||||||
|         this.parentNote = parentNote; |         this.parentNote = parentNote; | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ async function getHeaders(headers) { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (utils.isElectron()) { |     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; |         allHeaders.cookie = document.cookie; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| /** | /** | ||||||
|  * Fetch note with given ID from backend |  * 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) { | async function fetchNote(noteId = null) { | ||||||
|     if (!noteId) { |     if (!noteId) { | ||||||
|   | |||||||
| @@ -26,7 +26,7 @@ export default class AbstractBulkAction { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // to be overriden |     // to be overridden | ||||||
|     doRender() {} |     doRender() {} | ||||||
|  |  | ||||||
|     async saveAction(data) { |     async saveAction(data) { | ||||||
|   | |||||||
| @@ -50,7 +50,7 @@ export default class RightDropdownButtonWidget extends BasicWidget { | |||||||
|         this.$widget.find(".dropdown-menu").append(this.$dropdownContent); |         this.$widget.find(".dropdown-menu").append(this.$dropdownContent); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // to be overriden |     // to be overridden | ||||||
|     async dropdownShow() {} |     async dropdownShow() {} | ||||||
|  |  | ||||||
|     hideDropdown() { |     hideDropdown() { | ||||||
|   | |||||||
| @@ -25,7 +25,7 @@ const TPL = ` | |||||||
|                 </div> |                 </div> | ||||||
|  |  | ||||||
|                 <div class="checkbox"> |                 <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"> |                         <input class="erase-notes" value="1" type="checkbox"> | ||||||
|  |  | ||||||
|                         erase notes permanently (can't be undone), including all clones. This will force application reload. |                         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 options from "../services/options.js"; | ||||||
| import OnClickButtonWidget from "./buttons/onclick_button.js"; | import OnClickButtonWidget from "./buttons/onclick_button.js"; | ||||||
|  |  | ||||||
| const TPL = `<div class="highlists-list-widget"> | const TPL = `<div class="highlights-list-widget"> | ||||||
|     <style> |     <style> | ||||||
|         .highlists-list-widget { |         .highlights-list-widget { | ||||||
|             padding: 10px; |             padding: 10px; | ||||||
|             contain: none;  |             contain: none;  | ||||||
|             overflow: auto; |             overflow: auto; | ||||||
|             position: relative; |             position: relative; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         .highlists-list > ol { |         .highlights-list > ol { | ||||||
|             padding-left: 20px; |             padding-left: 20px; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         .highlists-list li { |         .highlights-list li { | ||||||
|             cursor: pointer; |             cursor: pointer; | ||||||
|             margin-bottom: 3px; |             margin-bottom: 3px; | ||||||
|             text-align: justify; |             text-align: justify; | ||||||
| @@ -32,18 +32,18 @@ const TPL = `<div class="highlists-list-widget"> | |||||||
|             hyphens: auto; |             hyphens: auto; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         .highlists-list li:hover { |         .highlights-list li:hover { | ||||||
|             font-weight: bold; |             font-weight: bold; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         .close-highlists-list { |         .close-highlights-list { | ||||||
|             position: absolute; |             position: absolute; | ||||||
|             top: 2px; |             top: 2px; | ||||||
|             right: 0px; |             right: 0px; | ||||||
|         } |         } | ||||||
|     </style> |     </style> | ||||||
|  |  | ||||||
|     <span class="highlists-list"></span> |     <span class="highlights-list"></span> | ||||||
| </div>`; | </div>`; | ||||||
|  |  | ||||||
| export default class HighlightsListWidget extends RightPanelWidget { | export default class HighlightsListWidget extends RightPanelWidget { | ||||||
| @@ -55,61 +55,61 @@ export default class HighlightsListWidget extends RightPanelWidget { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     get widgetTitle() { |     get widgetTitle() { | ||||||
|         return "Highlighted Text"; |         return "Highlights List"; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     isEnabled() { |     isEnabled() { | ||||||
|         return super.isEnabled() |         return super.isEnabled() | ||||||
|             && this.note.type === 'text' |             && this.note.type === 'text' | ||||||
|             && !this.noteContext.viewScope.highlightedTextTemporarilyHidden |             && !this.noteContext.viewScope.highlightsListTemporarilyHidden | ||||||
|             && this.noteContext.viewScope.viewMode === 'default'; |             && this.noteContext.viewScope.viewMode === 'default'; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async doRenderBody() { |     async doRenderBody() { | ||||||
|         this.$body.empty().append($(TPL)); |         this.$body.empty().append($(TPL)); | ||||||
|         this.$highlightsList = this.$body.find('.highlists-list'); |         this.$highlightsList = this.$body.find('.highlights-list'); | ||||||
|         this.$body.find('.highlists-list-widget').append(this.closeHltButton.render()); |         this.$body.find('.highlights-list-widget').append(this.closeHltButton.render()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async refreshWithNote(note) { |     async refreshWithNote(note) { | ||||||
|         /* The reason for adding highlightedTextPreviousVisible is to record whether the previous state |         /* The reason for adding highlightsListPreviousVisible 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. |            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, |            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, |            every time the note content is changed, highlighttext Widget will appear and then close immediately, | ||||||
|            because getHlt function will consume time */ |            because getHlt function will consume time */ | ||||||
|         if (this.noteContext.viewScope.highlightedTextPreviousVisible) { |         if (this.noteContext.viewScope.highlightsListPreviousVisible) { | ||||||
|             this.toggleInt(true); |             this.toggleInt(true); | ||||||
|         } else { |         } else { | ||||||
|             this.toggleInt(false); |             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.toggleInt(false); | ||||||
|             this.triggerCommand("reEvaluateRightPaneVisibility"); |             this.triggerCommand("reEvaluateRightPaneVisibility"); | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         let $highlightsList = "", hltLiCount = -1; |         let $highlightsList = "", hlLiCount = -1; | ||||||
|         // Check for type text unconditionally in case alwaysShowWidget is set |         // Check for type text unconditionally in case alwaysShowWidget is set | ||||||
|         if (this.note.type === 'text') { |         if (this.note.type === 'text') { | ||||||
|             const {content} = await note.getNoteComplement(); |             const {content} = await note.getNoteComplement(); | ||||||
|             ({$highlightsList, hltLiCount} = this.getHighlightList(content, optionsHlt)); |             ({$highlightsList, hlLiCount} = this.getHighlightList(content, optionsHighlightsList)); | ||||||
|         } |         } | ||||||
|         this.$highlightsList.empty().append($highlightsList); |         this.$highlightsList.empty().append($highlightsList); | ||||||
|         if (hltLiCount > 0) { |         if (hlLiCount > 0) { | ||||||
|             this.toggleInt(true); |             this.toggleInt(true); | ||||||
|             this.noteContext.viewScope.highlightedTextPreviousVisible = true; |             this.noteContext.viewScope.highlightsListPreviousVisible = true; | ||||||
|         } else { |         } else { | ||||||
|             this.toggleInt(false); |             this.toggleInt(false); | ||||||
|             this.noteContext.viewScope.highlightedTextPreviousVisible = false; |             this.noteContext.viewScope.highlightsListPreviousVisible = false; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         this.triggerCommand("reEvaluateRightPaneVisibility"); |         this.triggerCommand("reEvaluateRightPaneVisibility"); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     getHighlightList(content, optionsHlt) { |     getHighlightList(content, optionsHighlightsList) { | ||||||
|         // matches a span containing background-color |         // matches a span containing background-color | ||||||
|         const regex1 = /<span[^>]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi; |         const regex1 = /<span[^>]*style\s*=\s*[^>]*background-color:[^>]*?>[\s\S]*?<\/span>/gi; | ||||||
|         // matches a span containing color |         // matches a span containing color | ||||||
| @@ -120,27 +120,27 @@ export default class HighlightsListWidget extends RightPanelWidget { | |||||||
|         const regex4 = /<strong>[\s\S]*?<\/strong>/gi; |         const regex4 = /<strong>[\s\S]*?<\/strong>/gi; | ||||||
|         // match underline |         // match underline | ||||||
|         const regex5 = /<u>[\s\S]*?<\/u>/g; |         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 |         // element priority: span>i>strong>u | ||||||
|         let findSubStr = "", combinedRegexStr = ""; |         let findSubStr = "", combinedRegexStr = ""; | ||||||
|         if (optionsHlt.includes("bgColor")) { |         if (optionsHighlightsList.includes("bgColor")) { | ||||||
|             findSubStr += `,span[style*="background-color"]`; |             findSubStr += `,span[style*="background-color"]:not(section.include-note span[style*="background-color"])`; | ||||||
|             combinedRegexStr += `|${regex1.source}`; |             combinedRegexStr += `|${regex1.source}`; | ||||||
|         } |         } | ||||||
|         if (optionsHlt.includes("color")) { |         if (optionsHighlightsList.includes("color")) { | ||||||
|             findSubStr += `,span[style*="color"]`; |             findSubStr += `,span[style*="color"]:not(section.include-note span[style*="color"])`; | ||||||
|             combinedRegexStr += `|${regex2.source}`; |             combinedRegexStr += `|${regex2.source}`; | ||||||
|         } |         } | ||||||
|         if (optionsHlt.includes("italic")) { |         if (optionsHighlightsList.includes("italic")) { | ||||||
|             findSubStr += `,i`; |             findSubStr += `,i:not(section.include-note i)`; | ||||||
|             combinedRegexStr += `|${regex3.source}`; |             combinedRegexStr += `|${regex3.source}`; | ||||||
|         } |         } | ||||||
|         if (optionsHlt.indexOf("bold")) { |         if (optionsHighlightsList.includes("bold")) { | ||||||
|             findSubStr += `,strong`; |             findSubStr += `,strong:not(section.include-note strong)`; | ||||||
|             combinedRegexStr += `|${regex4.source}`; |             combinedRegexStr += `|${regex4.source}`; | ||||||
|         } |         } | ||||||
|         if (optionsHlt.includes("underline")) { |         if (optionsHighlightsList.includes("underline")) { | ||||||
|             findSubStr += `,u`; |             findSubStr += `,u:not(section.include-note u)`; | ||||||
|             combinedRegexStr += `|${regex5.source}`; |             combinedRegexStr += `|${regex5.source}`; | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -148,7 +148,7 @@ export default class HighlightsListWidget extends RightPanelWidget { | |||||||
|         combinedRegexStr = `(` + combinedRegexStr.substring(1) + `)`; |         combinedRegexStr = `(` + combinedRegexStr.substring(1) + `)`; | ||||||
|         const combinedRegex = new RegExp(combinedRegexStr, 'gi'); |         const combinedRegex = new RegExp(combinedRegexStr, 'gi'); | ||||||
|         const $highlightsList = $("<ol>"); |         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++) { |         for (let match = null, hltIndex = 0; ((match = combinedRegex.exec(content)) !== null); hltIndex++) { | ||||||
|             const subHtml = match[0]; |             const subHtml = match[0]; | ||||||
|             const startIndex = match.index; |             const startIndex = match.index; | ||||||
| @@ -158,16 +158,18 @@ export default class HighlightsListWidget extends RightPanelWidget { | |||||||
|                 $highlightsList.children().last().append(subHtml); |                 $highlightsList.children().last().append(subHtml); | ||||||
|             } else { |             } else { | ||||||
|                 // TODO: can't be done with $(subHtml).text()? |                 // 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) { |                 if (hasText) { | ||||||
|                     $highlightsList.append( |                     $highlightsList.append( | ||||||
|                         $('<li>') |                         $('<li>') | ||||||
|                             .html(subHtml) |                             .html(subHtml) | ||||||
|                             .on("click", () => this.jumpToHighlightedText(findSubStr, hltIndex)) |                             .on("click", () => this.jumpToHighlightsList(findSubStr, hltIndex)) | ||||||
|                     ); |                     ); | ||||||
|  |  | ||||||
|                     hltLiCount++; |                     hlLiCount++; | ||||||
|                 } else { |                 } else { | ||||||
|                     // hide li if its text content is empty |                     // hide li if its text content is empty | ||||||
|                     continue; |                     continue; | ||||||
| @@ -177,11 +179,11 @@ export default class HighlightsListWidget extends RightPanelWidget { | |||||||
|         } |         } | ||||||
|         return { |         return { | ||||||
|             $highlightsList, |             $highlightsList, | ||||||
|             hltLiCount |             hlLiCount | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     async jumpToHighlightedText(findSubStr, itemIndex) { |     async jumpToHighlightsList(findSubStr, itemIndex) { | ||||||
|         const isReadOnly = await this.noteContext.isReadOnly(); |         const isReadOnly = await this.noteContext.isReadOnly(); | ||||||
|         let targetElement; |         let targetElement; | ||||||
|         if (isReadOnly) { |         if (isReadOnly) { | ||||||
| @@ -224,7 +226,7 @@ export default class HighlightsListWidget extends RightPanelWidget { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     async closeHltCommand() { |     async closeHltCommand() { | ||||||
|         this.noteContext.viewScope.highlightedTextTemporarilyHidden = true; |         this.noteContext.viewScope.highlightsListTemporarilyHidden = true; | ||||||
|         await this.refresh(); |         await this.refresh(); | ||||||
|         this.triggerCommand('reEvaluateRightPaneVisibility'); |         this.triggerCommand('reEvaluateRightPaneVisibility'); | ||||||
|     } |     } | ||||||
| @@ -245,13 +247,13 @@ class CloseHltButton extends OnClickButtonWidget { | |||||||
|         super(); |         super(); | ||||||
|  |  | ||||||
|         this.icon("bx-x") |         this.icon("bx-x") | ||||||
|             .title("Close HighlightedTextWidget") |             .title("Close HighlightsListWidget") | ||||||
|             .titlePlacement("bottom") |             .titlePlacement("bottom") | ||||||
|             .onClick((widget, e) => { |             .onClick((widget, e) => { | ||||||
|                 e.stopPropagation(); |                 e.stopPropagation(); | ||||||
|  |  | ||||||
|                 widget.triggerCommand("closeHlt"); |                 widget.triggerCommand("closeHlt"); | ||||||
|             }) |             }) | ||||||
|             .class("icon-action close-highlists-list"); |             .class("icon-action close-highlights-list"); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -9584,7 +9584,7 @@ const icons = [ | |||||||
|         "term": [ |         "term": [ | ||||||
|             "honor", |             "honor", | ||||||
|             "honour", |             "honour", | ||||||
|             "acheivement" |             "achievement" | ||||||
|         ] |         ] | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
| @@ -9595,7 +9595,7 @@ const icons = [ | |||||||
|         "term": [ |         "term": [ | ||||||
|             "honor", |             "honor", | ||||||
|             "honour", |             "honour", | ||||||
|             "acheivement" |             "achievement" | ||||||
|         ] |         ] | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|   | |||||||
| @@ -166,7 +166,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget { | |||||||
|  |  | ||||||
|     generateColorFromString(str) { |     generateColorFromString(str) { | ||||||
|         if (this.themeStyle === "dark") { |         if (this.themeStyle === "dark") { | ||||||
|             str = `0${str}`; // magic lightening modifier |             str = `0${str}`; // magic lightning modifier | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         let hash = 0; |         let hash = 0; | ||||||
|   | |||||||
| @@ -1116,7 +1116,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget { | |||||||
|                 const note = froca.getNoteFromCache(ecAttr.noteId); |                 const note = froca.getNoteFromCache(ecAttr.noteId); | ||||||
|  |  | ||||||
|                 if (note && note.getChildNoteIds().includes(ecAttr.value)) { |                 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, |                     // 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) |                     // 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); |                     noteIdsToReload.add(ecAttr.noteId); | ||||||
|   | |||||||
| @@ -39,7 +39,7 @@ export default class AbstractSearchOption extends Component { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // to be overriden |     // to be overridden | ||||||
|     doRender() {} |     doRender() {} | ||||||
|  |  | ||||||
|     async deleteOption() { |     async deleteOption() { | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import AbstractSearchOption from "./abstract_search_option.js"; | |||||||
| import SpacedUpdate from "../../services/spaced_update.js"; | import SpacedUpdate from "../../services/spaced_update.js"; | ||||||
| import server from "../../services/server.js"; | import server from "../../services/server.js"; | ||||||
| import shortcutService from "../../services/shortcuts.js"; | import shortcutService from "../../services/shortcuts.js"; | ||||||
|  | import appContext from "../../components/app_context.js"; | ||||||
|  |  | ||||||
| const TPL = ` | const TPL = ` | ||||||
| <tr> | <tr> | ||||||
| @@ -56,6 +57,7 @@ export default class SearchString extends AbstractSearchOption { | |||||||
|  |  | ||||||
|         this.spacedUpdate = new SpacedUpdate(async () => { |         this.spacedUpdate = new SpacedUpdate(async () => { | ||||||
|             const searchString = this.$searchString.val(); |             const searchString = this.$searchString.val(); | ||||||
|  |             appContext.lastSearchString = searchString; | ||||||
|  |  | ||||||
|             await this.setAttribute('label', 'searchString', searchString); |             await this.setAttribute('label', 'searchString', searchString); | ||||||
|  |  | ||||||
| @@ -84,6 +86,7 @@ export default class SearchString extends AbstractSearchOption { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     focusOnSearchDefinitionEvent() { |     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) { |         if (isReadOnly) { | ||||||
|             const $container = await this.noteContext.getContentElement(); |             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) { |             if (headingElement != null) { | ||||||
|                 headingElement.scrollIntoView({ behavior: "smooth" }); |                 headingElement.scrollIntoView({ behavior: "smooth" }); | ||||||
| @@ -206,7 +206,7 @@ export default class TocWidget extends RightPanelWidget { | |||||||
|             // navigate (note that the TOC rendering and other TOC |             // navigate (note that the TOC rendering and other TOC | ||||||
|             // entries' navigation could be wrong too) |             // entries' navigation could be wrong too) | ||||||
|             if (headingNode != null) { |             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' |                     behavior: 'smooth' | ||||||
|                 }); |                 }); | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -77,7 +77,7 @@ const TPL = ` | |||||||
|  * |  * | ||||||
|  * Discussion of storing svg in the note: |  * 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. |  *  - 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. |  *         desktop instance mitigates that issue. | ||||||
|  * |  * | ||||||
|  * Roadmap: |  * Roadmap: | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import MaxContentWidthOptions from "./options/appearance/max_content_width.js"; | |||||||
| import KeyboardShortcutsOptions from "./options/shortcuts.js"; | import KeyboardShortcutsOptions from "./options/shortcuts.js"; | ||||||
| import HeadingStyleOptions from "./options/text_notes/heading_style.js"; | import HeadingStyleOptions from "./options/text_notes/heading_style.js"; | ||||||
| import TableOfContentsOptions from "./options/text_notes/table_of_contents.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 TextAutoReadOnlySizeOptions from "./options/text_notes/text_auto_read_only_size.js"; | ||||||
| import VimKeyBindingsOptions from "./options/code_notes/vim_key_bindings.js"; | import VimKeyBindingsOptions from "./options/code_notes/vim_key_bindings.js"; | ||||||
| import WrapLinesOptions from "./options/code_notes/wrap_lines.js"; | import WrapLinesOptions from "./options/code_notes/wrap_lines.js"; | ||||||
| @@ -63,7 +63,7 @@ const CONTENT_WIDGETS = { | |||||||
|     _optionsTextNotes: [ |     _optionsTextNotes: [ | ||||||
|         HeadingStyleOptions, |         HeadingStyleOptions, | ||||||
|         TableOfContentsOptions, |         TableOfContentsOptions, | ||||||
|         HighlightedTextOptions, |         HighlightsListOptions, | ||||||
|         TextAutoReadOnlySizeOptions |         TextAutoReadOnlySizeOptions | ||||||
|     ], |     ], | ||||||
|     _optionsCodeNotes: [ |     _optionsCodeNotes: [ | ||||||
|   | |||||||
| @@ -96,7 +96,7 @@ export default class EtapiOptions extends OptionsWidget { | |||||||
|                     .append($("<td>").append( |                     .append($("<td>").append( | ||||||
|                         $('<span class="bx bx-pen token-table-button" title="Rename this token"></span>') |                         $('<span class="bx bx-pen token-table-button" title="Rename this token"></span>') | ||||||
|                             .on("click", () => this.renameToken(token.etapiTokenId, token.name)), |                             .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)) |                             .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) { |         if (!originalEvent) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| "use strict"; | "use strict"; | ||||||
|  |  | ||||||
| const attributeService = require("../../services/attributes"); | const attributeService = require("../../services/attributes"); | ||||||
|  | const cloneService = require("../../services/cloning"); | ||||||
| const noteService = require('../../services/notes'); | const noteService = require('../../services/notes'); | ||||||
| const dateNoteService = require('../../services/date_notes'); | const dateNoteService = require('../../services/date_notes'); | ||||||
| const dateUtils = require('../../services/date_utils'); | const dateUtils = require('../../services/date_utils'); | ||||||
| @@ -13,46 +14,25 @@ const path = require('path'); | |||||||
| const BAttribute = require('../../becca/entities/battribute'); | const BAttribute = require('../../becca/entities/battribute'); | ||||||
| const htmlSanitizer = require('../../services/html_sanitizer'); | const htmlSanitizer = require('../../services/html_sanitizer'); | ||||||
| const {formatAttrForSearch} = require("../../services/attribute_formatter"); | const {formatAttrForSearch} = require("../../services/attribute_formatter"); | ||||||
|  | const jsdom = require("jsdom"); | ||||||
| function findClippingNote(clipperInboxNote, pageUrl) { | const { JSDOM } = jsdom; | ||||||
|     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; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| function addClipping(req) { | 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; |     let {title, content, pageUrl, images} = req.body; | ||||||
|  |     const clipType = 'clippings'; | ||||||
|  |  | ||||||
|     const clipperInbox = getClipperInboxNote(); |     const clipperInbox = getClipperInboxNote(); | ||||||
|  |     const dailyNote = dateNoteService.getDayNote(dateUtils.localNowDate()); | ||||||
|  |  | ||||||
|     pageUrl = htmlSanitizer.sanitizeUrl(pageUrl); |     pageUrl = htmlSanitizer.sanitizeUrl(pageUrl); | ||||||
|     let clippingNote = findClippingNote(clipperInbox, pageUrl); |     let clippingNote = findClippingNote(clipperInbox, pageUrl, clipType); | ||||||
|  |  | ||||||
|     if (!clippingNote) { |     if (!clippingNote) { | ||||||
|         clippingNote = noteService.createNewNote({ |         clippingNote = noteService.createNewNote({ | ||||||
|             parentNoteId: clipperInbox.noteId, |             parentNoteId: dailyNote.noteId, | ||||||
|             title: title, |             title: title, | ||||||
|             content: '', |             content: '', | ||||||
|             type: 'text' |             type: 'text' | ||||||
| @@ -67,13 +47,45 @@ function addClipping(req) { | |||||||
|  |  | ||||||
|     const existingContent = clippingNote.getContent(); |     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 { |     return { | ||||||
|         noteId: clippingNote.noteId |         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) { | function createNote(req) { | ||||||
|     let {title, content, pageUrl, images, clipType, labels} = req.body; |     let {title, content, pageUrl, images, clipType, labels} = req.body; | ||||||
|  |  | ||||||
| @@ -81,17 +93,21 @@ function createNote(req) { | |||||||
|         title = `Clipped note from ${pageUrl}`; |         title = `Clipped note from ${pageUrl}`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const clipperInbox = getClipperInboxNote(); |  | ||||||
|  |  | ||||||
|     const {note} = noteService.createNewNote({ |  | ||||||
|         parentNoteId: clipperInbox.noteId, |  | ||||||
|         title, |  | ||||||
|         content, |  | ||||||
|         type: 'text' |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     clipType = htmlSanitizer.sanitize(clipType); |     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); |         note.setLabel('clipType', clipType); | ||||||
|  |  | ||||||
|         if (pageUrl) { |         if (pageUrl) { | ||||||
| @@ -100,6 +116,7 @@ function createNote(req) { | |||||||
|             note.setLabel('pageUrl', pageUrl); |             note.setLabel('pageUrl', pageUrl); | ||||||
|             note.setLabel('iconClass', 'bx bx-globe'); |             note.setLabel('iconClass', 'bx bx-globe'); | ||||||
|         } |         } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     if (labels) { |     if (labels) { | ||||||
|         for (const labelName in labels) { |         for (const labelName in labels) { | ||||||
| @@ -108,9 +125,9 @@ function createNote(req) { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const existingContent = note.getContent(); | ||||||
|     const rewrittenContent = processContent(images, note, content); |     const rewrittenContent = processContent(images, note, content); | ||||||
|  |     note.setContent(`${existingContent}${existingContent.trim() ? "<br/>" : ""}${rewrittenContent}`); | ||||||
|     note.setContent(rewrittenContent); |  | ||||||
|  |  | ||||||
|     return { |     return { | ||||||
|         noteId: note.noteId |         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 ( |     // fallback if parsing/downloading images fails for some reason on the extension side ( | ||||||
|     rewrittenContent = noteService.downloadImages(note.noteId, rewrittenContent); |     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; |     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 = { | module.exports = { | ||||||
|     createNote, |     createNote, | ||||||
|     addClipping, |     addClipping, | ||||||
|     openNote, |     openNote, | ||||||
|     handshake |     handshake, | ||||||
|  |     findNotesByUrl | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -49,7 +49,7 @@ const ALLOWED_OPTIONS = new Set([ | |||||||
|     'compressImages', |     'compressImages', | ||||||
|     'downloadImagesAutomatically', |     'downloadImagesAutomatically', | ||||||
|     'minTocHeadings', |     'minTocHeadings', | ||||||
|     'highlightedText', |     'highlightsList', | ||||||
|     'checkForUpdates', |     'checkForUpdates', | ||||||
|     'disableTray', |     'disableTray', | ||||||
|     'eraseUnusedAttachmentsAfterSeconds', |     'eraseUnusedAttachmentsAfterSeconds', | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ function addRecentNote(req) { | |||||||
|     }).save(); |     }).save(); | ||||||
|  |  | ||||||
|     if (Math.random() < 0.05) { |     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)); |         const cutOffDate = dateUtils.utcDateTimeStr(new Date(Date.now() - 24 * 3600 * 1000)); | ||||||
|  |  | ||||||
|         sql.execute(`DELETE FROM recent_notes WHERE utcDateCreated < ?`, [cutOffDate]); |         sql.execute(`DELETE FROM recent_notes WHERE utcDateCreated < ?`, [cutOffDate]); | ||||||
|   | |||||||
| @@ -28,6 +28,12 @@ function execute(req) { | |||||||
|         for (let query of queries) { |         for (let query of queries) { | ||||||
|             query = query.trim(); |             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) { |             if (!query) { | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -62,7 +62,7 @@ function checkSync() { | |||||||
| function syncNow() { | function syncNow() { | ||||||
|     log.info("Received request to trigger sync now."); |     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(); |     ws.syncPullInProgress(); | ||||||
|  |  | ||||||
|     return syncService.sync(); |     return syncService.sync(); | ||||||
|   | |||||||
| @@ -269,6 +269,7 @@ function register(app) { | |||||||
|     route(PST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler); |     route(PST, '/api/clipper/clippings', clipperMiddleware, clipperRoute.addClipping, apiResultHandler); | ||||||
|     route(PST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler); |     route(PST, '/api/clipper/notes', clipperMiddleware, clipperRoute.createNote, apiResultHandler); | ||||||
|     route(PST, '/api/clipper/open/:noteId', clipperMiddleware, clipperRoute.openNote, 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/inbox/:date', specialNotesRoute.getInboxNote); | ||||||
|     apiRoute(GET, '/api/special-notes/days/:date', specialNotesRoute.getDayNote); |     apiRoute(GET, '/api/special-notes/days/:date', specialNotesRoute.getDayNote); | ||||||
|   | |||||||
| @@ -395,7 +395,7 @@ class ConsistencyChecks { | |||||||
|             ({noteId, isProtected, type, mime}) => { |             ({noteId, isProtected, type, mime}) => { | ||||||
|                 if (this.autoFix) { |                 if (this.autoFix) { | ||||||
|                     // it might be possible that the blob is not available only because of the interrupted |                     // 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. |                     // record won't overwrite the real one coming from the sync. | ||||||
|                     const fakeDate = "2000-01-01 00:00:00Z"; |                     const fakeDate = "2000-01-01 00:00:00Z"; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -57,5 +57,7 @@ function sanitize(dirtyHtml) { | |||||||
|  |  | ||||||
| module.exports = { | module.exports = { | ||||||
|     sanitize, |     sanitize, | ||||||
|     sanitizeUrl |     sanitizeUrl: url => { | ||||||
|  |         return sanitizeUrl(url).trim(); | ||||||
|  |     } | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -83,7 +83,7 @@ const defaultOptions = [ | |||||||
|     { name: 'compressImages', value: 'true', isSynced: true }, |     { name: 'compressImages', value: 'true', isSynced: true }, | ||||||
|     { name: 'downloadImagesAutomatically', value: 'true', isSynced: true }, |     { name: 'downloadImagesAutomatically', value: 'true', isSynced: true }, | ||||||
|     { name: 'minTocHeadings', value: '5', 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: 'checkForUpdates', value: 'true', isSynced: true }, | ||||||
|     { name: 'disableTray', value: 'false', isSynced: false }, |     { name: 'disableTray', value: 'false', isSynced: false }, | ||||||
|     { name: 'eraseUnusedAttachmentsAfterSeconds', value: '2592000', isSynced: true }, |     { 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. |  * ENTITY CHANGES IN CLS. | ||||||
|  * |  * | ||||||
|  * This method preserves frontend startNode - that's why we start execution from currentNote and override |  * 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 {BNote} note | ||||||
|          * @param {string[]} tokens |          * @param {string[]} remainingTokens - tokens still needed to be found in the path towards root | ||||||
|          * @param {string[]} path |          * @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) => { |         const searchPathTowardsRoot = (note, remainingTokens, takenPath) => { | ||||||
|             if (tokens.length === 0) { |             if (remainingTokens.length === 0) { | ||||||
|                 const retPath = this.getNotePath(note, path); |                 // we're done, just build the result | ||||||
|  |                 const resultPath = this.getNotePath(note, takenPath); | ||||||
|  |  | ||||||
|                 if (retPath) { |                 if (resultPath) { | ||||||
|                     const noteId = retPath[retPath.length - 1]; |                     const noteId = resultPath[resultPath.length - 1]; | ||||||
|  |  | ||||||
|                     if (!resultNoteSet.hasNoteId(noteId)) { |                     if (!resultNoteSet.hasNoteId(noteId)) { | ||||||
|                         // we could get here from multiple paths, the first one wins because the paths |                         // we could get here from multiple paths, the first one wins because the paths | ||||||
|                         // are sorted by importance |                         // are sorted by importance | ||||||
|                         executionContext.noteIdToNotePath[noteId] = retPath; |                         executionContext.noteIdToNotePath[noteId] = resultPath; | ||||||
|  |  | ||||||
|                         resultNoteSet.add(becca.notes[noteId]); |                         resultNoteSet.add(becca.notes[noteId]); | ||||||
|                     } |                     } | ||||||
| @@ -42,22 +44,23 @@ class NoteFlatTextExp extends Expression { | |||||||
|             } |             } | ||||||
|  |  | ||||||
|             if (note.parents.length === 0 || note.noteId === 'root') { |             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; |                 return; | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             const foundAttrTokens = []; |             const foundAttrTokens = []; | ||||||
|  |  | ||||||
|             for (const token of tokens) { |             for (const token of remainingTokens) { | ||||||
|                 if (note.type.includes(token) || note.mime.includes(token)) { |                 if (note.type.includes(token) || note.mime.includes(token)) { | ||||||
|                     foundAttrTokens.push(token); |                     foundAttrTokens.push(token); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |  | ||||||
|             for (const attribute of note.ownedAttributes) { |             for (const attribute of note.getOwnedAttributes()) { | ||||||
|                 const normalizedName = utils.normalize(attribute.name); |                 const normalizedName = utils.normalize(attribute.name); | ||||||
|                 const normalizedValue = utils.normalize(attribute.value); |                 const normalizedValue = utils.normalize(attribute.value); | ||||||
|  |  | ||||||
|                 for (const token of tokens) { |                 for (const token of remainingTokens) { | ||||||
|                     if (normalizedName.includes(token) || normalizedValue.includes(token)) { |                     if (normalizedName.includes(token) || normalizedValue.includes(token)) { | ||||||
|                         foundAttrTokens.push(token); |                         foundAttrTokens.push(token); | ||||||
|                     } |                     } | ||||||
| @@ -68,19 +71,19 @@ class NoteFlatTextExp extends Expression { | |||||||
|                 const title = utils.normalize(beccaService.getNoteTitle(note.noteId, parentNote.noteId)); |                 const title = utils.normalize(beccaService.getNoteTitle(note.noteId, parentNote.noteId)); | ||||||
|                 const foundTokens = foundAttrTokens.slice(); |                 const foundTokens = foundAttrTokens.slice(); | ||||||
|  |  | ||||||
|                 for (const token of tokens) { |                 for (const token of remainingTokens) { | ||||||
|                     if (title.includes(token)) { |                     if (title.includes(token)) { | ||||||
|                         foundTokens.push(token); |                         foundTokens.push(token); | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 if (foundTokens.length > 0) { |                 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 { |                 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) { |         for (const note of candidateNotes) { | ||||||
|             // autocomplete should be able to find notes by their noteIds as well (only leafs) |             // 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]) { |             if (this.tokens.length === 1 && note.noteId.toLowerCase() === this.tokens[0]) { | ||||||
|                 searchDownThePath(note, [], []); |                 searchPathTowardsRoot(note, [], [note.noteId]); | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
|  |  | ||||||
| @@ -123,7 +126,7 @@ class NoteFlatTextExp extends Expression { | |||||||
|                 if (foundTokens.length > 0) { |                 if (foundTokens.length > 0) { | ||||||
|                     const remainingTokens = this.tokens.filter(token => !foundTokens.includes(token)); |                     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; |         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(); |             return note.getBestNotePath(); | ||||||
|         } else { |         } else { | ||||||
|             const closestNoteId = path[0]; |             // this note is the closest to root containing the last matching token(s), thus completing the requirements | ||||||
|             const closestNoteBestNotePath = becca.getNote(closestNoteId).getBestNotePath(); |             // 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 beccaService = require('../../../becca/becca_service'); | ||||||
| const utils = require('../../utils'); | const utils = require('../../utils'); | ||||||
| const log = require('../../log'); | const log = require('../../log'); | ||||||
| const scriptService = require("../../script"); |  | ||||||
| const hoistedNoteService = require("../../hoisted_note"); | const hoistedNoteService = require("../../hoisted_note"); | ||||||
|  |  | ||||||
| function searchFromNote(note) { | function searchFromNote(note) { | ||||||
| @@ -73,6 +72,7 @@ function searchFromRelation(note, relationName) { | |||||||
|         return []; |         return []; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const scriptService = require("../../script"); // to avoid circular dependency | ||||||
|     const result = scriptService.executeNote(scriptNote, {originEntity: note}); |     const result = scriptService.executeNote(scriptNote, {originEntity: note}); | ||||||
|  |  | ||||||
|     if (!Array.isArray(result)) { |     if (!Array.isArray(result)) { | ||||||
|   | |||||||
| @@ -13,7 +13,7 @@ class TaskContext { | |||||||
|         this.noteDeletionHandlerTriggered = false; |         this.noteDeletionHandlerTriggered = false; | ||||||
|  |  | ||||||
|         // progressCount is meant to represent just some progress - to indicate the task is not stuck |         // 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 |         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 |         // 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>From the Trilium Menu, click Options.</li> | ||||||
|                 <li>Click on Sync tab.</li> |                 <li>Click on Sync tab.</li> | ||||||
|                 <li>Change server instance address to: <span id="current-host"></span> and click save.</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> |                 <li>Once you've completed these steps, click <a href="/">here</a>.</li> | ||||||
|             </ol> |             </ol> | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user