Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ~shareTemplate and #shareExternalLink #4298

Merged
merged 5 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions bin/tpl/anonymize-database.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ UPDATE attributes SET name = 'name', value = 'value' WHERE type = 'label'
'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox',
'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId',
'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top',
'fullContentWidth', 'shareHiddenFromTree', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription',
'fullContentWidth', 'shareHiddenFromTree', 'shareExternalLink', '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');
'inherit', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareTemplate', 'shareFavicon');
UPDATE attributes SET name = 'name' WHERE type = 'relation'
AND name NOT IN
('inbox', 'disableVersioning', 'calendarRoot', 'archived', 'excludeFromExport', 'disableInclusion', 'appCss',
Expand All @@ -30,13 +30,13 @@ UPDATE attributes SET name = 'name' WHERE type = 'relation'
'workspaceTabBackgroundColor', 'workspaceCalendarRoot', 'workspaceTemplate', 'searchHome', 'workspaceInbox',
'workspaceSearchHome', 'sqlConsoleHome', 'datePattern', 'pageSize', 'viewType', 'mapRootNoteId',
'bookmarkFolder', 'sorted', 'sortDirection', 'sortFoldersFirst', 'sortNatural', 'sortLocale', 'top',
'fullContentWidth', 'shareHiddenFromTree', 'shareOmitDefaultCss', 'shareRoot', 'shareDescription',
'fullContentWidth', 'shareHiddenFromTree', 'shareExternalLink', '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');
'inherit', 'widget', 'renderNote', 'shareCss', 'shareJs', 'shareTemplate', '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',
Expand Down
2 changes: 2 additions & 0 deletions src/public/app/widgets/attribute_widgets/attribute_detail.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
Expand Down Expand Up @@ -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'.",
}
};
Expand Down
2 changes: 2 additions & 0 deletions src/services/builtin_attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
Expand Down Expand Up @@ -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' },
];
13 changes: 11 additions & 2 deletions src/share/content_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ function renderIndex(result) {
const rootNote = shaca.getNote(shareRoot.SHARE_ROOT_NOTE_ID);

for (const childNote of rootNote.getChildNotes()) {
result.content += `<li><a class="${childNote.type}" href="./${childNote.shareId}">${childNote.escapedTitle}</a></li>`;
const isExternalLink = childNote.hasLabel("shareExternalLink");
const href = isExternalLink ? childNote.getLabelValue("shareExternalLink") : `./${childNote.shareId}`;
const target = isExternalLink ? `target="_blank" rel="noopener noreferrer"` : "";
result.content += `<li><a class="${childNote.type}" href="${href}" ${target}>${childNote.escapedTitle}</a></li>`;
}

result.content += '</ul>';
Expand Down Expand Up @@ -84,7 +87,13 @@ function renderText(result, note) {
const noteId = notePathSegments[notePathSegments.length - 1];
const linkedNote = shaca.getNote(noteId);
if (linkedNote) {
linkEl.setAttribute("href", 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");
Expand Down
79 changes: 69 additions & 10 deletions src/share/routes.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
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");
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
Expand Down Expand Up @@ -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};
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all currently passed to the user's custom template as well, should we only pass a subset to the user? Or are there other things that would be useful to the user template that are not currently passed?

Either way this creates a sort of pseudo-api so it would be good to be certain of what should be passed and try not to change it much going forward. But I would still call user templates an "at your own risk" type of extension.

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) => {
Expand Down Expand Up @@ -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/notes', (req, res, next) => {
shacaLoader.ensureLoad();

const ancestorNoteId = req.query.ancestorNoteId ?? "_share";
let note;

// This will automatically return if no ancestorNoteId is provided and there is no shareIndex
if (!(note = checkNoteAccess(ancestorNoteId, req, res))) {
return;
}

const {search} = req.query;

if (!search?.trim()) {
return res.status(400).json({ message: "'search' parameter is mandatory." });
}

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(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.shareId, title: fullNote.title, score: sr.score, path: pathTitle };
});

res.json({ results: filteredResults });
});
}

module.exports = {
Expand Down
11 changes: 8 additions & 3 deletions src/views/share/page.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
<%- header %>
<title><%= note.title %></title>
</head>
<body data-note-id="<%= note.noteId %>">
<body data-note-id="<%= note.noteId %>" data-ancestor-note-id="<%= subRoot.note.noteId %>">
<div id="layout">
<div id="main">
<% if (note.parents[0].noteId !== '_share' && note.parents.length !== 0) { %>
Expand Down Expand Up @@ -62,9 +62,14 @@
<% } %>

<ul>
<% 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"` : '';
%>
<li>
<a href="<%= childNote.shareId %>"
<a href="<%= linkHref %>" <%= target %>
class="type-<%= childNote.type %>"><%= childNote.title %></a>
</li>
<% } %>
Expand Down
7 changes: 6 additions & 1 deletion src/views/share/tree_item.ejs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
<%
const isExternalLink = note.hasLabel('shareExternalLink');
const linkHref = isExternalLink ? note.getLabelValue('shareExternalLink') : `./${note.shareId}`;
const target = isExternalLink ? ` target="_blank" rel="noopener noreferrer"` : '';
%>
<p>
<% const titleWithPrefix = (branch.prefix ? `${branch.prefix} - ` : '') + note.title; %>

<% if (activeNote.noteId === note.noteId) { %>
<strong><%= titleWithPrefix %></strong>
<% } else { %>
<a class="type-<%= note.type %>" href="./<%= note.shareId %>"><%= titleWithPrefix %></a>
<a class="type-<%= note.type %>" href="<%= linkHref %>"<%= target %>><%= titleWithPrefix %></a>
<% } %>
</p>

Expand Down