diff --git a/db/demo.zip b/db/demo.zip index f9efd3e44..5344f44c2 100644 Binary files a/db/demo.zip and b/db/demo.zip differ diff --git a/db/migrations/0204__migrate_bookmarks_to_clones.js b/db/migrations/0204__migrate_bookmarks_to_clones.js index 82cc11ebe..6b180a446 100644 --- a/db/migrations/0204__migrate_bookmarks_to_clones.js +++ b/db/migrations/0204__migrate_bookmarks_to_clones.js @@ -12,5 +12,10 @@ module.exports = () => { attr.markAsDeleted("0204__migrate_bookmarks_to_clones"); } + + // bookmarkFolder used to work in 0.57 without the bookmarked label + for (const attr of becca.findAttributes('label','bookmarkFolder')) { + cloningService.toggleNoteInParent(true, attr.noteId, '_lbBookmarks'); + } }); }; diff --git a/src/becca/becca_service.js b/src/becca/becca_service.js index d22c1054e..9d19edcd7 100644 --- a/src/becca/becca_service.js +++ b/src/becca/becca_service.js @@ -82,10 +82,8 @@ function getNoteTitleArrayForPath(notePathArray) { throw new Error(`${notePathArray} is not an array.`); } - const hoistedNoteId = cls.getHoistedNoteId(); - - if (notePathArray.length === 1 && notePathArray[0] === hoistedNoteId) { - return [getNoteTitle(hoistedNoteId)]; + if (notePathArray.length === 1) { + return [getNoteTitle(notePathArray[0])]; } const titles = []; @@ -94,6 +92,7 @@ function getNoteTitleArrayForPath(notePathArray) { let hoistedNotePassed = false; // this is a notePath from outside of hoisted subtree so full title path needs to be returned + const hoistedNoteId = cls.getHoistedNoteId(); const outsideOfHoistedSubtree = !notePathArray.includes(hoistedNoteId); for (const noteId of notePathArray) { diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index 71e351f38..c41c202b3 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -11,6 +11,7 @@ const BNoteRevision = require("./bnote_revision"); const TaskContext = require("../../services/task_context"); const dayjs = require("dayjs"); const utc = require('dayjs/plugin/utc'); +const eventService = require("../../services/events"); dayjs.extend(utc) const LABEL = 'label'; @@ -314,6 +315,11 @@ class BNote extends AbstractBeccaEntity { utcDateChanged: pojo.utcDateModified, isSynced: true }); + + eventService.emit(eventService.ENTITY_CHANGED, { + entityName: 'note_contents', + entity: this + }); } setJsonContent(content) { @@ -1124,6 +1130,13 @@ class BNote extends AbstractBeccaEntity { return notePaths; } + /** + * @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree + */ + isHiddenCompletely() { + return !this.getAllNotePaths().find(notePathArr => !notePathArr.includes('_hidden')); + } + /** * @param ancestorNoteId * @returns {boolean} - true if ancestorNoteId occurs in at least one of the note's paths @@ -1368,7 +1381,7 @@ class BNote extends AbstractBeccaEntity { } isOptions() { - return this.noteId.startsWith("options"); + return this.noteId.startsWith("_options"); } get isDeleted() { diff --git a/src/public/app/doc_notes/launchbar_history_navigation.html b/src/public/app/doc_notes/launchbar_history_navigation.html new file mode 100644 index 000000000..bf4411572 --- /dev/null +++ b/src/public/app/doc_notes/launchbar_history_navigation.html @@ -0,0 +1,3 @@ +

Back and Forward buttons allow you to move in the navigation history.

+ +

These launchers are active only in the desktop build and will be ignored in the server edition where you can use the native browser navigation buttons instead.

