From 614cc4dd8250cb8d2081deeaa0cedc514fb063b6 Mon Sep 17 00:00:00 2001 From: Zack Rauen Date: Wed, 27 Sep 2023 14:34:07 -0400 Subject: [PATCH 1/4] Allow users to use their own share template --- src/share/content_renderer.js | 6 ++- src/share/routes.js | 79 ++++++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/share/content_renderer.js b/src/share/content_renderer.js index cc456bc2ef..8c1dbee10e 100644 --- a/src/share/content_renderer.js +++ b/src/share/content_renderer.js @@ -44,7 +44,8 @@ function renderIndex(result) { const rootNote = shaca.getNote(shareRoot.SHARE_ROOT_NOTE_ID); for (const childNote of rootNote.getChildNotes()) { - result.content += `
  • ${childNote.escapedTitle}
  • `; + const href = childNote.getLabelValue("shareExternal") ?? `./${childNote.shareId}`; + result.content += `
  • ${childNote.escapedTitle}
  • `; } result.content += ''; @@ -84,7 +85,8 @@ function renderText(result, note) { const noteId = notePathSegments[notePathSegments.length - 1]; const linkedNote = shaca.getNote(noteId); if (linkedNote) { - linkEl.setAttribute("href", linkedNote.shareId); + const href = linkedNote.getLabelValue("shareExternal") ?? `./${linkedNote.shareId}`; + linkEl.setAttribute("href", href); linkEl.classList.add(`type-${linkedNote.type}`); } else { linkEl.removeAttribute("href"); diff --git a/src/share/routes.js b/src/share/routes.js index 462ad1aeb1..80fa3423f8 100644 --- a/src/share/routes.js +++ b/src/share/routes.js @@ -1,6 +1,7 @@ const express = require('express'); const path = require('path'); const safeCompare = require('safe-compare'); +const ejs = require("ejs"); const shaca = require("./shaca/shaca"); const shacaLoader = require("./shaca/shaca_loader"); @@ -8,6 +9,9 @@ const shareRoot = require("./share_root"); const contentRenderer = require("./content_renderer"); const assetPath = require("../services/asset_path"); const appPath = require("../services/app_path"); +const searchService = require("../services/search/services/search"); +const SearchContext = require("../services/search/search_context"); +const log = require("../services/log"); /** * @param {SNote} note @@ -128,18 +132,42 @@ function register(router) { } const {header, content, isEmpty} = contentRenderer.getContent(note); - const subRoot = getSharedSubTreeRoot(note); + const opts = {note, header, content, isEmpty, subRoot, assetPath, appPath}; + let useDefaultView = true; + + // Check if the user has their own template + if (note.hasRelation('shareTemplate')) { + // Get the template note and content + const templateId = note.getRelation('shareTemplate').value; + const templateNote = shaca.getNote(templateId); + + // Make sure the note type is correct + if (templateNote.type === 'code' && templateNote.mime === 'application/x-ejs') { + + // EJS caches the result of this so we don't need to pre-cache + const includer = (path) => { + const childNote = templateNote.children.find(n => path === n.title); + if (!childNote) return null; + if (childNote.type !== 'code' || childNote.mime !== 'application/x-ejs') return null; + return { template: childNote.getContent() }; + }; + + // Try to render user's template, w/ fallback to default view + try { + const ejsResult = ejs.render(templateNote.getContent(), opts, {includer}); + res.send(ejsResult); + useDefaultView = false; // Rendering went okay, don't use default view + } + catch (e) { + log.error(`Rendering user provided share template (${templateId}) threw exception ${e.message} with stacktrace: ${e.stack}`); + } + } + } - res.render("share/page", { - note, - header, - content, - isEmpty, - subRoot, - assetPath, - appPath - }); + if (useDefaultView) { + res.render('share/page', opts); + } } router.get('/share/', (req, res, next) => { @@ -303,6 +331,37 @@ function register(router) { res.send(note.getContent()); }); + + // Used for searching, require noteId so we know the subTreeRoot + router.get('/share/api/search/:noteId', (req, res, next) => { + shacaLoader.ensureLoad(); + + let note; + + if (!(note = checkNoteAccess(req.params.noteId, req, res))) { + return; + } + + const {query} = req.query; + + if (!query?.trim()) { + return res.status(400).json({ message: "'query' parameter is mandatory." }); + } + + const subRootPath = getSharedSubTreeRoot(note); + const subRoot = subRootPath.note; + const searchContext = new SearchContext({ancestorNoteId: subRoot.noteId}); + const searchResults = searchService.findResultsWithQuery(query, searchContext); + const filteredResults = searchResults.map(sr => { + const fullNote = shaca.notes[sr.noteId]; + const startIndex = sr.notePathArray.indexOf(subRoot.noteId); + const localPathArray = sr.notePathArray.slice(startIndex + 1); + const pathTitle = localPathArray.map(id => shaca.notes[id].title).join(" / "); + return { id: fullNote.noteId, title: fullNote.title, score: sr.score, path: pathTitle }; + }); + + res.json({ results: filteredResults }); + }); } module.exports = { From 7729aad1e950d691a8e156160e9a36937a2aeff4 Mon Sep 17 00:00:00 2001 From: Zack Rauen Date: Sat, 30 Sep 2023 00:44:10 -0400 Subject: [PATCH 2/4] Allow external link notes in share tree --- src/share/content_renderer.js | 13 ++++++++++--- src/views/share/page.ejs | 11 ++++++++--- src/views/share/tree_item.ejs | 8 +++++++- 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/share/content_renderer.js b/src/share/content_renderer.js index 8c1dbee10e..1870f039b4 100644 --- a/src/share/content_renderer.js +++ b/src/share/content_renderer.js @@ -44,8 +44,10 @@ function renderIndex(result) { const rootNote = shaca.getNote(shareRoot.SHARE_ROOT_NOTE_ID); for (const childNote of rootNote.getChildNotes()) { - const href = childNote.getLabelValue("shareExternal") ?? `./${childNote.shareId}`; - result.content += `
  • ${childNote.escapedTitle}
  • `; + const isExternalLink = childNote.hasLabel("shareExternalLink"); + const href = isExternalLink ? childNote.getLabelValue("shareExternalLink") : `./${childNote.shareId}`; + const target = isExternalLink ? `target="_blank" rel="noopener noreferrer"` : ""; + result.content += `
  • ${childNote.escapedTitle}
  • `; } result.content += ''; @@ -85,8 +87,13 @@ function renderText(result, note) { const noteId = notePathSegments[notePathSegments.length - 1]; const linkedNote = shaca.getNote(noteId); if (linkedNote) { - const href = linkedNote.getLabelValue("shareExternal") ?? `./${linkedNote.shareId}`; + const isExternalLink = linkedNote.hasLabel("shareExternalLink"); + const href = isExternalLink ? linkedNote.getLabelValue("shareExternalLink") : `./${linkedNote.shareId}`; linkEl.setAttribute("href", href); + if (isExternalLink) { + linkEl.setAttribute("target", "_blank"); + linkEl.setAttribute("rel", "noopener noreferrer"); + } linkEl.classList.add(`type-${linkedNote.type}`); } else { linkEl.removeAttribute("href"); diff --git a/src/views/share/page.ejs b/src/views/share/page.ejs index 52e891dbcf..9d95a155a7 100644 --- a/src/views/share/page.ejs +++ b/src/views/share/page.ejs @@ -32,7 +32,7 @@ <%- header %> <%= note.title %> - +
    <% if (note.parents[0].noteId !== '_share' && note.parents.length !== 0) { %> @@ -62,9 +62,14 @@ <% } %>
      - <% for (const childNote of note.getVisibleChildNotes()) { %> + <% + for (const childNote of note.getVisibleChildNotes()) { + const isExternalLink = childNote.hasLabel('shareExternalLink'); + const linkHref = isExternalLink ? childNote.getLabelValue('shareExternalLink') : `./${childNote.shareId}`; + const target = isExternalLink ? `target="_blank" rel="noopener noreferrer"` : ''; + %>
    • - class="type-<%= childNote.type %>"><%= childNote.title %>
    • <% } %> diff --git a/src/views/share/tree_item.ejs b/src/views/share/tree_item.ejs index 12eec3fbb9..f139be8acd 100644 --- a/src/views/share/tree_item.ejs +++ b/src/views/share/tree_item.ejs @@ -1,10 +1,16 @@ +<% +const isExternalLink = note.hasLabel('shareExternalLink'); +const linkHref = isExternalLink ? note.getLabelValue('shareExternalLink') : `./${note.shareId}`; +const target = isExternalLink ? ` target="_blank" rel="noopener noreferrer"` : ''; +console.log(note.shareId, note.title, isExternalLink, linkHref, target); +%>

      <% const titleWithPrefix = (branch.prefix ? `${branch.prefix} - ` : '') + note.title; %> <% if (activeNote.noteId === note.noteId) { %> <%= titleWithPrefix %> <% } else { %> - <%= titleWithPrefix %> + ><%= titleWithPrefix %> <% } %>

      From d259931bd24fd3853f2bdc87694df55db36a47c1 Mon Sep 17 00:00:00 2001 From: Zack Rauen Date: Sat, 30 Sep 2023 00:48:34 -0400 Subject: [PATCH 3/4] Add new attributes in appropriate locations --- bin/tpl/anonymize-database.sql | 4 ++++ src/public/app/widgets/attribute_widgets/attribute_detail.js | 2 ++ src/services/builtin_attributes.js | 2 ++ 3 files changed, 8 insertions(+) diff --git a/bin/tpl/anonymize-database.sql b/bin/tpl/anonymize-database.sql index 3e8279eeb6..231be94b1b 100644 --- a/bin/tpl/anonymize-database.sql +++ b/bin/tpl/anonymize-database.sql @@ -47,6 +47,7 @@ UPDATE attributes SET name = 'name', value = 'value' 'top', 'fullContentWidth', 'shareHiddenFromTree', + 'shareExternalLink', 'shareAlias', 'shareOmitDefaultCss', 'shareRoot', @@ -67,6 +68,7 @@ UPDATE attributes SET name = 'name', value = 'value' 'renderNote', 'shareCss', 'shareJs', + 'shareTemplate', 'shareFavicon', 'executeButton', 'keepCurrentHoisting', @@ -122,6 +124,7 @@ UPDATE attributes SET name = 'name' 'top', 'fullContentWidth', 'shareHiddenFromTree', + 'shareExternalLink', 'shareAlias', 'shareOmitDefaultCss', 'shareRoot', @@ -142,6 +145,7 @@ UPDATE attributes SET name = 'name' 'renderNote', 'shareCss', 'shareJs', + 'shareTemplate', 'shareFavicon', 'executeButton', 'keepCurrentHoisting', diff --git a/src/public/app/widgets/attribute_widgets/attribute_detail.js b/src/public/app/widgets/attribute_widgets/attribute_detail.js index 715d3f093f..db209e168a 100644 --- a/src/public/app/widgets/attribute_widgets/attribute_detail.js +++ b/src/public/app/widgets/attribute_widgets/attribute_detail.js @@ -225,6 +225,7 @@ const ATTR_HELP = { "sqlConsoleHome": "default location of SQL console notes", "bookmarkFolder": "note with this label will appear in bookmarks as folder (allowing access to its children)", "shareHiddenFromTree": "this note is hidden from left navigation tree, but still accessible with its URL", + "shareExternalLink": "note will act as a link to an external website in the share tree", "shareAlias": "define an alias using which the note will be available under https://your_trilium_host/share/[your_alias]", "shareOmitDefaultCss": "default share page CSS will be omitted. Use when you make extensive styling changes.", "shareRoot": "marks note which is served on /share root.", @@ -271,6 +272,7 @@ const ATTR_HELP = { "widget": "target of this relation will be executed and rendered as a widget in the sidebar", "shareCss": "CSS note which will be injected into the share page. CSS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree' and 'shareOmitDefaultCss' as well.", "shareJs": "JavaScript note which will be injected into the share page. JS note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.", + "shareTemplate": "Embedded JavaScript note that will be used as the template for displaying the shared note. Falls back to the default template. Consider using 'shareHiddenFromTree'.", "shareFavicon": "Favicon note to be set in the shared page. Typically you want to set it to share root and make it inheritable. Favicon note must be in the shared sub-tree as well. Consider using 'shareHiddenFromTree'.", } }; diff --git a/src/services/builtin_attributes.js b/src/services/builtin_attributes.js index 5083f3dc76..0c2fa5f0ce 100644 --- a/src/services/builtin_attributes.js +++ b/src/services/builtin_attributes.js @@ -48,6 +48,7 @@ module.exports = [ { type: 'label', name: 'bottom' }, { type: 'label', name: 'fullContentWidth' }, { type: 'label', name: 'shareHiddenFromTree' }, + { type: 'label', name: 'shareExternalLink' }, { type: 'label', name: 'shareAlias' }, { type: 'label', name: 'shareOmitDefaultCss' }, { type: 'label', name: 'shareRoot' }, @@ -89,5 +90,6 @@ module.exports = [ { type: 'relation', name: 'renderNote', isDangerous: true }, { type: 'relation', name: 'shareCss' }, { type: 'relation', name: 'shareJs' }, + { type: 'relation', name: 'shareTemplate' }, { type: 'relation', name: 'shareFavicon' }, ]; From ec6b8476f9aa7a0bc516b88b3e5e7708a1dfef8f Mon Sep 17 00:00:00 2001 From: Zack Rauen Date: Sun, 8 Oct 2023 14:54:37 -0400 Subject: [PATCH 4/4] Adjust shared notes search api --- src/share/routes.js | 24 ++++++++++++------------ src/views/share/page.ejs | 2 +- src/views/share/tree_item.ejs | 1 - 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/share/routes.js b/src/share/routes.js index 80fa3423f8..a22691ce27 100644 --- a/src/share/routes.js +++ b/src/share/routes.js @@ -333,31 +333,31 @@ function register(router) { }); // Used for searching, require noteId so we know the subTreeRoot - router.get('/share/api/search/:noteId', (req, res, next) => { + router.get('/share/api/notes', (req, res, next) => { shacaLoader.ensureLoad(); + const ancestorNoteId = req.query.ancestorNoteId ?? "_share"; let note; - if (!(note = checkNoteAccess(req.params.noteId, req, res))) { + // This will automatically return if no ancestorNoteId is provided and there is no shareIndex + if (!(note = checkNoteAccess(ancestorNoteId, req, res))) { return; } - const {query} = req.query; + const {search} = req.query; - if (!query?.trim()) { - return res.status(400).json({ message: "'query' parameter is mandatory." }); + if (!search?.trim()) { + return res.status(400).json({ message: "'search' parameter is mandatory." }); } - const subRootPath = getSharedSubTreeRoot(note); - const subRoot = subRootPath.note; - const searchContext = new SearchContext({ancestorNoteId: subRoot.noteId}); - const searchResults = searchService.findResultsWithQuery(query, searchContext); + const searchContext = new SearchContext({ancestorNoteId: ancestorNoteId}); + const searchResults = searchService.findResultsWithQuery(search, searchContext); const filteredResults = searchResults.map(sr => { const fullNote = shaca.notes[sr.noteId]; - const startIndex = sr.notePathArray.indexOf(subRoot.noteId); - const localPathArray = sr.notePathArray.slice(startIndex + 1); + const startIndex = sr.notePathArray.indexOf(ancestorNoteId); + const localPathArray = sr.notePathArray.slice(startIndex + 1).filter(id => shaca.notes[id]); const pathTitle = localPathArray.map(id => shaca.notes[id].title).join(" / "); - return { id: fullNote.noteId, title: fullNote.title, score: sr.score, path: pathTitle }; + return { id: fullNote.shareId, title: fullNote.title, score: sr.score, path: pathTitle }; }); res.json({ results: filteredResults }); diff --git a/src/views/share/page.ejs b/src/views/share/page.ejs index 9d95a155a7..8c6c3033d5 100644 --- a/src/views/share/page.ejs +++ b/src/views/share/page.ejs @@ -32,7 +32,7 @@ <%- header %> <%= note.title %> - +
      <% if (note.parents[0].noteId !== '_share' && note.parents.length !== 0) { %> diff --git a/src/views/share/tree_item.ejs b/src/views/share/tree_item.ejs index f139be8acd..9953170363 100644 --- a/src/views/share/tree_item.ejs +++ b/src/views/share/tree_item.ejs @@ -2,7 +2,6 @@ const isExternalLink = note.hasLabel('shareExternalLink'); const linkHref = isExternalLink ? note.getLabelValue('shareExternalLink') : `./${note.shareId}`; const target = isExternalLink ? ` target="_blank" rel="noopener noreferrer"` : ''; -console.log(note.shareId, note.title, isExternalLink, linkHref, target); %>

      <% const titleWithPrefix = (branch.prefix ? `${branch.prefix} - ` : '') + note.title; %>