diff --git a/bin/create-anonymization-script.js b/bin/create-anonymization-script.js old mode 100644 new mode 100755 diff --git a/bin/tpl/anonymize-database.sql b/bin/tpl/anonymize-database.sql index 3e8279eeb6..ff935d6e1d 100644 --- a/bin/tpl/anonymize-database.sql +++ b/bin/tpl/anonymize-database.sql @@ -1,165 +1,47 @@ - UPDATE etapi_tokens SET tokenHash = 'API token hash value'; -UPDATE notes SET title = 'title' WHERE noteId != 'root' AND noteId NOT LIKE '\_%' ESCAPE '\'; +UPDATE notes SET title = 'title' WHERE title NOT IN ('root', '_hidden', '_share'); UPDATE blobs SET content = 'text' WHERE content IS NOT NULL; UPDATE revisions SET title = 'title'; -UPDATE attributes SET name = 'name', value = 'value' - WHERE type = 'label' - AND name NOT IN ('inbox', - 'disableVersioning', - 'calendarRoot', - 'archived', - 'excludeFromExport', - 'disableInclusion', - 'appCss', - 'appTheme', - 'hidePromotedAttributes', - 'readOnly', - 'autoReadOnlyDisabled', - 'cssClass', - 'iconClass', - 'keyboardShortcut', - 'run', - 'runOnInstance', - 'runAtHour', - 'customRequestHandler', - 'customResourceProvider', - 'widget', - 'noteInfoWidgetDisabled', - 'linkMapWidgetDisabled', - 'revisionsWidgetDisabled', - 'whatLinksHereWidgetDisabled', - 'similarNotesWidgetDisabled', - 'workspace', - 'workspaceIconClass', - 'workspaceTabBackgroundColor', - 'searchHome', - 'workspaceInbox', - 'workspaceSearchHome', - 'sqlConsoleHome', - 'datePattern', - 'pageSize', - 'viewType', - 'mapRootNoteId', - 'bookmarkFolder', - 'sorted', - 'top', - 'fullContentWidth', - 'shareHiddenFromTree', - 'shareAlias', - 'shareOmitDefaultCss', - 'shareRoot', - 'internalLink', - 'imageLink', - 'relationMapLink', - 'includeMapLink', - 'runOnNoteCreation', - 'runOnNoteTitleChange', - 'runOnNoteContentChange', - 'runOnNoteChange', - 'runOnChildNoteCreation', - 'runOnAttributeCreation', - 'runOnAttributeChange', - 'template', - 'inherit', - 'widget', - 'renderNote', - 'shareCss', - 'shareJs', - 'shareFavicon', - 'executeButton', - 'keepCurrentHoisting', - 'color', - 'toc', - 'excludeFromNoteMap', - 'docName', - 'launcherType', - 'builtinWidget', - 'baseSize', - 'growthFactor' - ); - -UPDATE attributes SET name = 'name' - AND name NOT IN ('inbox', - 'disableVersioning', - 'calendarRoot', - 'archived', - 'excludeFromExport', - 'disableInclusion', - 'appCss', - 'appTheme', - 'hidePromotedAttributes', - 'readOnly', - 'autoReadOnlyDisabled', - 'cssClass', - 'iconClass', - 'keyboardShortcut', - 'run', - 'runOnInstance', - 'runAtHour', - 'customRequestHandler', - 'customResourceProvider', - 'widget', - 'noteInfoWidgetDisabled', - 'linkMapWidgetDisabled', - 'revisionsWidgetDisabled', - 'whatLinksHereWidgetDisabled', - 'similarNotesWidgetDisabled', - 'workspace', - 'workspaceIconClass', - 'workspaceTabBackgroundColor', - 'searchHome', - 'workspaceInbox', - 'workspaceSearchHome', - 'sqlConsoleHome', - 'datePattern', - 'pageSize', - 'viewType', - 'mapRootNoteId', - 'bookmarkFolder', - 'sorted', - 'top', - 'fullContentWidth', - 'shareHiddenFromTree', - 'shareAlias', - 'shareOmitDefaultCss', - 'shareRoot', - 'internalLink', - 'imageLink', - 'relationMapLink', - 'includeMapLink', - 'runOnNoteCreation', - 'runOnNoteTitleChange', - 'runOnNoteContentChange', - 'runOnNoteChange', - 'runOnChildNoteCreation', - 'runOnAttributeCreation', - 'runOnAttributeChange', - 'template', - 'inherit', - 'widget', - 'renderNote', - 'shareCss', - 'shareJs', - 'shareFavicon', - 'executeButton', - 'keepCurrentHoisting', - 'color', - 'toc', - 'excludeFromNoteMap', - 'docName', - 'launcherType', - 'builtinWidget', - 'baseSize', - 'growthFactor' - ); - +UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label' + AND name NOT IN + ('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss', + 'appTheme', 'hidePromotedAttributes', 'readOnly', 'autoReadOnlyDisabled', 'cssClass', 'iconClass', + 'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider', + 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'revisionsWidgetDisabled', + 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', + 'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox', + 'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', + 'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top', + 'fullContentWidth', 'shareHiddenFromTree', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription', + 'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate', + 'template', 'toc', 'color', 'keepCurrentHoisting', 'executeButton', 'executeDescription', 'newNotesOnTop', + 'clipperInbox', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation', + 'runOnNoteTitleChange', 'runOnNoteChange', 'runOnNoteContentChange', 'runOnNoteDeletion', 'runOnBranchCreation', + 'runOnBranchDeletion', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template', + 'inherit', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon'); +UPDATE attributes SET name = 'name' WHERE type = 'relation' + AND name NOT IN + ('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss', + 'appTheme', 'hidePromotedAttributes', 'readOnly', 'autoReadOnlyDisabled', 'cssClass', 'iconClass', + 'keyboardShortcut', 'run', 'runOnInstance', 'runAtHour', 'customRequestHandler', 'customResourceProvider', + 'widget', 'noteInfoWidgetDisabled', 'linkMapWidgetDisabled', 'revisionsWidgetDisabled', + 'whatLinksHereWidgetDisabled', 'similarNotesWidgetDisabled', 'workspace', 'workspaceIconClass', + 'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox', + 'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId', + 'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top', + 'fullContentWidth', 'shareHiddenFromTree', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription', + 'shareRaw', 'shareDisallowRobotIndexing', 'shareIndex', 'displayRelations', 'hideRelations', 'titleTemplate', + 'template', 'toc', 'color', 'keepCurrentHoisting', 'executeButton', 'executeDescription', 'newNotesOnTop', + 'clipperInbox', 'internalLink', 'imageLink', 'relationMapLink', 'includeMapLink', 'runOnNoteCreation', + 'runOnNoteTitleChange', 'runOnNoteChange', 'runOnNoteContentChange', 'runOnNoteDeletion', 'runOnBranchCreation', + 'runOnBranchDeletion', 'runOnChildNoteCreation', 'runOnAttributeCreation', 'runOnAttributeChange', 'template', + 'inherit', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareFavicon'); UPDATE branches SET prefix = 'prefix' WHERE prefix IS NOT NULL AND prefix != 'recovered'; UPDATE options SET value = 'anonymized' WHERE name IN - ('documentId', 'documentSecret', 'encryptedDataKey', - 'passwordVerificationHash', 'passwordVerificationSalt', - 'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy') - AND value != ''; + ('documentId', 'documentSecret', 'encryptedDataKey', + 'passwordVerificationHash', 'passwordVerificationSalt', + 'passwordDerivedKeySalt', 'username', 'syncServerHost', 'syncProxy') + AND value != ''; VACUUM; diff --git a/db/migrations/0226__rename_noteSize_label.sql b/db/migrations/0226__rename_noteSize_label.sql new file mode 100644 index 0000000000..cd2239af49 --- /dev/null +++ b/db/migrations/0226__rename_noteSize_label.sql @@ -0,0 +1 @@ +UPDATE attributes SET value = 'contentAndAttachmentsAndRevisionsSize' WHERE name = 'orderBy' AND value = 'noteSize'; diff --git a/docker-compose.yml b/docker-compose.yml index ccfff054f3..6798574acb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,5 @@ +# Running `docker-compose up` will create/use the "trilium-data" directory in the user home +# Run `TRILIUM_DATA_DIR=/path/of/your/choice docker-compose up` to set a different directory version: '2.1' services: trilium: @@ -8,7 +10,7 @@ services: ports: - "8080:8080" volumes: - - trilium:/home/node/trilium-data + - ${TRILIUM_DATA_DIR:-~/trilium-data}:/home/node/trilium-data volumes: trilium: diff --git a/libraries/codemirror/addon/lint/eslint.js b/libraries/codemirror/addon/lint/eslint.js index 1751df1d5a..ce6e226a79 100644 --- a/libraries/codemirror/addon/lint/eslint.js +++ b/libraries/codemirror/addon/lint/eslint.js @@ -46,7 +46,7 @@ const errors = new eslint().verify(text, { root: true, parserOptions: { - ecmaVersion: 2022 + ecmaVersion: "latest" }, extends: ['eslint:recommended', 'airbnb-base'], env: { diff --git a/libraries/codemirror/addon/lint/lint.js b/libraries/codemirror/addon/lint/lint.js index 7b40e10e91..21631b9d24 100644 --- a/libraries/codemirror/addon/lint/lint.js +++ b/libraries/codemirror/addon/lint/lint.js @@ -24,8 +24,10 @@ function position(e) { if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position); - tt.style.top = Math.max(0, e.clientY - tt.offsetHeight - 5) + "px"; - tt.style.left = (e.clientX + 5) + "px"; + var top = Math.max(0, e.clientY - tt.offsetHeight - 5); + var left = Math.max(0, Math.min(e.clientX + 5, tt.ownerDocument.defaultView.innerWidth - tt.offsetWidth)); + tt.style.top = top + "px" + tt.style.left = left + "px"; } CodeMirror.on(document, "mousemove", position); position(e); @@ -199,10 +201,6 @@ var anns = annotations[line]; if (!anns) continue; - // filter out duplicate messages - var message = []; - anns = anns.filter(function(item) { return message.indexOf(item.message) > -1 ? false : message.push(item.message) }); - var maxSeverity = null; var tipLabel = state.hasGutter && document.createDocumentFragment(); @@ -220,9 +218,8 @@ __annotation: ann })); } - // use original annotations[line] to show multiple messages if (state.hasGutter) - cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, annotations[line].length > 1, + cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, anns.length > 1, options.tooltips)); if (options.highlightLines) diff --git a/libraries/codemirror/addon/mode/multiplex.js b/libraries/codemirror/addon/mode/multiplex.js new file mode 100644 index 0000000000..d7f378af63 --- /dev/null +++ b/libraries/codemirror/addon/mode/multiplex.js @@ -0,0 +1,136 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/5/LICENSE + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.multiplexingMode = function(outer /*, others */) { + // Others should be {open, close, mode [, delimStyle] [, innerStyle] [, parseDelimiters]} objects + var others = Array.prototype.slice.call(arguments, 1); + + function indexOf(string, pattern, from, returnEnd) { + if (typeof pattern == "string") { + var found = string.indexOf(pattern, from); + return returnEnd && found > -1 ? found + pattern.length : found; + } + var m = pattern.exec(from ? string.slice(from) : string); + return m ? m.index + from + (returnEnd ? m[0].length : 0) : -1; + } + + return { + startState: function() { + return { + outer: CodeMirror.startState(outer), + innerActive: null, + inner: null, + startingInner: false + }; + }, + + copyState: function(state) { + return { + outer: CodeMirror.copyState(outer, state.outer), + innerActive: state.innerActive, + inner: state.innerActive && CodeMirror.copyState(state.innerActive.mode, state.inner), + startingInner: state.startingInner + }; + }, + + token: function(stream, state) { + if (!state.innerActive) { + var cutOff = Infinity, oldContent = stream.string; + for (var i = 0; i < others.length; ++i) { + var other = others[i]; + var found = indexOf(oldContent, other.open, stream.pos); + if (found == stream.pos) { + if (!other.parseDelimiters) stream.match(other.open); + state.startingInner = !!other.parseDelimiters + state.innerActive = other; + + // Get the outer indent, making sure to handle CodeMirror.Pass + var outerIndent = 0; + if (outer.indent) { + var possibleOuterIndent = outer.indent(state.outer, "", ""); + if (possibleOuterIndent !== CodeMirror.Pass) outerIndent = possibleOuterIndent; + } + + state.inner = CodeMirror.startState(other.mode, outerIndent); + return other.delimStyle && (other.delimStyle + " " + other.delimStyle + "-open"); + } else if (found != -1 && found < cutOff) { + cutOff = found; + } + } + if (cutOff != Infinity) stream.string = oldContent.slice(0, cutOff); + var outerToken = outer.token(stream, state.outer); + if (cutOff != Infinity) stream.string = oldContent; + return outerToken; + } else { + var curInner = state.innerActive, oldContent = stream.string; + if (!curInner.close && stream.sol()) { + state.innerActive = state.inner = null; + return this.token(stream, state); + } + var found = curInner.close && !state.startingInner ? + indexOf(oldContent, curInner.close, stream.pos, curInner.parseDelimiters) : -1; + if (found == stream.pos && !curInner.parseDelimiters) { + stream.match(curInner.close); + state.innerActive = state.inner = null; + return curInner.delimStyle && (curInner.delimStyle + " " + curInner.delimStyle + "-close"); + } + if (found > -1) stream.string = oldContent.slice(0, found); + var innerToken = curInner.mode.token(stream, state.inner); + if (found > -1) stream.string = oldContent; + else if (stream.pos > stream.start) state.startingInner = false + + if (found == stream.pos && curInner.parseDelimiters) + state.innerActive = state.inner = null; + + if (curInner.innerStyle) { + if (innerToken) innerToken = innerToken + " " + curInner.innerStyle; + else innerToken = curInner.innerStyle; + } + + return innerToken; + } + }, + + indent: function(state, textAfter, line) { + var mode = state.innerActive ? state.innerActive.mode : outer; + if (!mode.indent) return CodeMirror.Pass; + return mode.indent(state.innerActive ? state.inner : state.outer, textAfter, line); + }, + + blankLine: function(state) { + var mode = state.innerActive ? state.innerActive.mode : outer; + if (mode.blankLine) { + mode.blankLine(state.innerActive ? state.inner : state.outer); + } + if (!state.innerActive) { + for (var i = 0; i < others.length; ++i) { + var other = others[i]; + if (other.open === "\n") { + state.innerActive = other; + state.inner = CodeMirror.startState(other.mode, mode.indent ? mode.indent(state.outer, "", "") : 0); + } + } + } else if (state.innerActive.close === "\n") { + state.innerActive = state.inner = null; + } + }, + + electricChars: outer.electricChars, + + innerMode: function(state) { + return state.inner ? {state: state.inner, mode: state.innerActive.mode} : {state: state.outer, mode: outer}; + } + }; +}; + +}); diff --git a/libraries/codemirror/addon/mode/overlay.js b/libraries/codemirror/addon/mode/overlay.js new file mode 100644 index 0000000000..1aab1595ee --- /dev/null +++ b/libraries/codemirror/addon/mode/overlay.js @@ -0,0 +1,90 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/5/LICENSE + +// Utility function that allows modes to be combined. The mode given +// as the base argument takes care of most of the normal mode +// functionality, but a second (typically simple) mode is used, which +// can override the style of text. Both modes get to parse all of the +// text, but when both assign a non-null style to a piece of code, the +// overlay wins, unless the combine argument was true and not overridden, +// or state.overlay.combineTokens was true, in which case the styles are +// combined. + +(function(mod) { + if (typeof exports == "object" && typeof module == "object") // CommonJS + mod(require("../../lib/codemirror")); + else if (typeof define == "function" && define.amd) // AMD + define(["../../lib/codemirror"], mod); + else // Plain browser env + mod(CodeMirror); +})(function(CodeMirror) { +"use strict"; + +CodeMirror.overlayMode = function(base, overlay, combine) { + return { + startState: function() { + return { + base: CodeMirror.startState(base), + overlay: CodeMirror.startState(overlay), + basePos: 0, baseCur: null, + overlayPos: 0, overlayCur: null, + streamSeen: null + }; + }, + copyState: function(state) { + return { + base: CodeMirror.copyState(base, state.base), + overlay: CodeMirror.copyState(overlay, state.overlay), + basePos: state.basePos, baseCur: null, + overlayPos: state.overlayPos, overlayCur: null + }; + }, + + token: function(stream, state) { + if (stream != state.streamSeen || + Math.min(state.basePos, state.overlayPos) < stream.start) { + state.streamSeen = stream; + state.basePos = state.overlayPos = stream.start; + } + + if (stream.start == state.basePos) { + state.baseCur = base.token(stream, state.base); + state.basePos = stream.pos; + } + if (stream.start == state.overlayPos) { + stream.pos = stream.start; + state.overlayCur = overlay.token(stream, state.overlay); + state.overlayPos = stream.pos; + } + stream.pos = Math.min(state.basePos, state.overlayPos); + + // state.overlay.combineTokens always takes precedence over combine, + // unless set to null + if (state.overlayCur == null) return state.baseCur; + else if (state.baseCur != null && + state.overlay.combineTokens || + combine && state.overlay.combineTokens == null) + return state.baseCur + " " + state.overlayCur; + else return state.overlayCur; + }, + + indent: base.indent && function(state, textAfter, line) { + return base.indent(state.base, textAfter, line); + }, + electricChars: base.electricChars, + + innerMode: function(state) { return {state: state.base, mode: base}; }, + + blankLine: function(state) { + var baseToken, overlayToken; + if (base.blankLine) baseToken = base.blankLine(state.base); + if (overlay.blankLine) overlayToken = overlay.blankLine(state.overlay); + + return overlayToken == null ? + baseToken : + (combine && baseToken != null ? baseToken + " " + overlayToken : overlayToken); + } + }; +}; + +}); diff --git a/libraries/codemirror/codemirror.js b/libraries/codemirror/codemirror.js index 5ca96da451..156fc307dd 100644 --- a/libraries/codemirror/codemirror.js +++ b/libraries/codemirror/codemirror.js @@ -8259,8 +8259,8 @@ } function disableBrowserMagic(field, spellcheck, autocorrect, autocapitalize) { - field.setAttribute("autocorrect", autocorrect ? "" : "off"); - field.setAttribute("autocapitalize", autocapitalize ? "" : "off"); + field.setAttribute("autocorrect", autocorrect ? "on" : "off"); + field.setAttribute("autocapitalize", autocapitalize ? "on" : "off"); field.setAttribute("spellcheck", !!spellcheck); } @@ -8275,7 +8275,6 @@ else { te.setAttribute("wrap", "off"); } // If border: 0; -- iOS fails to open keyboard (issue #1287) if (ios) { te.style.border = "1px solid black"; } - disableBrowserMagic(te); return div } @@ -8897,6 +8896,7 @@ } // Old-fashioned briefly-focus-a-textarea hack var kludge = hiddenTextarea(), te = kludge.firstChild; + disableBrowserMagic(te); cm.display.lineSpace.insertBefore(kludge, cm.display.lineSpace.firstChild); te.value = lastCopied.text.join("\n"); var hadFocus = activeElt(div.ownerDocument); @@ -9461,6 +9461,8 @@ // The semihidden textarea that is focused when the editor is // focused, and receives input. this.textarea = this.wrapper.firstChild; + var opts = this.cm.options; + disableBrowserMagic(this.textarea, opts.spellcheck, opts.autocorrect, opts.autocapitalize); }; TextareaInput.prototype.screenReaderLabelChanged = function (label) { @@ -9865,7 +9867,7 @@ addLegacyProps(CodeMirror); - CodeMirror.version = "5.65.9"; + CodeMirror.version = "5.65.15"; return CodeMirror; diff --git a/libraries/codemirror/mode/clike/clike.js b/libraries/codemirror/mode/clike/clike.js index 748909efeb..e9f441fc0a 100644 --- a/libraries/codemirror/mode/clike/clike.js +++ b/libraries/codemirror/mode/clike/clike.js @@ -218,7 +218,8 @@ CodeMirror.defineMode("clike", function(config, parserConfig) { }, indent: function(state, textAfter) { - if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine) return CodeMirror.Pass; + if (state.tokenize != tokenBase && state.tokenize != null || state.typeAtEndOfLine && isTopScope(state.context)) + return CodeMirror.Pass; var ctx = state.context, firstChar = textAfter && textAfter.charAt(0); var closing = firstChar == ctx.type; if (ctx.type == "statement" && firstChar == "}") ctx = ctx.prev; @@ -512,8 +513,8 @@ CodeMirror.defineMode("clike", function(config, parserConfig) { name: "clike", keywords: words("abstract as async await base break case catch checked class const continue" + " default delegate do else enum event explicit extern finally fixed for" + - " foreach goto if implicit in interface internal is lock namespace new" + - " operator out override params private protected public readonly ref return sealed" + + " foreach goto if implicit in init interface internal is lock namespace new" + + " operator out override params private protected public readonly record ref required return sealed" + " sizeof stackalloc static struct switch this throw try typeof unchecked" + " unsafe using virtual void volatile while add alias ascending descending dynamic from get" + " global group into join let orderby partial remove select set value var yield"), @@ -522,7 +523,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) { " UInt64 bool byte char decimal double short int long object" + " sbyte float string ushort uint ulong"), blockKeywords: words("catch class do else finally for foreach if struct switch try while"), - defKeywords: words("class interface namespace struct var"), + defKeywords: words("class interface namespace record struct var"), typeFirstDefinitions: true, atoms: words("true false null"), hooks: { @@ -613,6 +614,7 @@ CodeMirror.defineMode("clike", function(config, parserConfig) { return state.tokenize(stream, state); }, "'": function(stream) { + if (stream.match(/^(\\[^'\s]+|[^\\'])'/)) return "string-2" stream.eatWhile(/[\w\$_\xa1-\uffff]/); return "atom"; }, diff --git a/libraries/codemirror/mode/dart/dart.js b/libraries/codemirror/mode/dart/dart.js index 340076712c..f81e4f91a4 100644 --- a/libraries/codemirror/mode/dart/dart.js +++ b/libraries/codemirror/mode/dart/dart.js @@ -12,10 +12,10 @@ "use strict"; var keywords = ("this super static final const abstract class extends external factory " + - "implements mixin get native set typedef with enum throw rethrow " + - "assert break case continue default in return new deferred async await covariant " + - "try catch finally do else for if switch while import library export " + - "part of show hide is as extension on yield late required").split(" "); + "implements mixin get native set typedef with enum throw rethrow assert break case " + + "continue default in return new deferred async await covariant try catch finally " + + "do else for if switch while import library export part of show hide is as extension " + + "on yield late required sealed base interface when inline").split(" "); var blockKeywords = "try catch finally do else for if switch while".split(" "); var atoms = "true false null".split(" "); var builtins = "void bool num int double dynamic var String Null Never".split(" "); diff --git a/libraries/codemirror/mode/javascript/javascript.js b/libraries/codemirror/mode/javascript/javascript.js index 48a46d65d0..bb735ebc96 100644 --- a/libraries/codemirror/mode/javascript/javascript.js +++ b/libraries/codemirror/mode/javascript/javascript.js @@ -779,7 +779,7 @@ CodeMirror.defineMode("javascript", function(config, parserConfig) { if (type == "async" || (type == "variable" && (value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) && - cx.stream.match(/^\s+[\w$\xa1-\uffff]/, false))) { + cx.stream.match(/^\s+#?[\w$\xa1-\uffff]/, false))) { cx.marked = "keyword"; return cont(classBody); } diff --git a/libraries/codemirror/mode/nsis/nsis.js b/libraries/codemirror/mode/nsis/nsis.js index 2173916bb2..de18871251 100644 --- a/libraries/codemirror/mode/nsis/nsis.js +++ b/libraries/codemirror/mode/nsis/nsis.js @@ -24,7 +24,7 @@ CodeMirror.defineSimpleMode("nsis",{ { regex: /`(?:[^\\`]|\\.)*`?/, token: "string" }, // Compile Time Commands - {regex: /^\s*(?:\!(addincludedir|addplugindir|appendfile|cd|define|delfile|echo|error|execute|finalize|getdllversion|gettlbversion|include|insertmacro|macro|macroend|makensis|packhdr|pragma|searchparse|searchreplace|system|tempfile|undef|uninstfinalize|verbose|warning))\b/i, token: "keyword"}, + {regex: /^\s*(?:\!(addincludedir|addplugindir|appendfile|assert|cd|define|delfile|echo|error|execute|finalize|getdllversion|gettlbversion|include|insertmacro|macro|macroend|makensis|packhdr|pragma|searchparse|searchreplace|system|tempfile|undef|uninstfinalize|verbose|warning))\b/i, token: "keyword"}, // Conditional Compilation {regex: /^\s*(?:\!(if(?:n?def)?|ifmacron?def|macro))\b/i, token: "keyword", indent: true}, diff --git a/libraries/codemirror/mode/pegjs/pegjs.js b/libraries/codemirror/mode/pegjs/pegjs.js index c0ed2fcc06..c0012c5cf2 100644 --- a/libraries/codemirror/mode/pegjs/pegjs.js +++ b/libraries/codemirror/mode/pegjs/pegjs.js @@ -31,8 +31,6 @@ CodeMirror.defineMode("pegjs", function (config) { }; }, token: function (stream, state) { - if (stream) - //check for state changes if (!state.inString && !state.inComment && ((stream.peek() == '"') || (stream.peek() == "'"))) { state.stringType = stream.peek(); @@ -43,7 +41,6 @@ CodeMirror.defineMode("pegjs", function (config) { state.inComment = true; } - //return state if (state.inString) { while (state.inString && !stream.eol()) { if (stream.peek() === state.stringType) { diff --git a/libraries/codemirror/mode/python/python.js b/libraries/codemirror/mode/python/python.js index d75b021e2c..3946ceeeb0 100644 --- a/libraries/codemirror/mode/python/python.js +++ b/libraries/codemirror/mode/python/python.js @@ -20,7 +20,7 @@ "def", "del", "elif", "else", "except", "finally", "for", "from", "global", "if", "import", "lambda", "pass", "raise", "return", - "try", "while", "with", "yield", "in"]; + "try", "while", "with", "yield", "in", "False", "True"]; var commonBuiltins = ["abs", "all", "any", "bin", "bool", "bytearray", "callable", "chr", "classmethod", "compile", "complex", "delattr", "dict", "dir", "divmod", "enumerate", "eval", "filter", "float", "format", "frozenset", @@ -60,7 +60,7 @@ if (py3) { // since http://legacy.python.org/dev/peps/pep-0465/ @ is also an operator var identifiers = parserConf.identifiers|| /^[_A-Za-z\u00A1-\uFFFF][_A-Za-z0-9\u00A1-\uFFFF]*/; - myKeywords = myKeywords.concat(["nonlocal", "False", "True", "None", "async", "await"]); + myKeywords = myKeywords.concat(["nonlocal", "None", "aiter", "anext", "async", "await", "breakpoint", "match", "case"]); myBuiltins = myBuiltins.concat(["ascii", "bytes", "exec", "print"]); var stringPrefixes = new RegExp("^(([rbuf]|(br)|(rb)|(fr)|(rf))?('{3}|\"{3}|['\"]))", "i"); } else { @@ -68,7 +68,7 @@ myKeywords = myKeywords.concat(["exec", "print"]); myBuiltins = myBuiltins.concat(["apply", "basestring", "buffer", "cmp", "coerce", "execfile", "file", "intern", "long", "raw_input", "reduce", "reload", - "unichr", "unicode", "xrange", "False", "True", "None"]); + "unichr", "unicode", "xrange", "None"]); var stringPrefixes = new RegExp("^(([rubf]|(ur)|(br))?('{3}|\"{3}|['\"]))", "i"); } var keywords = wordRegexp(myKeywords); diff --git a/libraries/codemirror/mode/sparql/sparql.js b/libraries/codemirror/mode/sparql/sparql.js index 5e68f5670d..6d928b5cde 100644 --- a/libraries/codemirror/mode/sparql/sparql.js +++ b/libraries/codemirror/mode/sparql/sparql.js @@ -33,6 +33,9 @@ CodeMirror.defineMode("sparql", function(config) { "true", "false", "with", "data", "copy", "to", "move", "add", "create", "drop", "clear", "load", "into"]); var operatorChars = /[*+\-<>=&|\^\/!\?]/; + var PN_CHARS = "[A-Za-z_\\-0-9]"; + var PREFIX_START = new RegExp("[A-Za-z]"); + var PREFIX_REMAINDER = new RegExp("((" + PN_CHARS + "|\\.)*(" + PN_CHARS + "))?:"); function tokenBase(stream, state) { var ch = stream.next(); @@ -71,20 +74,18 @@ CodeMirror.defineMode("sparql", function(config) { stream.eatWhile(/[a-z\d\-]/i); return "meta"; } - else { - stream.eatWhile(/[_\w\d]/); - if (stream.eat(":")) { + else if (PREFIX_START.test(ch) && stream.match(PREFIX_REMAINDER)) { eatPnLocal(stream); return "atom"; - } - var word = stream.current(); - if (ops.test(word)) - return "builtin"; - else if (keywords.test(word)) - return "keyword"; - else - return "variable"; } + stream.eatWhile(/[_\w\d]/); + var word = stream.current(); + if (ops.test(word)) + return "builtin"; + else if (keywords.test(word)) + return "keyword"; + else + return "variable"; } function eatPnLocal(stream) { diff --git a/libraries/codemirror/mode/sql/sql.js b/libraries/codemirror/mode/sql/sql.js index 105b22ffb9..d3983889f7 100644 --- a/libraries/codemirror/mode/sql/sql.js +++ b/libraries/codemirror/mode/sql/sql.js @@ -94,9 +94,7 @@ CodeMirror.defineMode("sql", function(config, parserConfig) { return "number"; if (stream.match(/^\.+/)) return null - // .table_name (ODBC) - // // ref: https://dev.mysql.com/doc/refman/8.0/en/identifier-qualifiers.html - if (support.ODBCdotTable && stream.match(/^[\w\d_$#]+/)) + if (stream.match(/^[\w\d_$#]+/)) return "variable-2"; } else if (operatorChars.test(ch)) { // operators @@ -207,7 +205,8 @@ CodeMirror.defineMode("sql", function(config, parserConfig) { blockCommentStart: "/*", blockCommentEnd: "*/", lineComment: support.commentSlashSlash ? "//" : support.commentHash ? "#" : "--", - closeBrackets: "()[]{}''\"\"``" + closeBrackets: "()[]{}''\"\"``", + config: parserConfig }; }); @@ -294,7 +293,7 @@ CodeMirror.defineMode("sql", function(config, parserConfig) { builtin: set(defaultBuiltin), atoms: set("false true null unknown"), dateSQL: set("date time timestamp"), - support: set("ODBCdotTable doubleQuote binaryNumber hexNumber") + support: set("doubleQuote binaryNumber hexNumber") }); CodeMirror.defineMIME("text/x-mssql", { @@ -321,7 +320,7 @@ CodeMirror.defineMode("sql", function(config, parserConfig) { atoms: set("false true null unknown"), operatorChars: /^[*+\-%<>!=&|^]/, dateSQL: set("date time timestamp"), - support: set("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber doubleQuote nCharCast charsetCast commentHash commentSpaceRequired"), + support: set("decimallessFloat zerolessFloat binaryNumber hexNumber doubleQuote nCharCast charsetCast commentHash commentSpaceRequired"), hooks: { "@": hookVar, "`": hookIdentifier, @@ -337,7 +336,7 @@ CodeMirror.defineMode("sql", function(config, parserConfig) { atoms: set("false true null unknown"), operatorChars: /^[*+\-%<>!=&|^]/, dateSQL: set("date time timestamp"), - support: set("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber doubleQuote nCharCast charsetCast commentHash commentSpaceRequired"), + support: set("decimallessFloat zerolessFloat binaryNumber hexNumber doubleQuote nCharCast charsetCast commentHash commentSpaceRequired"), hooks: { "@": hookVar, "`": hookIdentifier, @@ -408,7 +407,7 @@ CodeMirror.defineMode("sql", function(config, parserConfig) { atoms: set("false true null unknown"), operatorChars: /^[*+\-%<>!=]/, dateSQL: set("date timestamp"), - support: set("ODBCdotTable doubleQuote binaryNumber hexNumber") + support: set("doubleQuote binaryNumber hexNumber") }); CodeMirror.defineMIME("text/x-pgsql", { @@ -418,12 +417,12 @@ CodeMirror.defineMode("sql", function(config, parserConfig) { // For pl/pgsql lang - https://github.com/postgres/postgres/blob/REL_11_2/src/pl/plpgsql/src/pl_scanner.c keywords: set(sqlKeywords + "a abort abs absent absolute access according action ada add admin after aggregate alias all allocate also alter always analyse analyze and any are array array_agg array_max_cardinality as asc asensitive assert assertion assignment asymmetric at atomic attach attribute attributes authorization avg backward base64 before begin begin_frame begin_partition bernoulli between bigint binary bit bit_length blob blocked bom boolean both breadth by c cache call called cardinality cascade cascaded case cast catalog catalog_name ceil ceiling chain char char_length character character_length character_set_catalog character_set_name character_set_schema characteristics characters check checkpoint class class_origin clob close cluster coalesce cobol collate collation collation_catalog collation_name collation_schema collect column column_name columns command_function command_function_code comment comments commit committed concurrently condition condition_number configuration conflict connect connection connection_name constant constraint constraint_catalog constraint_name constraint_schema constraints constructor contains content continue control conversion convert copy corr corresponding cost count covar_pop covar_samp create cross csv cube cume_dist current current_catalog current_date current_default_transform_group current_path current_role current_row current_schema current_time current_timestamp current_transform_group_for_type current_user cursor cursor_name cycle data database datalink datatype date datetime_interval_code datetime_interval_precision day db deallocate debug dec decimal declare default defaults deferrable deferred defined definer degree delete delimiter delimiters dense_rank depends depth deref derived desc describe descriptor detach detail deterministic diagnostics dictionary disable discard disconnect dispatch distinct dlnewcopy dlpreviouscopy dlurlcomplete dlurlcompleteonly dlurlcompletewrite dlurlpath dlurlpathonly dlurlpathwrite dlurlscheme dlurlserver dlvalue do document domain double drop dump dynamic dynamic_function dynamic_function_code each element else elseif elsif empty enable encoding encrypted end end_frame end_partition endexec enforced enum equals errcode error escape event every except exception exclude excluding exclusive exec execute exists exit exp explain expression extension external extract false family fetch file filter final first first_value flag float floor following for force foreach foreign fortran forward found frame_row free freeze from fs full function functions fusion g general generated get global go goto grant granted greatest group grouping groups handler having header hex hierarchy hint hold hour id identity if ignore ilike immediate immediately immutable implementation implicit import in include including increment indent index indexes indicator info inherit inherits initially inline inner inout input insensitive insert instance instantiable instead int integer integrity intersect intersection interval into invoker is isnull isolation join k key key_member key_type label lag language large last last_value lateral lead leading leakproof least left length level library like like_regex limit link listen ln load local localtime localtimestamp location locator lock locked log logged loop lower m map mapping match matched materialized max max_cardinality maxvalue member merge message message_length message_octet_length message_text method min minute minvalue mod mode modifies module month more move multiset mumps name names namespace national natural nchar nclob nesting new next nfc nfd nfkc nfkd nil no none normalize normalized not nothing notice notify notnull nowait nth_value ntile null nullable nullif nulls number numeric object occurrences_regex octet_length octets of off offset oids old on only open operator option options or order ordering ordinality others out outer output over overlaps overlay overriding owned owner p pad parallel parameter parameter_mode parameter_name parameter_ordinal_position parameter_specific_catalog parameter_specific_name parameter_specific_schema parser partial partition pascal passing passthrough password path percent percent_rank percentile_cont percentile_disc perform period permission pg_context pg_datatype_name pg_exception_context pg_exception_detail pg_exception_hint placing plans pli policy portion position position_regex power precedes preceding precision prepare prepared preserve primary print_strict_params prior privileges procedural procedure procedures program public publication query quote raise range rank read reads real reassign recheck recovery recursive ref references referencing refresh regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy regr_syy reindex relative release rename repeatable replace replica requiring reset respect restart restore restrict result result_oid return returned_cardinality returned_length returned_octet_length returned_sqlstate returning returns reverse revoke right role rollback rollup routine routine_catalog routine_name routine_schema routines row row_count row_number rows rowtype rule savepoint scale schema schema_name schemas scope scope_catalog scope_name scope_schema scroll search second section security select selective self sensitive sequence sequences serializable server server_name session session_user set setof sets share show similar simple size skip slice smallint snapshot some source space specific specific_name specifictype sql sqlcode sqlerror sqlexception sqlstate sqlwarning sqrt stable stacked standalone start state statement static statistics stddev_pop stddev_samp stdin stdout storage strict strip structure style subclass_origin submultiset subscription substring substring_regex succeeds sum symmetric sysid system system_time system_user t table table_name tables tablesample tablespace temp template temporary text then ties time timestamp timezone_hour timezone_minute to token top_level_count trailing transaction transaction_active transactions_committed transactions_rolled_back transform transforms translate translate_regex translation treat trigger trigger_catalog trigger_name trigger_schema trim trim_array true truncate trusted type types uescape unbounded uncommitted under unencrypted union unique unknown unlink unlisten unlogged unnamed unnest until untyped update upper uri usage use_column use_variable user user_defined_type_catalog user_defined_type_code user_defined_type_name user_defined_type_schema using vacuum valid validate validator value value_of values var_pop var_samp varbinary varchar variable_conflict variadic varying verbose version versioning view views volatile warning when whenever where while whitespace width_bucket window with within without work wrapper write xml xmlagg xmlattributes xmlbinary xmlcast xmlcomment xmlconcat xmldeclaration xmldocument xmlelement xmlexists xmlforest xmliterate xmlnamespaces xmlparse xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltext xmlvalidate year yes zone"), // https://www.postgresql.org/docs/11/datatype.html - builtin: set("bigint int8 bigserial serial8 bit varying varbit boolean bool box bytea character char varchar cidr circle date double precision float8 inet integer int int4 interval json jsonb line lseg macaddr macaddr8 money numeric decimal path pg_lsn point polygon real float4 smallint int2 smallserial serial2 serial serial4 text time without zone with timetz timestamp timestamptz tsquery tsvector txid_snapshot uuid xml"), + builtin: set("bigint int8 bigserial serial8 bit varying varbit boolean bool box bytea character char varchar cidr circle date double precision float8 inet integer int int4 interval json jsonb line lseg macaddr macaddr8 money numeric decimal path pg_lsn point polygon real float4 smallint int2 smallserial serial2 serial serial4 text time zone timetz timestamp timestamptz tsquery tsvector txid_snapshot uuid xml"), atoms: set("false true null unknown"), operatorChars: /^[*\/+\-%<>!=&|^\/#@?~]/, backslashStringEscapes: false, dateSQL: set("date time timestamp"), - support: set("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber nCharCast charsetCast escapeConstant") + support: set("decimallessFloat zerolessFloat binaryNumber hexNumber nCharCast charsetCast escapeConstant") }); // Google's SQL-like query language, GQL @@ -445,7 +444,7 @@ CodeMirror.defineMode("sql", function(config, parserConfig) { atoms: set("false true null unknown"), operatorChars: /^[*+\-%<>!=&|^\/#@?~]/, dateSQL: set("date time timestamp"), - support: set("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber nCharCast charsetCast") + support: set("decimallessFloat zerolessFloat binaryNumber hexNumber nCharCast charsetCast") }); // Spark SQL @@ -456,7 +455,7 @@ CodeMirror.defineMode("sql", function(config, parserConfig) { atoms: set("false true null"), operatorChars: /^[*\/+\-%<>!=~&|^]/, dateSQL: set("date time timestamp"), - support: set("ODBCdotTable doubleQuote zerolessFloat") + support: set("doubleQuote zerolessFloat") }); // Esper @@ -489,7 +488,7 @@ CodeMirror.defineMode("sql", function(config, parserConfig) { dateSQL: set("date time timestamp zone"), // hexNumber is necessary for VARBINARY literals, e.g. X'65683F' // but it also enables 0xFF hex numbers, which Trino doesn't support. - support: set("ODBCdotTable decimallessFloat zerolessFloat hexNumber") + support: set("decimallessFloat zerolessFloat hexNumber") }); }); @@ -507,7 +506,6 @@ CodeMirror.defineMode("sql", function(config, parserConfig) { Commands parsed and executed by the client (not the server). support: A list of supported syntaxes which are not common, but are supported by more than 1 DBMS. - * ODBCdotTable: .tableName * zerolessFloat: .1 * decimallessFloat: 1. * hexNumber: X'01AF' X'01af' x'01AF' x'01af' 0x01AF 0x01af diff --git a/libraries/codemirror/mode/yaml/yaml.js b/libraries/codemirror/mode/yaml/yaml.js index 298db55f6f..895d1330a2 100644 --- a/libraries/codemirror/mode/yaml/yaml.js +++ b/libraries/codemirror/mode/yaml/yaml.js @@ -85,7 +85,7 @@ CodeMirror.defineMode("yaml", function() { } /* pairs (associative arrays) -> key */ - if (!state.pair && stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)) { + if (!state.pair && stream.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^\s,\[\]{}#&*!|>'"%@`])[^#:]*(?=:($|\s))/)) { state.pair = true; state.keyCol = stream.indentation(); return "atom"; diff --git a/package.json b/package.json index 32480889f8..63bd9987d6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "trilium", "productName": "Trilium Notes", "description": "Trilium Notes", - "version": "0.61.7-beta", + "version": "0.61.8-beta", "license": "AGPL-3.0-only", "main": "electron.js", "bin": { @@ -36,7 +36,7 @@ "@excalidraw/excalidraw": "0.16.1", "archiver": "5.3.1", "async-mutex": "0.4.0", - "axios": "1.5.0", + "axios": "1.5.1", "better-sqlite3": "8.4.0", "chokidar": "3.5.3", "cls-hooked": "4.2.2", @@ -68,7 +68,7 @@ "jimp": "0.22.10", "joplin-turndown-plugin-gfm": "1.0.12", "jsdom": "22.1.0", - "marked": "9.0.3", + "marked": "9.1.0", "mime-types": "2.1.35", "multer": "1.4.5-lts.1", "node-abi": "3.47.0", @@ -78,11 +78,11 @@ "react": "18.2.0", "react-dom": "18.2.0", "request": "2.88.2", - "rimraf": "5.0.1", + "rimraf": "5.0.5", "safe-compare": "1.1.4", "sanitize-filename": "1.6.3", "sanitize-html": "2.11.0", - "sax": "1.2.4", + "sax": "1.3.0", "semver": "7.5.4", "serve-favicon": "2.5.0", "session-file-store": "1.5.0", @@ -97,11 +97,11 @@ }, "devDependencies": { "cross-env": "7.0.3", - "electron": "25.8.2", + "electron": "25.9.0", "electron-builder": "24.6.4", "electron-packager": "17.1.2", "electron-rebuild": "3.2.9", - "eslint": "8.49.0", + "eslint": "8.50.0", "eslint-config-airbnb-base": "15.0.0", "eslint-config-prettier": "9.0.0", "eslint-plugin-import": "2.28.1", diff --git a/src/becca/becca.js b/src/becca/becca.js index 8b1a3fcb97..4b08b7706a 100644 --- a/src/becca/becca.js +++ b/src/becca/becca.js @@ -208,6 +208,7 @@ class Becca { return this.etapiTokens[etapiTokenId]; } + /** @returns {AbstractBeccaEntity|null} */ getEntity(entityName, entityId) { if (!entityName || !entityId) { return null; diff --git a/src/becca/becca_loader.js b/src/becca/becca_loader.js index e1518e8084..8c86b724e8 100644 --- a/src/becca/becca_loader.js +++ b/src/becca/becca_loader.js @@ -63,10 +63,10 @@ function load() { log.info(`Becca (note cache) load took ${Date.now() - start}ms`); } -function reload() { +function reload(reason) { load(); - require('../services/ws').reloadFrontend(); + require('../services/ws').reloadFrontend(reason || "becca reloaded"); } eventService.subscribeBeccaLoader([eventService.ENTITY_CHANGE_SYNCED], ({entityName, entityRow}) => { diff --git a/src/becca/entities/abstract_becca_entity.js b/src/becca/entities/abstract_becca_entity.js index cb539ef5d6..9448ebb50a 100644 --- a/src/becca/entities/abstract_becca_entity.js +++ b/src/becca/entities/abstract_becca_entity.js @@ -18,31 +18,11 @@ let becca = null; class AbstractBeccaEntity { /** @protected */ beforeSaving() { - this.generateIdIfNecessary(); - } - - /** @protected */ - generateIdIfNecessary() { if (!this[this.constructor.primaryKeyName]) { this[this.constructor.primaryKeyName] = utils.newEntityId(); } } - /** @protected */ - generateHash(isDeleted = false) { - let contentToHash = ""; - - for (const propertyName of this.constructor.hashedProperties) { - contentToHash += `|${this[propertyName]}`; - } - - if (isDeleted) { - contentToHash += "|deleted"; - } - - return utils.hash(contentToHash).substr(0, 10); - } - /** @protected */ getUtcDateChanged() { return this.utcDateModified || this.utcDateCreated; @@ -61,7 +41,7 @@ class AbstractBeccaEntity { } /** @protected */ - putEntityChange(isDeleted = false) { + putEntityChange(isDeleted) { entityChangesService.putEntityChange({ entityName: this.constructor.entityName, entityId: this[this.constructor.primaryKeyName], @@ -72,11 +52,37 @@ class AbstractBeccaEntity { }); } + /** + * @protected + * @returns {string} + */ + generateHash(isDeleted) { + let contentToHash = ""; + + for (const propertyName of this.constructor.hashedProperties) { + contentToHash += `|${this[propertyName]}`; + } + + if (isDeleted) { + contentToHash += "|deleted"; + } + + return utils.hash(contentToHash).substr(0, 10); + } + /** @protected */ getPojoToSave() { return this.getPojo(); } + /** + * @protected + * @abstract + */ + getPojo() { + throw new Error(`Unimplemented getPojo() for entity '${this.constructor.name}'`) + } + /** * Saves entity - executes SQL, but doesn't commit the transaction on its own * @@ -88,9 +94,7 @@ class AbstractBeccaEntity { const isNewEntity = !this[primaryKeyName]; - if (this.beforeSaving) { - this.beforeSaving(opts); - } + this.beforeSaving(opts); const pojo = this.getPojoToSave(); @@ -101,7 +105,7 @@ class AbstractBeccaEntity { return; } - this.putEntityChange(false); + this.putEntityChange(!!this.isDeleted); if (!cls.isEntityEventsDisabled()) { const eventPayload = { diff --git a/src/becca/entities/battachment.js b/src/becca/entities/battachment.js index b05cdddf15..6e6e2efca4 100644 --- a/src/becca/entities/battachment.js +++ b/src/becca/entities/battachment.js @@ -20,8 +20,7 @@ const attachmentRoleToNoteTypeMapping = { class BAttachment extends AbstractBeccaEntity { static get entityName() { return "attachments"; } static get primaryKeyName() { return "attachmentId"; } - static get hashedProperties() { return ["attachmentId", "ownerId", "role", "mime", "title", "blobId", - "utcDateScheduledForErasureSince", "utcDateModified"]; } + static get hashedProperties() { return ["attachmentId", "ownerId", "role", "mime", "title", "blobId", "utcDateScheduledForErasureSince"]; } constructor(row) { super(); diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index 5725033adf..02ee587327 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -150,11 +150,17 @@ class BNote extends AbstractBeccaEntity { */ this.contentSize = null; /** - * size of the content and note revision contents in bytes + * size of the note content, attachment contents in bytes * @type {int|null} * @private */ - this.noteSize = null; + this.contentAndAttachmentsSize = null; + /** + * size of the note content, attachment contents and revision contents in bytes + * @type {int|null} + * @private + */ + this.contentAndAttachmentsAndRevisionsSize = null; /** * number of note revisions for this note * @type {int|null} @@ -1625,16 +1631,12 @@ class BNote extends AbstractBeccaEntity { revision.save(); // to generate revisionId, which is then used to save attachments - if (this.type === 'text') { - for (const noteAttachment of this.getAttachments()) { - if (noteAttachment.utcDateScheduledForErasureSince) { - continue; - } - - const revisionAttachment = noteAttachment.copy(); - revisionAttachment.ownerId = revision.revisionId; - revisionAttachment.setContent(noteAttachment.getContent(), {forceSave: true}); + for (const noteAttachment of this.getAttachments()) { + const revisionAttachment = noteAttachment.copy(); + revisionAttachment.ownerId = revision.revisionId; + revisionAttachment.setContent(noteAttachment.getContent(), {forceSave: true}); + if (this.type === 'text') { // content is rewritten to point to the revision attachments noteContent = noteContent.replaceAll(`attachments/${noteAttachment.attachmentId}`, `attachments/${revisionAttachment.attachmentId}`); diff --git a/src/becca/entities/brevision.js b/src/becca/entities/brevision.js index 2e0f1f33ea..09a78db643 100644 --- a/src/becca/entities/brevision.js +++ b/src/becca/entities/brevision.js @@ -86,6 +86,29 @@ class BRevision extends AbstractBeccaEntity { return this._getContent(); } + /** + * @returns {*} + * @throws Error in case of invalid JSON */ + getJsonContent() { + const content = this.getContent(); + + if (!content || !content.trim()) { + return null; + } + + return JSON.parse(content); + } + + /** @returns {*|null} valid object or null if the content cannot be parsed as JSON */ + getJsonContentSafely() { + try { + return this.getJsonContent(); + } + catch (e) { + return null; + } + } + /** * @param content * @param {object} [opts] @@ -105,6 +128,45 @@ class BRevision extends AbstractBeccaEntity { .map(row => new BAttachment(row)); } + /** @returns {BAttachment|null} */ + getAttachmentById(attachmentId, opts = {}) { + opts.includeContentLength = !!opts.includeContentLength; + + const query = opts.includeContentLength + ? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength + FROM attachments + JOIN blobs USING (blobId) + WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0` + : `SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`; + + return sql.getRows(query, [this.revisionId, attachmentId]) + .map(row => new BAttachment(row))[0]; + } + + /** @returns {BAttachment[]} */ + getAttachmentsByRole(role) { + return sql.getRows(` + SELECT attachments.* + FROM attachments + WHERE ownerId = ? + AND role = ? + AND isDeleted = 0 + ORDER BY position`, [this.revisionId, role]) + .map(row => new BAttachment(row)); + } + + /** @returns {BAttachment} */ + getAttachmentByTitle(title) { + return sql.getRows(` + SELECT attachments.* + FROM attachments + WHERE ownerId = ? + AND title = ? + AND isDeleted = 0 + ORDER BY position`, [this.revisionId, title]) + .map(row => new BAttachment(row))[0]; + } + beforeSaving() { super.beforeSaving(); diff --git a/src/etapi/etapi.openapi.yaml b/src/etapi/etapi.openapi.yaml index a8bb6a8d8d..09149397c6 100644 --- a/src/etapi/etapi.openapi.yaml +++ b/src/etapi/etapi.openapi.yaml @@ -127,7 +127,8 @@ paths: - targetRelationCount - targetRelationCountIncludingLinks - contentSize - - noteSize + - contentAndAttachmentsSize + - contentAndAttachmentsAndRevisionsSize - revisionCount - name: orderDirection in: query diff --git a/src/public/app/menus/launcher_context_menu.js b/src/public/app/menus/launcher_context_menu.js index 179a2a1be6..5f6607f89e 100644 --- a/src/public/app/menus/launcher_context_menu.js +++ b/src/public/app/menus/launcher_context_menu.js @@ -32,8 +32,8 @@ export default class LauncherContextMenu { const isVisibleItem = parentNoteId === '_lbVisibleLaunchers'; const isAvailableItem = parentNoteId === '_lbAvailableLaunchers'; const isItem = isVisibleItem || isAvailableItem; - const canBeDeleted = !note.isLaunchBarConfig(); - const canBeReset = note.isLaunchBarConfig(); + const canBeDeleted = !note.noteId.startsWith("_"); // fixed notes can't be deleted + const canBeReset = !canBeDeleted && note.isLaunchBarConfig();; return [ (isVisibleRoot || isAvailableRoot) ? { title: 'Add a note launcher', command: 'addNoteLauncher', uiIcon: "bx bx-plus" } : null, diff --git a/src/public/app/services/library_loader.js b/src/public/app/services/library_loader.js index 2cc7fa0b64..32d189c7bd 100644 --- a/src/public/app/services/library_loader.js +++ b/src/public/app/services/library_loader.js @@ -10,6 +10,8 @@ const CODE_MIRROR = { "libraries/codemirror/addon/lint/lint.js", "libraries/codemirror/addon/lint/eslint.js", "libraries/codemirror/addon/mode/loadmode.js", + "libraries/codemirror/addon/mode/multiplex.js", + "libraries/codemirror/addon/mode/overlay.js", "libraries/codemirror/addon/mode/simple.js", "libraries/codemirror/addon/search/match-highlighter.js", "libraries/codemirror/mode/meta.js", diff --git a/src/public/app/services/note_list_renderer.js b/src/public/app/services/note_list_renderer.js index a60f8c7317..ee2aac0d10 100644 --- a/src/public/app/services/note_list_renderer.js +++ b/src/public/app/services/note_list_renderer.js @@ -252,11 +252,15 @@ class NoteListRenderer { if (pageCount < 20 || i <= 5 || pageCount - i <= 5 || Math.abs(this.page - i) <= 2) { lastPrinted = true; + const startIndex = (i - 1) * this.pageSize + 1; + const endIndex = Math.min(this.noteIds.length, i * this.pageSize); + $pager.append( i === this.page ? $('').text(i).css('text-decoration', 'underline').css('font-weight', "bold") : $('') .text(i) + .attr("title", `Page of ${startIndex} - ${endIndex}`) .on('click', () => { this.page = i; this.renderList(); @@ -270,6 +274,9 @@ class NoteListRenderer { lastPrinted = false; } } + + // no need to distinguish "note" vs "notes" since in case of one result, there's no paging at all + $pager.append(`(${this.noteIds.length} notes)`); } async renderNote(note, expand = false) { diff --git a/src/public/app/widgets/buttons/note_actions.js b/src/public/app/widgets/buttons/note_actions.js index 4d48cff411..6e2ebb0cc1 100644 --- a/src/public/app/widgets/buttons/note_actions.js +++ b/src/public/app/widgets/buttons/note_actions.js @@ -45,6 +45,7 @@ const TPL = ` Export note Delete note Print note + Save revision `; diff --git a/src/public/app/widgets/dialogs/revisions.js b/src/public/app/widgets/dialogs/revisions.js index 1fa3eedc07..d5a56328c5 100644 --- a/src/public/app/widgets/dialogs/revisions.js +++ b/src/public/app/widgets/dialogs/revisions.js @@ -274,26 +274,11 @@ export default class RevisionsDialog extends BasicWidget { this.$content.html($table); } else if (revisionItem.type === 'canvas') { - /** - * FIXME: We load a font called Virgil.wof2, which originates from excalidraw.com - * REMOVE external dependency!!!! This is defined in the svg in defs.style - */ - const content = fullRevision.content; - - try { - const data = JSON.parse(content) - const svg = data.svg || "no svg present." - - /** - * maxWidth: 100% use full width of container but do not enlarge! - * height:auto to ensure that height scales with width - */ - const $svgHtml = $(svg).css({maxWidth: "100%", height: "auto"}); - this.$content.html($('
').append($svgHtml)); - } catch (err) { - console.error("error parsing fullRevision.content as JSON", fullRevision.content, err); - this.$content.html($("
").text("Error parsing content. Please check console.error() for more details.")); - } + const sanitizedTitle = revisionItem.title.replace(/[^a-z0-9-.]/gi, ""); + + this.$content.html($("") + .attr("src", `api/revisions/${revisionItem.revisionId}/image/${sanitizedTitle}?${Math.random()}`) + .css("max-width", "100%")); } else { this.$content.text("Preview isn't available for this note type."); } diff --git a/src/public/app/widgets/search_options/order_by.js b/src/public/app/widgets/search_options/order_by.js index 8e2085fd45..100311feed 100644 --- a/src/public/app/widgets/search_options/order_by.js +++ b/src/public/app/widgets/search_options/order_by.js @@ -14,7 +14,8 @@ const TPL = ` - + + diff --git a/src/routes/api/image.js b/src/routes/api/image.js index 902ad9e559..8f44eb1421 100644 --- a/src/routes/api/image.js +++ b/src/routes/api/image.js @@ -5,14 +5,27 @@ const becca = require('../../becca/becca'); const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR; const fs = require('fs'); -function returnImage(req, res) { +function returnImageFromNote(req, res) { const image = becca.getNote(req.params.noteId); + return returnImageInt(image, res); +} + +function returnImageFromRevision(req, res) { + const image = becca.getRevision(req.params.revisionId); + + return returnImageInt(image, res); +} + +/** + * @param {BNote|BRevision} image + * @param res + */ +function returnImageInt(image, res) { if (!image) { res.set('Content-Type', 'image/png'); return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`)); - } - else if (!["image", "canvas"].includes(image.type)){ + } else if (!["image", "canvas"].includes(image.type)) { return res.sendStatus(400); } @@ -84,7 +97,8 @@ function updateImage(req) { } module.exports = { - returnImage, + returnImageFromNote, + returnImageFromRevision, returnAttachedImage, updateImage }; diff --git a/src/routes/routes.js b/src/routes/routes.js index 50ef936348..97a2f8de89 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -181,6 +181,8 @@ function register(app) { apiRoute(GET, '/api/revisions/:revisionId/blob', revisionsApiRoute.getRevisionBlob); apiRoute(DEL, '/api/revisions/:revisionId', revisionsApiRoute.eraseRevision); apiRoute(PST, '/api/revisions/:revisionId/restore', revisionsApiRoute.restoreRevision); + route(GET, '/api/revisions/:revisionId/image/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImageFromRevision); + route(GET, '/api/revisions/:revisionId/download', [auth.checkApiAuthOrElectron], revisionsApiRoute.downloadRevision); @@ -200,7 +202,7 @@ function register(app) { apiRoute(GET, '/api/attribute-values/:attributeName', attributesRoute.getValuesForAttribute); // :filename is not used by trilium, but instead used for "save as" to assign a human-readable filename - route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImage); + route(GET, '/api/images/:noteId/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnImageFromNote); route(PUT, '/api/images/:noteId', [auth.checkApiAuthOrElectron, uploadMiddlewareWithErrorHandling, csrfMiddleware], imageRoute.updateImage, apiResultHandler); apiRoute(GET, '/api/options', optionsApiRoute.getOptions); diff --git a/src/services/anonymization.js b/src/services/anonymization.js index 0c0b139e64..877a9b38b8 100644 --- a/src/services/anonymization.js +++ b/src/services/anonymization.js @@ -7,8 +7,9 @@ const sql = require("./sql"); function getFullAnonymizationScript() { // we want to delete all non-builtin attributes because they can contain sensitive names and values -// on the other hand builtin/system attrs should not contain any sensitive info + // on the other hand builtin/system attrs should not contain any sensitive info const builtinAttrNames = BUILTIN_ATTRIBUTES + .filter(attr => !["shareCredentials", "shareAlias"].includes(attr.name)) .map(attr => `'${attr.name}'`).join(', '); const anonymizeScript = ` diff --git a/src/services/app_info.js b/src/services/app_info.js index 45d560ce4b..98bc9d59be 100644 --- a/src/services/app_info.js +++ b/src/services/app_info.js @@ -4,7 +4,7 @@ const build = require('./build'); const packageJson = require('../../package'); const {TRILIUM_DATA_DIR} = require('./data_dir'); -const APP_DB_VERSION = 225; +const APP_DB_VERSION = 226; const SYNC_VERSION = 31; const CLIPPER_PROTOCOL_VERSION = "1.0"; diff --git a/src/services/build.js b/src/services/build.js index 2b829512eb..6451155536 100644 --- a/src/services/build.js +++ b/src/services/build.js @@ -1 +1 @@ -module.exports = { buildDate:"2023-09-21T23:38:18+02:00", buildRevision: "79e5e3b65ff613cdb81e2afaa832037ccf06d7b8" }; +module.exports = { buildDate:"2023-09-29T00:54:45+02:00", buildRevision: "e5555beea9a1638fefa218118e0596f4cfc1f4d0" }; diff --git a/src/services/consistency_checks.js b/src/services/consistency_checks.js index 5a7c2d8a81..020848d839 100644 --- a/src/services/consistency_checks.js +++ b/src/services/consistency_checks.js @@ -763,7 +763,7 @@ class ConsistencyChecks { } if (this.reloadNeeded) { - require("../becca/becca_loader").reload(); + require("../becca/becca_loader").reload("consistency checks need becca reload"); } return !this.unrecoveredConsistencyErrors; diff --git a/src/services/entity_changes.js b/src/services/entity_changes.js index c76f0fcd85..f1fd2eb5f2 100644 --- a/src/services/entity_changes.js +++ b/src/services/entity_changes.js @@ -148,12 +148,13 @@ function fillEntityChanges(entityName, entityPrimaryKey, condition = '') { const entity = becca.getEntity(entityName, entityId); if (entity) { - ec.hash = entity.generateHash() || "|deleted"; + ec.hash = entity.generateHash(); ec.utcDateChanged = entity.getUtcDateChanged() || dateUtils.utcNowDateTime(); ec.isSynced = entityName !== 'options' || !!entity.isSynced; } else { // entity might be null (not present in becca) when it's deleted - // FIXME: hacky, not sure if it might cause some problems + // this will produce different hash value than when entity is being deleted since then + // all normal hashed attributes are being used. Sync should recover from that, though. ec.hash = "deleted"; ec.utcDateChanged = dateUtils.utcNowDateTime(); ec.isSynced = true; // deletable (the ones with isDeleted) entities are synced diff --git a/src/services/import/zip.js b/src/services/import/zip.js index 15b50cd80f..fcdb0cb5b5 100644 --- a/src/services/import/zip.js +++ b/src/services/import/zip.js @@ -327,6 +327,11 @@ async function importZip(taskContext, fileBuffer, importRootNote) { content = content.replace(/<\/body>.*<\/html>/gis, ""); content = content.replace(/src="([^"]*)"/g, (match, url) => { + if (url.startsWith("data:image")) { + // inline images are parsed and saved into attachments in the note service + return match; + } + try { url = decodeURIComponent(url).trim(); } catch (e) { @@ -456,7 +461,7 @@ async function importZip(taskContext, fileBuffer, importRootNote) { position: attachmentMeta.position }); - attachment.setContent(content); + attachment.setContent(content, { forceSave: true }); return; } diff --git a/src/services/protected_session.js b/src/services/protected_session.js index a1161cbcb3..120e9d60eb 100644 --- a/src/services/protected_session.js +++ b/src/services/protected_session.js @@ -60,7 +60,7 @@ function checkProtectedSessionExpiration() { log.info("Expiring protected session"); - require('./ws').reloadFrontend(); + require('./ws').reloadFrontend("leaving protected session"); } } diff --git a/src/services/search/expressions/order_by_and_limit.js b/src/services/search/expressions/order_by_and_limit.js index 8e8352f6d7..2de6547edb 100644 --- a/src/services/search/expressions/order_by_and_limit.js +++ b/src/services/search/expressions/order_by_and_limit.js @@ -56,9 +56,9 @@ class OrderByAndLimitExp extends Expression { if (!valA && !valB) { // the attribute value is empty/zero in both notes so continue to the next order definition continue; - } else if (!valB || valA < valB) { + } else if (valA < valB) { return smaller; - } else if (!valA || valA > valB) { + } else if (valA > valB) { return larger; } // else the values are equal and continue to next order definition diff --git a/src/services/search/expressions/property_comparison.js b/src/services/search/expressions/property_comparison.js index c506cf9955..f4183988e4 100644 --- a/src/services/search/expressions/property_comparison.js +++ b/src/services/search/expressions/property_comparison.js @@ -31,7 +31,8 @@ const PROP_MAPPING = { "targetrelationcount": "targetRelationCount", "targetrelationcountincludinglinks": "targetRelationCountIncludingLinks", "contentsize": "contentSize", - "notesize": "noteSize", + "contentandattachmentssize": "contentAndAttachmentsSize", + "contentandattachmentsandrevisionssize": "contentAndAttachmentsAndRevisionsSize", "revisioncount": "revisionCount" }; @@ -48,7 +49,7 @@ class PropertyComparisonExp extends Expression { this.comparedValue = comparedValue; // for DEBUG mode this.comparator = buildComparator(operator, comparedValue); - if (['contentsize', 'notesize', 'revisioncount'].includes(this.propertyName)) { + if (['contentsize', 'contentandattachmentssize', 'contentandattachmentsandrevisionssize', 'revisioncount'].includes(this.propertyName)) { searchContext.dbLoadNeeded = true; } } diff --git a/src/services/search/services/search.js b/src/services/search/services/search.js index cda20b94d3..a174e37b54 100644 --- a/src/services/search/services/search.js +++ b/src/services/search/services/search.js @@ -92,47 +92,107 @@ function searchFromRelation(note, relationName) { function loadNeededInfoFromDatabase() { const sql = require('../../sql'); - for (const noteId in becca.notes) { - becca.notes[noteId].contentSize = 0; - becca.notes[noteId].noteSize = 0; - becca.notes[noteId].revisionCount = 0; - } + /** + * This complex structure is needed to calculate total occupied space by a note. Several object instances + * (note, revisions, attachments) can point to a single blobId, and thus the blob size should count towards the total + * only once. + * + * @var {Object.>} - noteId => { blobId => blobSize } + */ + const noteBlobs = {}; const noteContentLengths = sql.getRows(` SELECT noteId, + blobId, LENGTH(content) AS length FROM notes JOIN blobs USING(blobId) WHERE notes.isDeleted = 0`); - for (const {noteId, length} of noteContentLengths) { + for (const {noteId, blobId, length} of noteContentLengths) { if (!(noteId in becca.notes)) { - log.error(`Note ${noteId} not found in becca.`); + log.error(`Note '${noteId}' not found in becca.`); continue; } becca.notes[noteId].contentSize = length; - becca.notes[noteId].noteSize = length; + becca.notes[noteId].revisionCount = 0; + + noteBlobs[noteId] = { [blobId]: length }; } - const revisionContentLengths = sql.getRows(` - SELECT - noteId, - LENGTH(content) AS length - FROM notes - JOIN revisions USING(noteId) - JOIN blobs USING(blobId) - WHERE notes.isDeleted = 0`); + const attachmentContentLengths = sql.getRows(` + SELECT + ownerId AS noteId, + attachments.blobId, + LENGTH(content) AS length + FROM attachments + JOIN notes ON attachments.ownerId = notes.noteId + JOIN blobs ON attachments.blobId = blobs.blobId + WHERE attachments.isDeleted = 0 + AND notes.isDeleted = 0`); + + for (const {noteId, blobId, length} of attachmentContentLengths) { + if (!(noteId in becca.notes)) { + log.error(`Note '${noteId}' not found in becca.`); + continue; + } - for (const {noteId, length} of revisionContentLengths) { + if (!(noteId in noteBlobs)) { + log.error(`Did not find a '${noteId}' in the noteBlobs.`); + continue; + } + + noteBlobs[noteId][blobId] = length; + } + + for (const noteId in noteBlobs) { + becca.notes[noteId].contentAndAttachmentsSize = Object.values(noteBlobs[noteId]).reduce((acc, size) => acc + size, 0); + } + + const revisionContentLengths = sql.getRows(` + SELECT + noteId, + revisions.blobId, + LENGTH(content) AS length, + 1 AS isNoteRevision + FROM notes + JOIN revisions USING(noteId) + JOIN blobs ON revisions.blobId = blobs.blobId + WHERE notes.isDeleted = 0 + UNION ALL + SELECT + noteId, + revisions.blobId, + LENGTH(content) AS length, + 0 AS isNoteRevision -- it's attachment not counting towards revision count + FROM notes + JOIN revisions USING(noteId) + JOIN attachments ON attachments.ownerId = revisions.revisionId + JOIN blobs ON attachments.blobId = blobs.blobId + WHERE notes.isDeleted = 0`); + + for (const {noteId, blobId, length, isNoteRevision} of revisionContentLengths) { if (!(noteId in becca.notes)) { - log.error(`Note ${noteId} not found in becca.`); + log.error(`Note '${noteId}' not found in becca.`); continue; } - becca.notes[noteId].noteSize += length; - becca.notes[noteId].revisionCount++; + if (!(noteId in noteBlobs)) { + log.error(`Did not find a '${noteId}' in the noteBlobs.`); + continue; + } + + noteBlobs[noteId][blobId] = length; + + if (isNoteRevision) { + becca.notes[noteId].revisionCount++; + } + } + + for (const noteId in noteBlobs) { + becca.notes[noteId].contentAndAttachmentsAndRevisionsSize = Object.values(noteBlobs[noteId]).reduce((acc, size) => acc + size, 0); } } diff --git a/src/services/search/value_extractor.js b/src/services/search/value_extractor.js index 633666d298..27aff0e6c4 100644 --- a/src/services/search/value_extractor.js +++ b/src/services/search/value_extractor.js @@ -27,7 +27,8 @@ const PROP_MAPPING = { "targetrelationcount": "targetRelationCount", "targetrelationcountincludinglinks": "targetRelationCountIncludingLinks", "contentsize": "contentSize", - "notesize": "noteSize", + "contentandattachmentssize": "contentAndAttachmentsSize", + "contentandattachmentsandrevisionssize": "contentAndAttachmentsAndRevisionsSize", "revisioncount": "revisionCount" }; @@ -42,7 +43,7 @@ class ValueExtractor { this.propertyPath = ['note', 'relations', this.propertyPath[0].substr(1), ...this.propertyPath.slice(1, this.propertyPath.length)]; } - if (['contentsize', 'notesize', 'revisioncount'].includes(this.propertyPath[this.propertyPath.length - 1])) { + if (['contentsize', 'contentandattachmentssize', 'contentandattachmentsandrevisionssize', 'revisioncount'].includes(this.propertyPath[this.propertyPath.length - 1])) { searchContext.dbLoadNeeded = true; } } diff --git a/src/services/sync.js b/src/services/sync.js index bdc31475c0..0316661960 100644 --- a/src/services/sync.js +++ b/src/services/sync.js @@ -151,9 +151,14 @@ async function pullChanges(syncContext) { if (entityChanges.length === 0) { break; } else { - const sizeInKb = Math.round(JSON.stringify(resp).length / 1024); + try { // https://github.com/zadam/trilium/issues/4310 + const sizeInKb = Math.round(JSON.stringify(resp).length / 1024); - log.info(`Sync ${logMarkerId}: Pulled ${entityChanges.length} changes in ${sizeInKb} KB, starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`); + log.info(`Sync ${logMarkerId}: Pulled ${entityChanges.length} changes in ${sizeInKb} KB, starting at entityChangeId=${lastSyncedPull} in ${pulledDate - startDate}ms and applied them in ${Date.now() - pulledDate}ms, ${outstandingPullCount} outstanding pulls`); + } + catch (e) { + log.error(`Error occurred ${e.message} ${e.stack}`); + } } } diff --git a/src/services/sync_update.js b/src/services/sync_update.js index aceb0d5be5..919f3c8cea 100644 --- a/src/services/sync_update.js +++ b/src/services/sync_update.js @@ -106,7 +106,7 @@ function updateNormalEntity(remoteEC, remoteEntityRow, instanceId, updateContext updateContext.updated[remoteEC.entityName] = updateContext.updated[remoteEC.entityName] || []; updateContext.updated[remoteEC.entityName].push(remoteEC.entityId); - if (!localEC || localEC.utcDateChanged < remoteEC.utcDateChanged) { + if (!localEC || localEC.utcDateChanged < remoteEC.utcDateChanged || localEC.hash !== remoteEC.hash) { entityChangesService.putEntityChangeWithInstanceId(remoteEC, instanceId); } diff --git a/src/services/ws.js b/src/services/ws.js index 6f613b0593..f5144dcb7a 100644 --- a/src/services/ws.js +++ b/src/services/ws.js @@ -13,7 +13,7 @@ const env = require('./env'); if (env.isDev()) { const chokidar = require('chokidar'); const debounce = require('debounce'); - const debouncedReloadFrontend = debounce(reloadFrontend, 200); + const debouncedReloadFrontend = debounce(() => reloadFrontend("source code change"), 200); chokidar .watch('src/public') .on('add', debouncedReloadFrontend) @@ -230,8 +230,8 @@ function syncFailed() { sendMessageToAllClients({ type: 'sync-failed', lastSyncedPush }); } -function reloadFrontend() { - sendMessageToAllClients({ type: 'reload-frontend' }); +function reloadFrontend(reason) { + sendMessageToAllClients({ type: 'reload-frontend', reason }); } function setLastSyncedPush(entityChangeId) {