diff --git a/src/public/app/entities/fnote.js b/src/public/app/entities/fnote.js index fa08995f2..42e8eadbe 100644 --- a/src/public/app/entities/fnote.js +++ b/src/public/app/entities/fnote.js @@ -360,6 +360,13 @@ class FNote { return notePaths; } + /** + * @return boolean - true if there's no non-hidden path, note is not cloned to the visible tree + */ + isHiddenCompletely() { + return !this.getAllNotePaths().find(notePathArr => !notePathArr.includes('_hidden')); + } + __filterAttrs(attributes, type, name) { this.__validateTypeName(type, name); @@ -852,7 +859,7 @@ class FNote { } isOptions() { - return this.noteId.startsWith("options"); + return this.noteId.startsWith("_options"); } } diff --git a/src/public/app/services/note_autocomplete.js b/src/public/app/services/note_autocomplete.js index 157c24261..07c75d447 100644 --- a/src/public/app/services/note_autocomplete.js +++ b/src/public/app/services/note_autocomplete.js @@ -230,6 +230,10 @@ function init() { $.fn.getSelectedNoteId = function () { const notePath = $(this).getSelectedNotePath(); + if (!notePath) { + return null; + } + const chunks = notePath.split('/'); return chunks.length >= 1 ? chunks[chunks.length - 1] : null; diff --git a/src/public/app/widgets/attribute_widgets/attribute_detail.js b/src/public/app/widgets/attribute_widgets/attribute_detail.js index 92d7d6b9b..7ceb9bbcb 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_detail.js +++ b/src/public/app/widgets/attribute_widgets/attribute_detail.js @@ -247,7 +247,7 @@ const ATTR_HELP = { "runOnNoteCreation": "executes when note is created on backend. Use this relation if you want to run the script for all notes created under a specific subtree. In that case, create it on the subtree root note and make it inheritable. A new note created within the subtree (any depth) will trigger the script.", "runOnChildNoteCreation": "executes when new note is created under the note where this relation is defined", "runOnNoteTitleChange": "executes when note title is changed (includes note creation as well)", - "runOnNoteContentChange": "executes when note content is changed (includes note creation as well).", + "runOnNoteContentChange": "executes when note content is changed (includes note creation as well).", "runOnNoteChange": "executes when note is changed (includes note creation as well). Does not include content changes", "runOnNoteDeletion": "executes when note is being deleted", "runOnBranchCreation": "executes when a branch is created. Branch is a link between parent note and child note and is created e.g. when cloning or moving note.", diff --git a/src/public/app/widgets/buttons/command_button.js b/src/public/app/widgets/buttons/command_button.js index 376c5a822..12e0dd821 100644 --- a/src/public/app/widgets/buttons/command_button.js +++ b/src/public/app/widgets/buttons/command_button.js @@ -39,7 +39,7 @@ export default class CommandButtonWidget extends AbstractButtonWidget { /** * @param {function|string} command - * @returns {CommandButtonWidget} + * @returns {this} */ command(command) { this.settings.command = command; diff --git a/src/public/app/widgets/buttons/global_menu.js b/src/public/app/widgets/buttons/global_menu.js index ff881aa20..63045532a 100644 --- a/src/public/app/widgets/buttons/global_menu.js +++ b/src/public/app/widgets/buttons/global_menu.js @@ -303,7 +303,7 @@ export default class GlobalMenuWidget extends BasicWidget { const resp = await fetch(RELEASES_API_URL); const data = await resp.json(); - return data.tag_name.substring(1); + return data?.tag_name?.substring(1); } downloadLatestVersionCommand() { diff --git a/src/public/app/widgets/buttons/history_navigation.js b/src/public/app/widgets/buttons/history_navigation.js index 430aeae21..08a86810f 100644 --- a/src/public/app/widgets/buttons/history_navigation.js +++ b/src/public/app/widgets/buttons/history_navigation.js @@ -23,6 +23,10 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget { doRender() { super.doRender(); + if (!utils.isElectron()) { + return; + } + this.webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents(); // without this the history is preserved across frontend reloads diff --git a/src/public/app/widgets/dialogs/note_revisions.js b/src/public/app/widgets/dialogs/note_revisions.js index 17d578c2d..f8c6584ab 100644 --- a/src/public/app/widgets/dialogs/note_revisions.js +++ b/src/public/app/widgets/dialogs/note_revisions.js @@ -234,14 +234,14 @@ export default class NoteRevisionsDialog extends BasicWidget { renderMathInElement($content[0], {trust: true}); } } - else if (revisionItem.type === 'code') { + else if (revisionItem.type === 'code' || revisionItem.type === 'mermaid') { this.$content.html($("
").text(fullNoteRevision.content));
         }
         else if (revisionItem.type === 'image') {
             this.$content.html($("")
                 // reason why we put this inline as base64 is that we do not want to let user copy this
                 // as a URL to be used in a note. Instead, if they copy and paste it into a note, it will be an uploaded as a new note
-                .attr("src", `data:${note.mime};base64,${fullNoteRevision.content}`)
+                .attr("src", `data:${fullNoteRevision.mime};base64,${fullNoteRevision.content}`)
                 .css("max-width", "100%")
                 .css("max-height", "100%"));
         }
diff --git a/src/public/app/widgets/floating_buttons/code_buttons.js b/src/public/app/widgets/floating_buttons/code_buttons.js
index 9363856a5..f1763870b 100644
--- a/src/public/app/widgets/floating_buttons/code_buttons.js
+++ b/src/public/app/widgets/floating_buttons/code_buttons.js
@@ -76,7 +76,7 @@ export default class CodeButtonsWidget extends NoteContextAwareWidget {
 
         this.$saveToNoteButton.toggle(
             note.mime === 'text/x-sqlite;schema=trilium'
-            && !note.getAllNotePaths().find(notePathArr => !notePathArr.includes('_hidden'))
+            && note.isHiddenCompletely()
         );
 
         this.$openTriliumApiDocsButton.toggle(note.mime.startsWith('application/javascript;env='));
diff --git a/src/public/app/widgets/note_tree.js b/src/public/app/widgets/note_tree.js
index 8e9fcdf04..204bb77db 100644
--- a/src/public/app/widgets/note_tree.js
+++ b/src/public/app/widgets/note_tree.js
@@ -1309,6 +1309,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
             await this.tree.reload([rootNode]);
         });
 
+        await this.filterHoistedBranch();
+
         if (activeNotePath) {
             const node = await this.getNodeFromPath(activeNotePath, true);
 
diff --git a/src/public/app/widgets/ribbon_widgets/search_definition.js b/src/public/app/widgets/ribbon_widgets/search_definition.js
index 0dc4d4841..4ca8e17d9 100644
--- a/src/public/app/widgets/ribbon_widgets/search_definition.js
+++ b/src/public/app/widgets/ribbon_widgets/search_definition.js
@@ -270,7 +270,7 @@ export default class SearchDefinitionWidget extends NoteContextAwareWidget {
     async refreshWithNote(note) {
         this.$component.show();
 
-        this.$saveToNoteButton.toggle(!note.getAllNotePaths().find(notePathArr => !notePathArr.includes('_hidden')));
+        this.$saveToNoteButton.toggle(note.isHiddenCompletely());
 
         this.$searchOptions.empty();
 
diff --git a/src/routes/api/options.js b/src/routes/api/options.js
index a176bb45f..ddb4321ec 100644
--- a/src/routes/api/options.js
+++ b/src/routes/api/options.js
@@ -112,7 +112,7 @@ function update(name, value) {
 }
 
 function getUserThemes() {
-    const notes = searchService.searchNotes("#appTheme");
+    const notes = searchService.searchNotes("#appTheme", {ignoreHoistedNote: true});
     const ret = [];
 
     for (const note of notes) {
diff --git a/src/services/build.js b/src/services/build.js
index f3031a049..dc5dc5172 100644
--- a/src/services/build.js
+++ b/src/services/build.js
@@ -1 +1 @@
-module.exports = { buildDate:"2023-01-11T23:44:33+01:00", buildRevision: "bdfdc0402ddb23e9af002580f368bc52e4268b3a" };
+module.exports = { buildDate:"2023-01-16T22:39:28+01:00", buildRevision: "9fd0b85ff2be264be35ec2052c956b654f0dac9e" };
diff --git a/src/services/builtin_attributes.js b/src/services/builtin_attributes.js
index d3558b884..1597c6599 100644
--- a/src/services/builtin_attributes.js
+++ b/src/services/builtin_attributes.js
@@ -71,6 +71,7 @@ module.exports = [
     { type: 'relation', name: 'runOnNoteCreation', isDangerous: true },
     { type: 'relation', name: 'runOnNoteTitleChange', isDangerous: true },
     { type: 'relation', name: 'runOnNoteChange', isDangerous: true },
+    { type: 'relation', name: 'runOnNoteContentChange', isDangerous: true },
     { type: 'relation', name: 'runOnNoteDeletion', isDangerous: true },
     { type: 'relation', name: 'runOnBranchCreation', isDangerous: true },
     { type: 'relation', name: 'runOnBranchDeletion', isDangerous: true },
diff --git a/src/services/handlers.js b/src/services/handlers.js
index 588b80555..4bb21f910 100644
--- a/src/services/handlers.js
+++ b/src/services/handlers.js
@@ -4,6 +4,8 @@ const treeService = require('./tree');
 const noteService = require('./notes');
 const becca = require('../becca/becca');
 const BAttribute = require('../becca/entities/battribute');
+const hiddenSubtreeService = require("./hidden_subtree");
+const oneTimeTimer = require("./one_time_timer");
 
 function runAttachedRelations(note, relationName, originEntity) {
     if (!note) {
@@ -206,6 +208,16 @@ eventService.subscribe(eventService.ENTITY_DELETED, ({ entityName, entity }) =>
     if (entityName === 'branches') {
         runAttachedRelations(entity.getNote(), 'runOnBranchDeletion', entity);
     }
+
+    if (entityName === 'notes' && entity.noteId.startsWith("_")) {
+        // "named" note has been deleted, we will probably need to rebuild the hidden subtree
+        // scheduling so that bulk deletes won't trigger so many checks
+        oneTimeTimer.scheduleExecution('hidden-subtree-check', 1000, () => {
+            console.log("Checking hidden subtree");
+
+            hiddenSubtreeService.checkHiddenSubtree();
+        });
+    }
 });
 
 module.exports = {
diff --git a/src/services/hidden_subtree.js b/src/services/hidden_subtree.js
index ff8036c1c..ba62ab288 100644
--- a/src/services/hidden_subtree.js
+++ b/src/services/hidden_subtree.js
@@ -1,6 +1,8 @@
 const becca = require("../becca/becca");
 const noteService = require("./notes");
 const BAttribute = require("../becca/entities/battribute");
+const log = require("./log");
+const migrationService = require("./migration");
 
 const LBTPL_ROOT = "_lbTplRoot";
 const LBTPL_BASE = "_lbTplBase";
@@ -179,8 +181,10 @@ const HIDDEN_SUBTREE_DEFINITION = {
                     isExpanded: true,
                     attributes: [ { type: 'label', name: 'docName', value: 'launchbar_intro' } ],
                     children: [
-                        { id: '_lbBackInHistory', title: 'Go to Previous Note', type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-left-arrow-square' },
-                        { id: '_lbForwardInHistory', title: 'Go to Next Note', type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-right-arrow-square' },
+                        { id: '_lbBackInHistory', title: 'Go to Previous Note', type: 'launcher', builtinWidget: 'backInHistoryButton', icon: 'bx bxs-left-arrow-square',
+                            attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
+                        { id: '_lbForwardInHistory', title: 'Go to Next Note', type: 'launcher', builtinWidget: 'forwardInHistoryButton', icon: 'bx bxs-right-arrow-square',
+                            attributes: [ { type: 'label', name: 'docName', value: 'launchbar_history_navigation' } ]},
                         { id: '_lbBackendLog', title: 'Backend Log', type: 'launcher', targetNoteId: '_backendLog', icon: 'bx bx-terminal' },
                     ]
                 },
@@ -237,6 +241,12 @@ const HIDDEN_SUBTREE_DEFINITION = {
 };
 
 function checkHiddenSubtree() {
+    if (!migrationService.isDbUpToDate()) {
+        // on-delete hook might get triggered during some future migration and cause havoc
+        log.info("Will not check hidden subtree until migration is finished.");
+        return;
+    }
+
     checkHiddenSubtreeRecursively('root', HIDDEN_SUBTREE_DEFINITION);
 }
 
diff --git a/src/services/import/enex.js b/src/services/import/enex.js
index b982d642a..cb1c3dcaa 100644
--- a/src/services/import/enex.js
+++ b/src/services/import/enex.js
@@ -76,8 +76,8 @@ function importEnex(taskContext, file, parentNote) {
         content = content.replace(/<\/ol>\s*
  • /g, "
  • "); // Replace en-todo with unicode ballot box - content = content.replace(//g, "\u2611 "); - content = content.replace(//g, "\u2610 "); + content = content.replace(//g, "\u2611 "); + content = content.replace(//g, "\u2610 "); // Replace OneNote converted checkboxes with unicode ballot box based // on known hash of checkboxes for regular, p1, and p2 checkboxes diff --git a/src/services/migration.js b/src/services/migration.js index 1bda273e4..7bda5bc36 100644 --- a/src/services/migration.js +++ b/src/services/migration.js @@ -119,5 +119,6 @@ async function migrateIfNecessary() { } module.exports = { - migrateIfNecessary + migrateIfNecessary, + isDbUpToDate }; diff --git a/src/services/notes.js b/src/services/notes.js index 1785d989f..b540e8522 100644 --- a/src/services/notes.js +++ b/src/services/notes.js @@ -108,8 +108,13 @@ function getAndValidateParent(params) { throw new ValidationError(`Only 'launcher' notes can be created in parent '${params.parentNoteId}'`); } - if (!params.ignoreForbiddenParents && (['_lbRoot', '_hidden'].includes(parentNote.noteId) || parentNote.isOptions())) { - throw new ValidationError(`Creating child notes into '${parentNote.noteId}' is not allowed.`); + if (!params.ignoreForbiddenParents) { + if (['_lbRoot', '_hidden'].includes(parentNote.noteId) + || parentNote.noteId.startsWith("_lbTpl") + || parentNote.isOptions()) { + + throw new ValidationError(`Creating child notes into '${parentNote.noteId}' is not allowed.`); + } } return parentNote; @@ -283,8 +288,12 @@ function protectNote(note, protect) { note.isProtected = protect; - // this will force de/encryption - note.setContent(content); + // see https://github.com/zadam/trilium/issues/3523 + // IIRC a zero-sized buffer can be returned as null from the database + if (content !== null) { + // this will force de/encryption + note.setContent(content); + } note.save(); } @@ -592,11 +601,6 @@ function updateNoteContent(noteId, content) { content = saveLinks(note, content); note.setContent(content); - - eventService.emit(eventService.ENTITY_CHANGED, { - entityName: 'note_contents', - entity: note - }); } /** diff --git a/src/services/one_time_timer.js b/src/services/one_time_timer.js new file mode 100644 index 000000000..68b4dfef4 --- /dev/null +++ b/src/services/one_time_timer.js @@ -0,0 +1,25 @@ +const scheduledExecutions = {}; + +/** + * Subsequent calls will not move the timer to future. The first caller determines the time of execution. + * + * The good thing about synchronous better-sqlite3 is that this cannot interrupt transaction. The execution will be called + * only outside of a transaction. + */ +function scheduleExecution(name, milliseconds, cb) { + if (name in scheduledExecutions) { + return; + } + + scheduledExecutions[name] = true; + + setTimeout(() => { + delete scheduledExecutions[name]; + + cb(); + }, milliseconds); +} + +module.exports = { + scheduleExecution +}; \ No newline at end of file diff --git a/src/services/search/expressions/is_hidden.js b/src/services/search/expressions/is_hidden.js new file mode 100644 index 000000000..023c69087 --- /dev/null +++ b/src/services/search/expressions/is_hidden.js @@ -0,0 +1,23 @@ +"use strict"; + +const Expression = require('./expression'); +const NoteSet = require('../note_set'); + +/** + * Note is hidden when all its note paths start in hidden subtree (i.e. the note is not cloned into visible tree) + */ +class IsHiddenExp extends Expression { + execute(inputNoteSet, executionContext, searchContext) { + const resultNoteSet = new NoteSet(); + + for (const note of inputNoteSet.notes) { + if (note.isHiddenCompletely()) { + resultNoteSet.add(note); + } + } + + return resultNoteSet; + } +} + +module.exports = IsHiddenExp; diff --git a/src/services/search/search_context.js b/src/services/search/search_context.js index 6c427cf5f..95b8b8c57 100644 --- a/src/services/search/search_context.js +++ b/src/services/search/search_context.js @@ -6,10 +6,11 @@ class SearchContext { constructor(params = {}) { this.fastSearch = !!params.fastSearch; this.includeArchivedNotes = !!params.includeArchivedNotes; + this.includeHiddenNotes = !!params.includeHiddenNotes; this.ignoreHoistedNote = !!params.ignoreHoistedNote; this.ancestorNoteId = params.ancestorNoteId; - if (!this.ancestorNoteId && !this.ignoreHoistedNote && !hoistedNoteService.isHoistedInHiddenSubtree()) { + if (!this.ancestorNoteId && !this.ignoreHoistedNote) { // hoisting in hidden subtree should not limit autocomplete // since we want to link (create relations) to the normal non-hidden notes this.ancestorNoteId = hoistedNoteService.getHoistedNoteId(); diff --git a/src/services/search/services/parse.js b/src/services/search/services/parse.js index 1226ab629..6332f7728 100644 --- a/src/services/search/services/parse.js +++ b/src/services/search/services/parse.js @@ -19,6 +19,7 @@ const buildComparator = require('./build_comparator'); const ValueExtractor = require('../value_extractor'); const utils = require("../../utils"); const TrueExp = require("../expressions/true"); +const IsHiddenExp = require("../expressions/is_hidden"); function getFulltext(tokens, searchContext) { tokens = tokens.map(t => utils.removeDiacritic(t.token)); @@ -429,7 +430,7 @@ function parse({fulltextTokens, expressionTokens, searchContext}) { let exp = AndExp.of([ searchContext.includeArchivedNotes ? null : new PropertyComparisonExp(searchContext, "isarchived", "=", "false"), - (searchContext.ancestorNoteId && searchContext.ancestorNoteId !== 'root') ? new AncestorExp(searchContext.ancestorNoteId, searchContext.ancestorDepth) : null, + getAncestorExp(searchContext), getFulltext(fulltextTokens, searchContext), expression ]); @@ -448,4 +449,14 @@ function parse({fulltextTokens, expressionTokens, searchContext}) { return exp; } +function getAncestorExp({ancestorNoteId, ancestorDepth, includeHiddenNotes}) { + if (ancestorNoteId && ancestorNoteId !== 'root') { + return new AncestorExp(ancestorNoteId, ancestorDepth); + } else if (!includeHiddenNotes) { + return new NotExp(new IsHiddenExp()); + } else { + return null; + } +} + module.exports = parse; diff --git a/src/services/search/services/search.js b/src/services/search/services/search.js index 59fa5fd38..9ca8386ba 100644 --- a/src/services/search/services/search.js +++ b/src/services/search/services/search.js @@ -11,6 +11,7 @@ const beccaService = require('../../../becca/becca_service'); const utils = require('../../utils'); const log = require('../../log'); const scriptService = require("../../script"); +const hoistedNoteService = require("../../hoisted_note"); function searchFromNote(note) { let searchResultNoteIds, highlightedTokens; @@ -272,7 +273,11 @@ function searchNotesForAutocomplete(query) { const searchContext = new SearchContext({ fastSearch: true, includeArchivedNotes: false, - fuzzyAttributeSearch: true + includeHiddenNotes: true, + fuzzyAttributeSearch: true, + ancestorNoteId: hoistedNoteService.isHoistedInHiddenSubtree() + ? 'root' + : hoistedNoteService.getHoistedNoteId() }); const allSearchResults = findResultsWithQuery(query, searchContext); diff --git a/src/services/special_notes.js b/src/services/special_notes.js index ee88a48eb..bbc1bc978 100644 --- a/src/services/special_notes.js +++ b/src/services/special_notes.js @@ -218,7 +218,7 @@ function resetLauncher(noteId) { log.info(`Note ${noteId} is not a resettable launcher note.`); } - hiddenSubtreeService.checkHiddenSubtree(); + // the re-building deleted launchers will be done in handlers } /** diff --git a/src/share/content_renderer.js b/src/share/content_renderer.js index 9b9aef442..d73fa6206 100644 --- a/src/share/content_renderer.js +++ b/src/share/content_renderer.js @@ -2,6 +2,7 @@ const {JSDOM} = require("jsdom"); const shaca = require("./shaca/shaca"); const assetPath = require("../services/asset_path"); const shareRoot = require('./share_root'); +const escapeHtml = require('escape-html'); function getContent(note) { if (note.isProtected) { @@ -112,17 +113,17 @@ function renderCode(result) { function renderMermaid(result) { result.content = ` -
    ${result.content}
    +
    ${escapeHtml(result.content)}

    Chart source -
    ${result.content}
    +
    ${escapeHtml(result.content)}
    ` result.header += ``; } function renderImage(result, note) { - result.content = ``; + result.content = ``; } function renderFile(note, result) {