Merge branch 'master' into next50

This commit is contained in:
zadam 2022-01-03 20:10:48 +01:00
commit ffdd917717
23 changed files with 5920 additions and 62 deletions

5734
libraries/codemirror/keymap/vim.js vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"name": "trilium",
"productName": "Trilium Notes",
"description": "Trilium Notes",
"version": "0.49.1-beta",
"version": "0.49.2-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"bin": {

View File

@ -1,7 +1,15 @@
import mimeTypesService from "../../services/mime_types.js";
import options from "../../services/options.js";
import server from "../../services/server.js";
import toastService from "../../services/toast.js";
import utils from "../../services/utils.js";
const TPL = `
<h4>Use vim keybindings in CodeNotes (no ex mode)</h4>
<div class="custom-control custom-checkbox">
<input type="checkbox" class="custom-control-input" id="vim-keymap-enabled">
<label class="custom-control-label" for="vim-keymap-enabled">Enable Vim Keybindings</label>
</div>
<h4>Available MIME types in the dropdown</h4>
<ul id="options-mime-types" style="max-height: 500px; overflow: auto; list-style-type: none;"></ul>`;
@ -10,12 +18,18 @@ export default class CodeNotesOptions {
constructor() {
$("#options-code-notes").html(TPL);
this.$vimKeymapEnabled = $("#vim-keymap-enabled");
this.$vimKeymapEnabled.on('change', () => {
const opts = { 'vimKeymapEnabled': this.$vimKeymapEnabled.is(":checked") ? "true" : "false" };
server.put('options', opts).then(() => toastService.showMessage("Options change have been saved."));
return false;
});
this.$mimeTypes = $("#options-mime-types");
}
async optionsLoaded() {
async optionsLoaded(options) {
this.$mimeTypes.empty();
this.$vimKeymapEnabled.prop("checked", options['vimKeymapEnabled'] === 'true');
let idCtr = 1;
for (const mimeType of await mimeTypesService.getMimeTypes()) {
@ -45,4 +59,4 @@ export default class CodeNotesOptions {
mimeTypesService.loadMimeTypes();
}
}
}

View File

@ -10,6 +10,7 @@ const CODE_MIRROR = {
"libraries/codemirror/addon/edit/matchtags.js",
"libraries/codemirror/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js",
"libraries/codemirror/keymap/vim.js",
"libraries/codemirror/addon/lint/lint.js",
"libraries/codemirror/addon/lint/eslint.js"
],

View File

@ -129,12 +129,7 @@ export default class TabManager extends Component {
window.history.pushState(null, "", url);
}
const titleFragments = [
// it helps navigating in history if note title is included in the title
activeNoteContext.note?.title,
"Trilium Notes"
].filter(Boolean);
document.title = titleFragments.join(" - ");
this.updateDocumentTitle(activeNoteContext);
this.triggerEvent('activeNoteChanged'); // trigger this even in on popstate event
}
@ -453,4 +448,22 @@ export default class TabManager extends Component {
hoistedNoteChangedEvent() {
this.tabsUpdate.scheduleUpdate();
}
updateDocumentTitle(activeNoteContext) {
const titleFragments = [
// it helps navigating in history if note title is included in the title
activeNoteContext.note?.title,
"Trilium Notes"
].filter(Boolean);
document.title = titleFragments.join(" - ");
}
entitiesReloadedEvent({loadResults}) {
const activeContext = this.getActiveContext();
if (activeContext && loadResults.isNoteReloaded(activeContext.noteId)) {
this.updateDocumentTitle(activeContext);
}
}
}

21
src/public/app/share.js Normal file
View File

@ -0,0 +1,21 @@
/**
* Fetch note with given ID from backend
*
* @param noteId of the given note to be fetched. If falsy, fetches current note.
*/
async function fetchNote(noteId = null) {
if (!noteId) {
noteId = document.body.getAttribute("data-note-id");
}
const resp = await fetch(`api/notes/${noteId}`);
return await resp.json();
}
document.addEventListener('DOMContentLoaded', () => {
const toggleMenuButton = document.getElementById('toggleMenuButton');
const layout = document.getElementById('layout');
toggleMenuButton.addEventListener('click', () => layout.classList.toggle('showMenu'));
}, false);

View File

@ -226,6 +226,8 @@ const ATTR_HELP = {
"renderNote": 'notes of type "render HTML note" will be rendered using a code note (HTML or script) and it is necessary to point using this relation to which note should be rendered',
"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'.",
"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'.",
}
};

View File

@ -6,6 +6,7 @@ import ws from "../../services/ws.js";
import appContext from "../../services/app_context.js";
import toastService from "../../services/toast.js";
import treeService from "../../services/tree.js";
import options from "../../services/options.js";
const TPL = `
<div class="note-detail-code note-detail-printable">
@ -94,6 +95,7 @@ export default class EditableCodeTypeWidget extends TypeWidget {
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
keyMap: options.is('vimKeymapEnabled') ? "vim": "default",
matchTags: {bothTags: true},
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false},
lint: true,

View File

@ -117,6 +117,15 @@ iframe.pdf-view {
margin-right: 20px;
}
#noteClippedFrom {
padding: 10px 0 10px 0;
margin: 20px 0 20px 0;
color: #666;
border: 1px solid #ddd;
border-left: 0;
border-right: 0;
}
#toggleMenuButton::after {
position: relative;
top: -2px;

View File

@ -32,6 +32,7 @@ const ALLOWED_OPTIONS = new Set([
'similarNotesWidget',
'editedNotesWidget',
'calendarWidget',
'vimKeymapEnabled',
'codeNotesMimeTypes',
'spellCheckEnabled',
'spellCheckLanguageCode',

View File

@ -67,6 +67,8 @@ const BUILTIN_ATTRIBUTES = [
{ type: 'relation', name: 'widget', isDangerous: true },
{ type: 'relation', name: 'renderNote', isDangerous: true },
{ type: 'relation', name: 'shareCss', isDangerous: false },
{ type: 'relation', name: 'shareJs', isDangerous: false },
{ type: 'relation', name: 'shareFavicon', isDangerous: false },
];
/** @returns {Note[]} */

View File

@ -1 +1 @@
module.exports = { buildDate:"2021-12-24T23:05:10+01:00", buildRevision: "0217b1c85de9a2824e7f07d07a357064c5803383" };
module.exports = { buildDate:"2022-01-02T22:43:30+01:00", buildRevision: "feffd57f240438d107c1ed1c1772545611a97dee" };

View File

@ -3,6 +3,10 @@ const sanitizeHtml = require('sanitize-html');
// intended mainly as protection against XSS via import
// secondarily it (partly) protects against "CSS takeover"
function sanitize(dirtyHtml) {
if (!dirtyHtml) {
return dirtyHtml;
}
// avoid H1 per https://github.com/zadam/trilium/issues/1552
// demote H1, and if that conflicts with existing H2, demote that, etc
const transformTags = {};

View File

@ -51,7 +51,7 @@ async function importOpml(taskContext, fileBuffer, parentNote) {
throw new Error("Unrecognized OPML version " + opmlVersion);
}
content = htmlSanitizer.sanitize(content);
content = htmlSanitizer.sanitize(content || "");
const {note} = noteService.createNewNote({
parentNoteId,

View File

@ -1,7 +1,16 @@
const becca = require('../becca/becca');
const sql = require("./sql.js");
function getOption(name) {
const option = require('../becca/becca').getOption(name);
let option;
if (becca.loaded) {
option = becca.getOption(name);
}
else {
// e.g. in initial sync becca is not loaded because DB is not initialized
option = sql.getRow("SELECT * FROM options WHERE name = ?", name);
}
if (!option) {
throw new Error(`Option "${name}" doesn't exist`);
@ -39,12 +48,12 @@ function getOptionBool(name) {
}
function setOption(name, value) {
const option = becca.getOption(name);
if (value === true || value === false) {
value = value.toString();
}
const option = becca.getOption(name);
if (option) {
option.value = value;

View File

@ -53,6 +53,7 @@ const defaultOptions = [
{ name: 'imageMaxWidthHeight', value: '2000', isSynced: true },
{ name: 'imageJpegQuality', value: '75', isSynced: true },
{ name: 'autoFixConsistencyIssues', value: 'true', isSynced: false },
{ name: 'vimKeymapEnabled', value: 'false', isSynced: false },
{ name: 'codeNotesMimeTypes', value: '["text/x-csrc","text/x-c++src","text/x-csharp","text/css","text/x-go","text/x-groovy","text/x-haskell","text/html","message/http","text/x-java","application/javascript;env=frontend","application/javascript;env=backend","application/json","text/x-kotlin","text/x-markdown","text/x-perl","text/x-php","text/x-python","text/x-ruby",null,"text/x-sql","text/x-sqlite;schema=trilium","text/x-swift","text/xml","text/x-yaml"]', isSynced: true },
{ name: 'leftPaneWidth', value: '25', isSynced: false },
{ name: 'leftPaneVisible', value: 'true', isSynced: false },

View File

@ -371,7 +371,10 @@ function getLastSyncedPull() {
function setLastSyncedPull(entityChangeId) {
const lastSyncedPullOption = becca.getOption('lastSyncedPull');
lastSyncedPullOption.value = entityChangeId + '';
if (lastSyncedPullOption) { // might be null in initial sync when becca is not loaded
lastSyncedPullOption.value = entityChangeId + '';
}
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPull']);
@ -389,7 +392,10 @@ function setLastSyncedPush(entityChangeId) {
ws.setLastSyncedPush(entityChangeId);
const lastSyncedPushOption = becca.getOption('lastSyncedPush');
lastSyncedPushOption.value = entityChangeId + '';
if (lastSyncedPushOption) { // might be null in initial sync when becca is not loaded
lastSyncedPushOption.value = entityChangeId + '';
}
// this way we avoid updating entity_changes which otherwise means that we've never pushed all entity_changes
sql.execute("UPDATE options SET value = ? WHERE name = ?", [entityChangeId, 'lastSyncedPush']);

View File

@ -16,7 +16,10 @@ let mainWindow;
let setupWindow;
async function createExtraWindow(notePath, hoistedNoteId = 'root') {
const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled');
const {BrowserWindow} = require('electron');
const win = new BrowserWindow({
width: 1000,
height: 800,
@ -25,7 +28,7 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') {
enableRemoteModule: true,
nodeIntegration: true,
contextIsolation: false,
spellcheck: optionService.getOptionBool('spellCheckEnabled')
spellcheck: spellcheckEnabled
},
frame: optionService.getOptionBool('nativeTitleBarVisible'),
icon: getIcon()
@ -33,6 +36,8 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') {
win.setMenuBarVisibility(false);
win.loadURL('http://127.0.0.1:' + await port + '/?extra=1&extraHoistedNoteId=' + hoistedNoteId + '#' + notePath);
configureWebContents(win.webContents, spellcheckEnabled);
}
ipcMain.on('create-extra-window', (event, arg) => {
@ -74,8 +79,10 @@ async function createMainWindow() {
mainWindow.loadURL('http://127.0.0.1:' + await port);
mainWindow.on('closed', () => mainWindow = null);
const {webContents} = mainWindow;
configureWebContents(mainWindow.webContents, spellcheckEnabled);
}
function configureWebContents(webContents, spellcheckEnabled) {
require("@electron/remote/main").enable(webContents);
webContents.on('new-window', (e, url) => {

View File

@ -46,19 +46,15 @@ function register(router) {
}
});
router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
const image = shaca.getNote(req.params.noteId);
router.get('/share/api/notes/:noteId', (req, res, next) => {
const {noteId} = req.params;
const note = shaca.getNote(noteId);
if (!image) {
return res.status(404).send("Not found");
}
else if (image.type !== 'image') {
return res.status(400).send("Requested note is not an image");
if (!note) {
return res.status(404).send(`Note ${noteId} not found`);
}
res.set('Content-Type', image.mime);
res.send(image.getContent());
res.json(note.getPojoWithAttributes());
});
router.get('/share/api/notes/:noteId/download', (req, res, next) => {
@ -66,7 +62,7 @@ function register(router) {
const note = shaca.getNote(noteId);
if (!note) {
return res.status(404).send(`Not found`);
return res.status(404).send(`Note ${noteId} not found`);
}
const utils = require("../services/utils");
@ -81,20 +77,30 @@ function register(router) {
res.send(note.getContent());
});
router.get('/share/api/images/:noteId/:filename', (req, res, next) => {
const image = shaca.getNote(req.params.noteId);
if (!image) {
return res.status(404).send(`Note ${noteId} not found`);
}
else if (image.type !== 'image') {
return res.status(400).send("Requested note is not an image");
}
res.set('Content-Type', image.mime);
res.send(image.getContent());
});
// used for PDF viewing
router.get('/share/api/notes/:noteId/view', (req, res, next) => {
const {noteId} = req.params;
const note = shaca.getNote(noteId);
if (!note) {
return res.status(404).send(`Not found`);
return res.status(404).send(`Note ${noteId} not found`);
}
const utils = require("../services/utils");
const filename = utils.formatDownloadTitle(note.title, note.type, note.mime);
// res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
res.setHeader('Content-Type', note.mime);

View File

@ -89,6 +89,18 @@ class Attribute extends AbstractEntity {
return this.shaca.getNote(this.value);
}
getPojo() {
return {
attributeId: this.attributeId,
noteId: this.noteId,
type: this.type,
name: this.name,
position: this.position,
value: this.value,
isInheritable: this.isInheritable
};
}
}
module.exports = Attribute;

View File

@ -410,6 +410,19 @@ class Note extends AbstractEntity {
return sharedAlias || this.noteId;
}
getPojoWithAttributes() {
return {
noteId: this.noteId,
title: this.title,
type: this.type,
mime: this.mime,
utcDateModified: this.utcDateModified,
attributes: this.getAttributes().map(attr => attr.getPojo()),
parentNoteIds: this.parents.map(parentNote => parentNote.noteId),
childNoteIds: this.children.map(child => child.noteId)
};
}
}
module.exports = Note;

View File

@ -59,11 +59,7 @@ function load() {
SELECT attributeId, noteId, type, name, value, isInheritable, position, utcDateModified
FROM attributes
WHERE isDeleted = 0
AND noteId IN (${noteIdStr})
AND (
(type = 'label' AND name IN ('archived', 'shareHiddenFromTree', 'shareAlias', 'shareOmitDefaultCss'))
OR (type = 'relation' AND name IN ('imageLink', 'template', 'shareCss'))
)`, []);
AND noteId IN (${noteIdStr})`);
for (const row of rawAttributeRows) {
new Attribute(row);

View File

@ -2,7 +2,12 @@
<html lang="en">
<head>
<meta charset="utf-8">
<% if (note.hasRelation("shareFavicon")) { %>
<link rel="shortcut icon" href="api/notes/<%= note.getRelation("shareFavicon").value %>/download">
<% } else { %>
<link rel="shortcut icon" href="../favicon.ico">
<% } %>
<script src="../app/share.js"></script>
<% if (!note.hasLabel("shareOmitDefaultCss")) { %>
<link href="../libraries/normalize.min.css" rel="stylesheet">
<link href="../stylesheets/share.css" rel="stylesheet">
@ -13,20 +18,28 @@
<% for (const cssRelation of note.getRelations("shareCss")) { %>
<link href="api/notes/<%= cssRelation.value %>/download" rel="stylesheet">
<% } %>
<% for (const jsRelation of note.getRelations("shareJs")) { %>
<script type="module" src="api/notes/<%= jsRelation.value %>/download"></script>
<% } %>
<%- header %>
<title><%= note.title %></title>
</head>
<body>
<body data-note-id="<%= note.noteId %>">
<div id="layout">
<div id="main">
<% if (note.parents[0].noteId !== 'share' && note.parents.length !== 0) { %>
<nav id="parentLink">
parent: <a href="<%= note.parents[0].noteId %>" class="type-<%= note.parents[0].type %>"><%= note.parents[0].title %></a>
parent: <a href="<%= note.parents[0].noteId %>"
class="type-<%= note.parents[0].type %>"><%= note.parents[0].title %></a>
</nav>
<% } %>
<h1 id="title"><%= note.title %></h1>
<% if (note.hasLabel("pageUrl")) { %>
<div id="noteClippedFrom">This note was originally clipped from <a href="<%= note.getLabelValue("pageUrl") %>"><%= note.getLabelValue("pageUrl") %></a></div>
<% } %>
<% if (note.type === 'book') { %>
<% } else if (isEmpty) { %>
<p>This note has no content.</p>
@ -39,18 +52,19 @@
<% if (note.hasChildren()) { %>
<nav id="childLinks" class="<% if (isEmpty) { %>grid<% } else { %>list<% } %>">
<% if (!isEmpty) { %>
<hr>
<div id="noteClippedFrom">
<span>Child notes: </span>
<ul>
<% for (const childNote of note.getChildNotes()) { %>
<li>
<a href="<%= childNote.shareId %>"
class="type-<%= childNote.type %>"><%= childNote.title %></a>
</li>
<% } %>
</ul>
</div>
<% } %>
<ul>
<% for (const childNote of note.getChildNotes()) { %>
<li>
<a href="<%= childNote.shareId %>"
class="type-<%= childNote.type %>"><%= childNote.title %></a>
</li>
<% } %>
</ul>
</nav>
<% } %>
</div>
@ -63,14 +77,5 @@
</nav>
<% } %>
</div>
<script>
(function () {
const toggleMenuButton = document.getElementById('toggleMenuButton');
const layout = document.getElementById('layout');
toggleMenuButton.addEventListener('click', () => layout.classList.toggle('showMenu'));
}());
</script>
</body>
</html>