Compare commits

..

64 Commits

Author SHA1 Message Date
azivner
348c622845 release 0.9.1-beta 2018-03-09 00:12:22 -05:00
azivner
44bcdedaba Updated all scripts to current versions working with current script API 2018-03-09 00:10:43 -05:00
azivner
755c0f3ce2 fix exclude from export 2018-03-09 00:10:02 -05:00
azivner
895bda41b5 return only startup bundles for executale notes 2018-03-08 23:36:08 -05:00
azivner
b2df622cb6 Merge branch 'stable' 2018-03-08 23:35:17 -05:00
azivner
9ba6e6d0f5 disable inclusion should work only on non-root notes 2018-03-08 23:35:08 -05:00
azivner
a5c9180533 release 0.9.0-beta 2018-03-08 20:18:37 -05:00
azivner
e86f1e0d05 changes to the HTML template to allow more complete styling 2018-03-07 23:58:34 -05:00
azivner
b6277049f3 added support for app_css attribute, which allows custom styling 2018-03-07 23:24:23 -05:00
azivner
c831221cc4 add "play" icon for "render" note types 2018-03-07 20:52:34 -05:00
azivner
577a168714 stop propagation of ctrl+enter from SQL console, fixes #73 2018-03-07 20:46:01 -05:00
azivner
b0bd27321a escape will close SQL console, closes #72 2018-03-07 20:33:41 -05:00
azivner
90c5348ca7 fix saving only image in a note, fixes #77 2018-03-07 20:19:53 -05:00
azivner
8e95b080da fixed render notes 2018-03-07 00:17:18 -05:00
azivner
766a567a32 changes in access to startNote and currentNote 2018-03-06 23:04:35 -05:00
azivner
6d0218cb36 execute note (ctrl+enter) now works for both frontend and backend scripts 2018-03-05 23:19:46 -05:00
azivner
d26170762b inclusion of scripts based on script environment 2018-03-05 23:09:36 -05:00
azivner
b3209a9bbf split javascript mime type into frontend and backend 2018-03-05 22:08:45 -05:00
azivner
61c2456cf6 startNote/currentNote is now accessible on frontend as well 2018-03-04 23:28:26 -05:00
azivner
1c6fc9029f new "disable_inclusion" attribute 2018-03-04 22:09:51 -05:00
azivner
5c91e38dfe server.exec() refactored into api 2018-03-04 21:43:14 -05:00
azivner
07bf075894 cleaned up unused jobs implementation 2018-03-04 21:33:06 -05:00
azivner
ddce5c959e fix render 2018-03-04 21:05:14 -05:00
azivner
3b9d1df05c fixed frontend script execution 2018-03-04 14:21:11 -05:00
azivner
d239ef2956 refactoring of getModules function 2018-03-04 12:06:35 -05:00
azivner
7a865a9081 common JS module system prototype 2018-03-04 10:32:53 -05:00
azivner
83d6c2970f added versioning to the metadata files in export tars 2018-03-03 09:32:21 -05:00
azivner
8c7d159012 fix export/import for multi-valued attributes 2018-03-03 09:30:18 -05:00
azivner
d169f67901 changes in backend script running 2018-03-03 09:11:41 -05:00
azivner
982b723647 basic scheduling of backend scripts using attributes 2018-03-02 20:56:58 -05:00
azivner
31d5ac05ff release 0.8.1 2018-03-01 23:08:53 -05:00
azivner
72d91d1571 don't use eslint on JSON notes, closes #70 2018-03-01 22:42:51 -05:00
azivner
f4b57f4c57 Allow attachments to be included in the scripts, closes #66 2018-03-01 22:30:06 -05:00
azivner
ee0833390a fix export in electron (auth problem) 2018-02-27 09:47:05 -05:00
azivner
2acff07368 release 0.8.0-beta 2018-02-26 22:57:15 -05:00
azivner
bea1d24f07 tweaks to eslint 2018-02-26 22:55:58 -05:00
azivner
adc270c59f removed reference to reddit plugin 2018-02-26 22:31:35 -05:00
azivner
66064f7a94 Script API changes, finished porting reddit plugin, reddit importer tar file 2018-02-26 20:47:34 -05:00
azivner
1501fa8dbf import notes from tar archive, closes #63 2018-02-26 00:07:43 -05:00
azivner
60bba46d80 export subtree to tar file 2018-02-25 10:55:21 -05:00
azivner
12c06ae97e manual transaction handling for jobs 2018-02-24 22:44:45 -05:00
azivner
f0bea9cf71 API changes necessary to port reddit plugin, closes #58 2018-02-24 21:23:04 -05:00
azivner
a555b6319c support for backend jobs and other script API changes 2018-02-24 14:42:52 -05:00
azivner
5dd93e4cdc eslint for javascript inside HTML (htmlmixed mode), closes #62 2018-02-24 00:58:11 -05:00
azivner
3b4509d833 support encryption for files, closes #60 2018-02-23 22:58:24 -05:00
azivner
19308bbfbd small changes to linting and protected session 2018-02-23 20:10:29 -05:00
azivner
4acc5432c3 autocomplete returns items which have at least one of the tokens in the leaf note title, closes #59 2018-02-22 19:52:08 -05:00
azivner
08b8141fdf upgrade to codemirror 5.35 2018-02-21 23:09:52 -05:00
azivner
e1200aa308 lazy loading of eslint only for JS code 2018-02-21 20:30:15 -05:00
azivner
89666eb078 paperclip icon for attachment, closes #61 2018-02-21 19:53:46 -05:00
azivner
d5605aa64d initial support for eslint backed JS linting 2018-02-20 23:24:55 -05:00
azivner
2582b016f9 increased "connection lost" timeout from 5 seconds to 30, it was way to common and mostly false positive 2018-02-20 07:52:39 -05:00
azivner
e8c52e25f0 release 0.7.0-beta 2018-02-19 23:03:30 -05:00
azivner
a149c6a105 lazy / dynamic loading of CKEditor and Code mirror 2018-02-19 22:02:03 -05:00
azivner
131af9ab12 fix attachment sync 2018-02-18 22:55:36 -05:00
azivner
aa2bbc6575 attachment download now works also in electron, added option to open the attachment 2018-02-18 22:19:07 -05:00
azivner
78e8c15786 attachment upload and download now works for browser 2018-02-18 21:28:24 -05:00
azivner
fda4146150 correct handling of inclusion of dependencies 2018-02-18 10:47:02 -05:00
azivner
ddc885066e support passing functions to the backend as parameters 2018-02-18 09:53:36 -05:00
azivner
08bc2afb49 now it's possible to add comment to the weight, closes #54 2018-02-17 11:47:22 -05:00
azivner
1d0220b03d add weight causes updating old chart instead of creating new chart, closes #53 2018-02-17 10:45:00 -05:00
azivner
3033f7cc08 attribute value is now non-null, fixes #52 2018-02-16 19:07:59 -05:00
azivner
6b9ff47c88 Merge branch 'stable' 2018-02-15 23:24:02 -05:00
azivner
cdde6a4d8e file/attachment upload, wiP 2018-02-14 23:31:20 -05:00
67 changed files with 106853 additions and 4455 deletions

View File

@@ -1,3 +1,7 @@
[General]
# Instance name can be used to distinguish between different instances
instanceName=
[Network]
port=8080
# true for TLS/SSL/HTTPS (secure), false for HTTP (unsecure).

View File

@@ -0,0 +1,23 @@
UPDATE attributes SET value = '' WHERE value IS NULL;
CREATE TABLE IF NOT EXISTS "attributes_mig"
(
attributeId TEXT PRIMARY KEY NOT NULL,
noteId TEXT NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL DEFAULT '',
position INT NOT NULL DEFAULT 0,
dateCreated TEXT NOT NULL,
dateModified TEXT NOT NULL,
isDeleted INT NOT NULL
);
INSERT INTO attributes_mig (attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted)
SELECT attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted FROM attributes;
DROP TABLE attributes;
ALTER TABLE attributes_mig RENAME TO attributes;
CREATE INDEX IDX_attributes_noteId ON attributes (noteId);
CREATE INDEX IDX_attributes_name_value ON attributes (name, value);

View File

@@ -0,0 +1 @@
UPDATE notes SET mime = 'application/javascript;env=frontend' WHERE type = 'code' AND mime = 'application/javascript';

View File

@@ -15,6 +15,8 @@ require('electron-debug')();
// Prevent window being garbage collected
let mainWindow;
require('electron-dl')({ saveAs: true });
function onClosed() {
// Dereference the window
// For multiple windows store them in an array

72
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "trilium",
"version": "0.6.1",
"version": "0.7.0-beta",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -3206,6 +3206,16 @@
"electron-localshortcut": "3.1.0"
}
},
"electron-dl": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-1.11.0.tgz",
"integrity": "sha512-iL9qHzzWOuL9bus+UT+P72SwrDQcFTV6QHqcbhwgqjCC9/K5jhdRzG0dIMB3TzYlk6rmApanPqh9DvWykwIH1Q==",
"requires": {
"ext-name": "5.0.0",
"pupa": "1.0.0",
"unused-filename": "1.0.0"
}
},
"electron-download": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz",
@@ -4374,6 +4384,23 @@
}
}
},
"ext-list": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz",
"integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==",
"requires": {
"mime-db": "1.30.0"
}
},
"ext-name": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz",
"integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==",
"requires": {
"ext-list": "2.2.2",
"sort-keys-length": "1.0.1"
}
},
"extend": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
@@ -5922,8 +5949,7 @@
"is-plain-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz",
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=",
"dev": true
"integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4="
},
"is-png": {
"version": "1.1.0",
@@ -7152,6 +7178,11 @@
}
}
},
"modify-filename": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz",
"integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE="
},
"moment": {
"version": "2.20.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz",
@@ -7564,6 +7595,11 @@
"mimic-fn": "1.1.0"
}
},
"open": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz",
"integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw="
},
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
@@ -8391,6 +8427,11 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
},
"pupa": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/pupa/-/pupa-1.0.0.tgz",
"integrity": "sha1-mpVopa9+ZXuEYqbp1TKHQ1YM7/Y="
},
"q": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
@@ -9186,11 +9227,18 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz",
"integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=",
"dev": true,
"requires": {
"is-plain-obj": "1.1.0"
}
},
"sort-keys-length": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz",
"integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=",
"requires": {
"sort-keys": "1.1.2"
}
},
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -10963,6 +11011,22 @@
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
"integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
},
"unused-filename": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-1.0.0.tgz",
"integrity": "sha1-00CID3GuIRXrqhMlvvBcxmhEacY=",
"requires": {
"modify-filename": "1.1.0",
"path-exists": "3.0.0"
},
"dependencies": {
"path-exists": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
}
}
},
"unzip-response": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "trilium",
"description": "Trilium Notes",
"version": "0.6.2",
"version": "0.9.1-beta",
"license": "AGPL-3.0-only",
"main": "electron.js",
"repository": {
@@ -29,6 +29,7 @@
"ejs": "~2.5.7",
"electron": "^1.8.2",
"electron-debug": "^1.5.0",
"electron-dl": "^1.11.0",
"electron-in-page-search": "^1.2.4",
"express": "~4.16.2",
"express-promise-wrap": "^0.2.2",
@@ -45,6 +46,7 @@
"jimp": "^0.2.28",
"moment": "^2.20.1",
"multer": "^1.3.0",
"open": "0.0.5",
"rand-token": "^0.4.0",
"request": "^2.83.0",
"request-promise": "^4.2.2",
@@ -55,6 +57,7 @@
"session-file-store": "^1.1.2",
"simple-node-logger": "^0.93.30",
"sqlite": "^2.9.0",
"tar-stream": "^1.5.5",
"unescape": "^1.0.1",
"ws": "^3.3.2"
},

View File

@@ -73,7 +73,7 @@ require('./services/backup');
// trigger consistency checks timer
require('./services/consistency_checks');
require('./plugins/reddit');
require('./services/scheduler');
module.exports = {
app,

View File

@@ -23,10 +23,53 @@ class Note extends Entity {
return this.type === "code" && this.mime === "application/json";
}
isJavaScript() {
return (this.type === "code" || this.type === "file")
&& (this.mime.startsWith("application/javascript") || this.mime === "application/x-javascript");
}
isHtml() {
return (this.type === "code" || this.type === "file") && this.mime === "text/html";
}
getScriptEnv() {
if (this.isHtml() || (this.isJavaScript() && this.mime.endsWith('env=frontend'))) {
return "frontend";
}
if (this.type === 'render') {
return "frontend";
}
if (this.isJavaScript() && this.mime.endsWith('env=backend')) {
return "backend";
}
return null;
}
async getAttributes() {
return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]);
}
// WARNING: this doesn't take into account the possibility to have multi-valued attributes!
async getAttributeMap() {
const map = {};
for (const attr of await this.getAttributes()) {
map[attr.name] = attr.value;
}
return map;
}
async hasAttribute(name) {
const map = await this.getAttributeMap();
return map.hasOwnProperty(name);
}
// WARNING: this doesn't take into account the possibility to have multi-valued attributes!
async getAttribute(name) {
return this.repository.getEntity("SELECT * FROM attributes WHERE noteId = ? AND name = ?", [this.noteId, name]);
}
@@ -39,6 +82,49 @@ class Note extends Entity {
return this.repository.getEntities("SELECT * FROM note_tree WHERE isDeleted = 0 AND noteId = ?", [this.noteId]);
}
async getChild(name) {
return this.repository.getEntity(`
SELECT notes.*
FROM note_tree
JOIN notes USING(noteId)
WHERE notes.isDeleted = 0
AND note_tree.isDeleted = 0
AND note_tree.parentNoteId = ?
AND notes.title = ?`, [this.noteId, name]);
}
async getChildren() {
return this.repository.getEntities(`
SELECT notes.*
FROM note_tree
JOIN notes USING(noteId)
WHERE notes.isDeleted = 0
AND note_tree.isDeleted = 0
AND note_tree.parentNoteId = ?
ORDER BY note_tree.notePosition`, [this.noteId]);
}
async getParents() {
return this.repository.getEntities(`
SELECT parent_notes.*
FROM
note_tree AS child_tree
JOIN notes AS parent_notes ON parent_notes.noteId = child_tree.parentNoteId
WHERE child_tree.noteId = ?
AND child_tree.isDeleted = 0
AND parent_notes.isDeleted = 0`, [this.noteId]);
}
async getNoteTree() {
return this.repository.getEntities(`
SELECT note_tree.*
FROM note_tree
JOIN notes USING(noteId)
WHERE notes.isDeleted = 0
AND note_tree.isDeleted = 0
AND note_tree.noteId = ?`, [this.noteId]);
}
beforeSaving() {
this.content = JSON.stringify(this.jsonContent, null, '\t');

View File

@@ -1,144 +0,0 @@
"use strict";
const sql = require('../services/sql');
const notes = require('../services/notes');
const axios = require('axios');
const log = require('../services/log');
const utils = require('../services/utils');
const unescape = require('unescape');
const attributes = require('../services/attributes');
const sync_mutex = require('../services/sync_mutex');
const config = require('../services/config');
const date_notes = require('../services/date_notes');
// "reddit" date note is subnote of date note which contains all reddit comments from that date
const REDDIT_DATE_ATTRIBUTE = 'reddit_date_note';
async function createNote(parentNoteId, noteTitle, noteText) {
return (await notes.createNewNote(parentNoteId, {
title: noteTitle,
content: noteText,
target: 'into',
isProtected: false
})).noteId;
}
function redditId(kind, id) {
return kind + "_" + id;
}
async function getDateNoteIdForReddit(dateTimeStr, rootNoteId) {
const dateStr = dateTimeStr.substr(0, 10);
let redditDateNoteId = await attributes.getNoteIdWithAttribute(REDDIT_DATE_ATTRIBUTE, dateStr);
if (!redditDateNoteId) {
const dateNoteId = await date_notes.getDateNoteId(dateTimeStr, rootNoteId);
redditDateNoteId = await createNote(dateNoteId, "Reddit");
await attributes.createAttribute(redditDateNoteId, REDDIT_DATE_ATTRIBUTE, dateStr);
await attributes.createAttribute(redditDateNoteId, "hide_in_autocomplete");
}
return redditDateNoteId;
}
async function importComments(rootNoteId, accountName, afterId = null) {
let url = `https://www.reddit.com/user/${accountName}.json`;
if (afterId) {
url += "?after=" + afterId;
}
const response = await axios.get(url);
const listing = response.data;
if (listing.kind !== 'Listing') {
log.info(`Reddit: Unknown object kind ${listing.kind}`);
return;
}
const children = listing.data.children;
let importedComments = 0;
for (const child of children) {
const comment = child.data;
let commentNoteId = await attributes.getNoteIdWithAttribute('reddit_id', redditId(child.kind, comment.id));
if (commentNoteId) {
continue;
}
const dateTimeStr = utils.dateStr(new Date(comment.created_utc * 1000));
const permaLink = 'https://reddit.com' + comment.permalink;
const noteText =
`<p><a href="${permaLink}">${permaLink}</a></p>
<p>author: <a href="https://reddit.com/u/${comment.author}">${comment.author}</a>,
subreddit: <a href="https://reddit.com/r/${comment.subreddit}">${comment.subreddit}</a>,
karma: ${comment.score}, created at ${dateTimeStr}</p><p></p>`
+ unescape(comment.body_html);
let parentNoteId = await getDateNoteIdForReddit(dateTimeStr, rootNoteId);
await sql.doInTransaction(async () => {
commentNoteId = await createNote(parentNoteId, comment.link_title, noteText);
log.info("Reddit: Imported comment to note " + commentNoteId);
importedComments++;
await attributes.createAttribute(commentNoteId, "reddit_kind", child.kind);
await attributes.createAttribute(commentNoteId, "reddit_id", redditId(child.kind, comment.id));
await attributes.createAttribute(commentNoteId, "reddit_created_utc", comment.created_utc);
});
}
// if there have been no imported comments on this page, there shouldn't be any to import
// on the next page since those are older
if (listing.data.after && importedComments > 0) {
importedComments += await importComments(rootNoteId, accountName, listing.data.after);
}
return importedComments;
}
let redditAccounts = [];
async function runImport() {
const rootNoteId = await date_notes.getRootNoteId();
// technically mutex shouldn't be necessary but we want to avoid doing potentially expensive import
// concurrently with sync
await sync_mutex.doExclusively(async () => {
let importedComments = 0;
for (const account of redditAccounts) {
importedComments += await importComments(rootNoteId, account);
}
log.info(`Reddit: Imported ${importedComments} comments.`);
});
}
sql.dbReady.then(async () => {
if (!config['Reddit'] || config['Reddit']['enabled'] !== true) {
return;
}
const redditAccountsStr = config['Reddit']['accounts'];
if (!redditAccountsStr) {
log.info("Reddit: No reddit accounts defined in option 'reddit_accounts'");
}
redditAccounts = redditAccountsStr.split(",").map(s => s.trim());
const pollingIntervalInSeconds = config['Reddit']['pollingIntervalInSeconds'] || (4 * 3600);
setInterval(runImport, pollingIntervalInSeconds * 1000);
setTimeout(runImport, 10000); // 10 seconds after startup - intentionally after initial sync
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

View File

@@ -1,5 +1,13 @@
const api = (function() {
const pluginButtonsEl = $("#plugin-buttons");
function ScriptContext(startNote, allNotes) {
return {
modules: {},
notes: toObject(allNotes, note => [note.noteId, note]),
apis: toObject(allNotes, note => [note.noteId, ScriptApi(startNote, note)]),
};
}
function ScriptApi(startNote, currentNote) {
const $pluginButtons = $("#plugin-buttons");
async function activateNote(notePath) {
await noteTree.activateNode(notePath);
@@ -10,12 +18,45 @@ const api = (function() {
button.attr('id', buttonId);
pluginButtonsEl.append(button);
$pluginButtons.append(button);
}
function prepareParams(params) {
if (!params) {
return params;
}
return params.map(p => {
if (typeof p === "function") {
return "!@#Function: " + p.toString();
}
else {
return p;
}
});
}
async function runOnServer(script, params = []) {
if (typeof script === "function") {
script = script.toString();
}
const ret = await server.post('script/exec', {
script: script,
params: prepareParams(params),
startNoteId: startNote.noteId,
currentNoteId: currentNote.noteId
});
return ret.executionResult;
}
return {
startNote: startNote,
currentNote: currentNote,
addButtonToToolbar,
activateNote
activateNote,
getInstanceName: noteTree.getInstanceName,
runOnServer
}
})();
}

View File

@@ -1,7 +1,7 @@
"use strict";
const contextMenu = (function() {
const treeEl = $("#tree");
const $tree = $("#tree");
let clipboardIds = [];
let clipboardMode = null;
@@ -85,16 +85,19 @@ const contextMenu = (function() {
{title: "Paste into <kbd>Ctrl+V</kbd>", cmd: "pasteInto", uiIcon: "ui-icon-clipboard"},
{title: "Paste after", cmd: "pasteAfter", uiIcon: "ui-icon-clipboard"},
{title: "----"},
{title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapse-sub-tree", uiIcon: "ui-icon-minus"},
{title: "Force note sync", cmd: "force-note-sync", uiIcon: "ui-icon-refresh"},
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sort-alphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
{title: "Export sub-tree", cmd: "exportSubTree", uiIcon: " ui-icon-arrowthick-1-ne"},
{title: "Import sub-tree into", cmd: "importSubTree", uiIcon: "ui-icon-arrowthick-1-sw"},
{title: "----"},
{title: "Collapse sub-tree <kbd>Alt+-</kbd>", cmd: "collapseSubTree", uiIcon: "ui-icon-minus"},
{title: "Force note sync", cmd: "forceNoteSync", uiIcon: "ui-icon-refresh"},
{title: "Sort alphabetically <kbd>Alt+S</kbd>", cmd: "sortAlphabetically", uiIcon: " ui-icon-arrowthick-2-n-s"}
],
beforeOpen: (event, ui) => {
const node = $.ui.fancytree.getNode(ui.target);
// Modify menu entries depending on node status
treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0);
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0);
// Activate node on right-click
node.setActive();
@@ -139,13 +142,19 @@ const contextMenu = (function() {
else if (ui.cmd === "delete") {
treeChanges.deleteNodes(noteTree.getSelectedNodes(true));
}
else if (ui.cmd === "collapse-sub-tree") {
else if (ui.cmd === "exportSubTree") {
exportSubTree(node.data.noteId);
}
else if (ui.cmd === "importSubTree") {
importSubTree(node.data.noteId);
}
else if (ui.cmd === "collapseSubTree") {
noteTree.collapseTree(node);
}
else if (ui.cmd === "force-note-sync") {
else if (ui.cmd === "forceNoteSync") {
forceNoteSync(node.data.noteId);
}
else if (ui.cmd === "sort-alphabetically") {
else if (ui.cmd === "sortAlphabetically") {
noteTree.sortAlphabetically(node.data.noteId);
}
else {

View File

@@ -17,27 +17,42 @@ const sqlConsole = (function() {
width: $(window).width(),
height: $(window).height(),
open: function() {
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($query[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
});
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
codeEditor.focus();
initEditor();
}
});
}
async function execute() {
async function initEditor() {
if (!codeEditor) {
await requireLibrary(CODE_MIRROR);
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
// removing Escape binding so that Escape will propagate to the dialog (which will close on escape)
delete CodeMirror.keyMap.basic["Esc"];
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($query[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false}
});
codeEditor.setOption("mode", "text/x-sqlite");
CodeMirror.autoLoadMode(codeEditor, "sql");
}
codeEditor.focus();
}
async function execute(e) {
// stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes)
e.preventDefault();
e.stopPropagation();
const sqlQuery = codeEditor.getValue();
const result = await server.post("sql/execute", {

View File

@@ -0,0 +1,32 @@
"use strict";
function exportSubTree(noteId) {
const url = getHost() + "/api/export/" + noteId + "?protectedSessionId="
+ encodeURIComponent(protected_session.getProtectedSessionId());
download(url);
}
let importNoteId;
function importSubTree(noteId) {
importNoteId = noteId;
$("#import-upload").trigger('click');
}
$("#import-upload").change(async function() {
const formData = new FormData();
formData.append('upload', this.files[0]);
await $.ajax({
url: baseApiUrl + 'import/' + importNoteId,
headers: server.getHeaders(),
data: formData,
type: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS
});
await noteTree.reload();
});

View File

@@ -114,22 +114,32 @@ $.ui.autocomplete.filter = (array, terms) => {
const tokens = terms.toLowerCase().split(" ");
for (const item of array) {
let found = true;
const lcLabel = item.label.toLowerCase();
for (const token of tokens) {
if (lcLabel.indexOf(token) === -1) {
found = false;
break;
const found = tokens.every(token => lcLabel.indexOf(token) !== -1);
if (!found) {
continue;
}
// this is not completely correct and might cause minor problems with note with names containing this " / "
const lastSegmentIndex = lcLabel.lastIndexOf(" / ");
if (lastSegmentIndex !== -1) {
const lastSegment = lcLabel.substr(lastSegmentIndex + 3);
// at least some token needs to be in the last segment (leaf note), otherwise this
// particular note is not that interesting (query is satisfied by parent note)
const foundInLastSegment = tokens.some(token => lastSegment.indexOf(token) !== -1);
if (!foundInLastSegment) {
continue;
}
}
if (found) {
results.push(item);
results.push(item);
if (results.length > 100) {
break;
}
if (results.length > 100) {
break;
}
}
@@ -191,9 +201,9 @@ window.onerror = function (msg, url, lineNo, columnNo, error) {
$("#logout-button").toggle(!isElectron());
$(document).ready(() => {
server.get("script/startup").then(scripts => {
for (const script of scripts) {
executeScript(script);
server.get("script/startup").then(scriptBundles => {
for (const bundle of scriptBundles) {
executeBundle(bundle);
}
});
});
@@ -213,4 +223,26 @@ if (isElectron()) {
noteTree.createNote(node, node.data.noteId, 'into', node.data.isProtected);
}, 500);
});
}
}
function uploadAttachment() {
$("#attachment-upload").trigger('click');
}
$("#attachment-upload").change(async function() {
const formData = new FormData();
formData.append('upload', this.files[0]);
const resp = await $.ajax({
url: baseApiUrl + 'attachments/upload/' + noteEditor.getCurrentNoteId(),
headers: server.getHeaders(),
data: formData,
type: 'POST',
contentType: false, // NEEDED, DON'T OMIT THIS
processData: false, // NEEDED, DON'T OMIT THIS
});
await noteTree.reload();
await noteTree.activateNode(resp.noteId);
});

View File

@@ -41,11 +41,11 @@ const link = (function() {
function goToLink(e) {
e.preventDefault();
const linkEl = $(e.target);
let notePath = linkEl.attr("note-path");
const $link = $(e.target);
let notePath = $link.attr("note-path");
if (!notePath) {
const address = linkEl.attr("note-path") ? linkEl.attr("note-path") : linkEl.attr('href');
const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href');
if (!address) {
return;

View File

@@ -1,7 +1,7 @@
"use strict";
const messaging = (function() {
const changesToPushCountEl = $("#changes-to-push-count");
const $changesToPushCount = $("#changes-to-push-count");
function logError(message) {
console.log(now(), message); // needs to be separate from .trace()
@@ -52,7 +52,7 @@ const messaging = (function() {
// we don't detect image changes here since images themselves are immutable and references should be
// updated in note detail as well
changesToPushCountEl.html(message.changesToPushCount);
$changesToPushCount.html(message.changesToPushCount);
}
else if (message.type === 'sync-hash-check-failed') {
showError("Sync check failed!", 60000);
@@ -84,7 +84,7 @@ const messaging = (function() {
let connectionBrokenNotification = null;
setInterval(async () => {
if (new Date().getTime() - lastPingTs > 5000) {
if (new Date().getTime() - lastPingTs > 30000) {
if (!connectionBrokenNotification) {
connectionBrokenNotification = $.notify({
// options

View File

@@ -1,16 +1,24 @@
"use strict";
const noteEditor = (function() {
const noteTitleEl = $("#note-title");
const noteDetailEl = $('#note-detail');
const noteDetailCodeEl = $('#note-detail-code');
const noteDetailRenderEl = $('#note-detail-render');
const protectButton = $("#protect-button");
const unprotectButton = $("#unprotect-button");
const noteDetailWrapperEl = $("#note-detail-wrapper");
const noteIdDisplayEl = $("#note-id-display");
const attributeListEl = $("#attribute-list");
const attributeListInnerEl = $("#attribute-list-inner");
const $noteTitle = $("#note-title");
const $noteDetail = $('#note-detail');
const $noteDetailCode = $('#note-detail-code');
const $noteDetailRender = $('#note-detail-render');
const $noteDetailAttachment = $('#note-detail-attachment');
const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button");
const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteIdDisplay = $("#note-id-display");
const $attributeList = $("#attribute-list");
const $attributeListInner = $("#attribute-list-inner");
const $attachmentFileName = $("#attachment-filename");
const $attachmentFileType = $("#attachment-filetype");
const $attachmentFileSize = $("#attachment-filesize");
const $attachmentDownload = $("#attachment-download");
const $attachmentOpen = $("#attachment-open");
let editor = null;
let codeEditor = null;
@@ -69,25 +77,27 @@ const noteEditor = (function() {
function updateNoteFromInputs(note) {
if (note.detail.type === 'text') {
note.detail.content = editor.getData();
let content = editor.getData();
// if content is only tags/whitespace (typically <p>&nbsp;</p>), then just make it empty
// this is important when setting new note to code
if (jQuery(note.detail.content).text().trim() === '') {
note.detail.content = ''
if (jQuery(content).text().trim() === '' && !content.includes("<img")) {
content = '';
}
note.detail.content = content;
}
else if (note.detail.type === 'code') {
note.detail.content = codeEditor.getValue();
}
else if (note.detail.type === 'render') {
else if (note.detail.type === 'render' || note.detail.type === 'file') {
// nothing
}
else {
throwError("Unrecognized type: " + note.detail.type);
}
const title = noteTitleEl.val();
const title = $noteTitle.val();
note.detail.title = title;
@@ -105,9 +115,9 @@ const noteEditor = (function() {
function setNoteBackgroundIfProtected(note) {
const isProtected = !!note.detail.isProtected;
noteDetailWrapperEl.toggleClass("protected", isProtected);
protectButton.toggle(!isProtected);
unprotectButton.toggle(isProtected);
$noteDetailWrapper.toggleClass("protected", isProtected);
$protectButton.toggle(!isProtected);
$unprotectButton.toggle(isProtected);
}
let isNewNoteCreated = false;
@@ -116,19 +126,46 @@ const noteEditor = (function() {
isNewNoteCreated = true;
}
function setContent(content) {
async function setContent(content) {
if (currentNote.detail.type === 'text') {
if (!editor) {
await requireLibrary(CKEDITOR);
editor = await BalloonEditor.create($noteDetail[0], {});
editor.document.on('change', noteChanged);
}
// temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49
editor.setData(content ? content : "<p></p>");
noteDetailEl.show();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').hide();
$noteDetail.show();
}
else if (currentNote.detail.type === 'code') {
noteDetailEl.hide();
noteDetailCodeEl.show();
noteDetailRenderEl.html('').hide();
if (!codeEditor) {
await requireLibrary(CODE_MIRROR);
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($("#note-detail-code")[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
matchTags: { bothTags: true },
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false },
lint: true,
gutters: ["CodeMirror-lint-markers"],
lineNumbers: true
});
codeEditor.on('change', noteChanged);
}
$noteDetailCode.show();
// this needs to happen after the element is shown, otherwise the editor won't be refresheds
codeEditor.setValue(content);
@@ -139,6 +176,8 @@ const noteEditor = (function() {
codeEditor.setOption("mode", info.mime);
CodeMirror.autoLoadMode(codeEditor, info.mode);
}
codeEditor.refresh();
}
}
@@ -148,10 +187,10 @@ const noteEditor = (function() {
if (isNewNoteCreated) {
isNewNoteCreated = false;
noteTitleEl.focus().select();
$noteTitle.focus().select();
}
noteIdDisplayEl.html(noteId);
$noteIdDisplay.html(noteId);
await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false);
@@ -163,26 +202,38 @@ const noteEditor = (function() {
// to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it.
protected_session.ensureDialogIsClosed();
noteDetailWrapperEl.show();
$noteDetailWrapper.show();
noteChangeDisabled = true;
noteTitleEl.val(currentNote.detail.title);
$noteTitle.val(currentNote.detail.title);
noteType.setNoteType(currentNote.detail.type);
noteType.setNoteMime(currentNote.detail.mime);
$noteDetail.hide();
$noteDetailCode.hide();
$noteDetailRender.html('').hide();
$noteDetailAttachment.hide();
if (currentNote.detail.type === 'render') {
noteDetailEl.hide();
noteDetailCodeEl.hide();
noteDetailRenderEl.html('').show();
$noteDetailRender.show();
const subTree = await server.get('script/subtree/' + getCurrentNoteId());
const bundle = await server.get('script/bundle/' + getCurrentNoteId());
noteDetailRenderEl.html(subTree);
$noteDetailRender.html(bundle.html);
executeBundle(bundle);
}
else if (currentNote.detail.type === 'file') {
$noteDetailAttachment.show();
$attachmentFileName.text(currentNote.attributes.original_file_name);
$attachmentFileSize.text(currentNote.attributes.file_size + " bytes");
$attachmentFileType.text(currentNote.detail.mime);
}
else {
setContent(currentNote.detail.content);
await setContent(currentNote.detail.content);
}
noteChangeDisabled = false;
@@ -191,7 +242,7 @@ const noteEditor = (function() {
noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top
noteDetailWrapperEl.scrollTop(0);
$noteDetailWrapper.scrollTop(0);
loadAttributeList();
}
@@ -201,17 +252,17 @@ const noteEditor = (function() {
const attributes = await server.get('notes/' + noteId + '/attributes');
attributeListInnerEl.html('');
$attributeListInner.html('');
if (attributes.length > 0) {
for (const attr of attributes) {
attributeListInnerEl.append(formatAttribute(attr) + " ");
$attributeListInner.append(formatAttribute(attr) + " ");
}
attributeListEl.show();
$attributeList.show();
}
else {
attributeListEl.hide();
$attributeList.hide();
}
}
@@ -227,12 +278,12 @@ const noteEditor = (function() {
const note = getCurrentNote();
if (note.detail.type === 'text') {
noteDetailEl.focus();
$noteDetail.focus();
}
else if (note.detail.type === 'code') {
codeEditor.focus();
}
else if (note.detail.type === 'render') {
else if (note.detail.type === 'render' || note.detail.type === 'file') {
// do nothing
}
else {
@@ -251,51 +302,50 @@ const noteEditor = (function() {
// make sure note is saved so we load latest changes
await saveNoteIfChanged();
const script = await server.get('script/subtree/' + getCurrentNoteId());
if (currentNote.detail.mime.endsWith("env=frontend")) {
const bundle = await server.get('script/bundle/' + getCurrentNoteId());
executeScript(script);
executeBundle(bundle);
}
if (currentNote.detail.mime.endsWith("env=backend")) {
await server.post('script/run/' + getCurrentNoteId());
}
showMessage("Note executed");
}
}
$attachmentDownload.click(() => download(getAttachmentUrl()));
$attachmentOpen.click(() => {
if (isElectron()) {
const open = require("open");
open(getAttachmentUrl());
}
else {
window.location.href = getAttachmentUrl();
}
});
function getAttachmentUrl() {
// electron needs absolute URL so we extract current host, port, protocol
return getHost() + "/api/attachments/download/" + getCurrentNoteId()
+ "?protectedSessionId=" + encodeURIComponent(protected_session.getProtectedSessionId());
}
$(document).ready(() => {
noteTitleEl.on('input', () => {
$noteTitle.on('input', () => {
noteChanged();
const title = noteTitleEl.val();
const title = $noteTitle.val();
noteTree.setNoteTitle(getCurrentNoteId(), title);
});
BalloonEditor
.create(document.querySelector('#note-detail'), {
})
.then(edit => {
editor = edit;
editor.document.on('change', noteChanged);
})
.catch(error => {
console.error(error);
});
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
CodeMirror.keyMap.default["Tab"] = "indentMore";
CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js';
codeEditor = CodeMirror($("#note-detail-code")[0], {
value: "",
viewportMargin: Infinity,
indentUnit: 4,
matchBrackets: true,
matchTags: { bothTags: true },
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false }
});
codeEditor.on('change', noteChanged);
// so that tab jumps from note title (which has tabindex 1)
noteDetailEl.attr("tabindex", 2);
$noteDetail.attr("tabindex", 2);
});
$(document).bind('keydown', "ctrl+return", executeCurrentNote);

View File

@@ -1,9 +1,11 @@
"use strict";
const noteTree = (function() {
const treeEl = $("#tree");
const parentListEl = $("#parent-list");
const parentListListEl = $("#parent-list-inner");
const $tree = $("#tree");
const $parentList = $("#parent-list");
const $parentListList = $("#parent-list-inner");
let instanceName = null; // should have better place
let startNotePath = null;
let notesTreeMap = {};
@@ -52,7 +54,7 @@ const noteTree = (function() {
// note that if you want to access data like noteId or isProtected, you need to go into "data" property
function getCurrentNode() {
return treeEl.fancytree("getActiveNode");
return $tree.fancytree("getActiveNode");
}
function getCurrentNotePath() {
@@ -155,6 +157,12 @@ const noteTree = (function() {
if (note.type === 'code') {
extraClasses.push("code");
}
else if (note.type === 'render') {
extraClasses.push('render');
}
else if (note.type === 'file') {
extraClasses.push('attachment');
}
return extraClasses.join(" ");
}
@@ -314,11 +322,11 @@ const noteTree = (function() {
}
if (parents.length <= 1) {
parentListEl.hide();
$parentList.hide();
}
else {
parentListEl.show();
parentListListEl.empty();
$parentList.show();
$parentListList.empty();
for (const parentNoteId of parents) {
const parentNotePath = getSomeNotePath(parentNoteId);
@@ -335,7 +343,7 @@ const noteTree = (function() {
item = link.createNoteLink(notePath, title);
}
parentListListEl.append($("<li/>").append(item));
$parentListList.append($("<li/>").append(item));
}
}
}
@@ -543,7 +551,7 @@ const noteTree = (function() {
}
};
treeEl.fancytree({
$tree.fancytree({
autoScroll: true,
keyboard: false, // we takover keyboard handling in the hotkeys plugin
extensions: ["hotkeys", "filter", "dnd", "clones"],
@@ -624,11 +632,11 @@ const noteTree = (function() {
}
});
treeEl.contextmenu(contextMenu.contextMenuSettings);
$tree.contextmenu(contextMenu.contextMenuSettings);
}
function getTree() {
return treeEl.fancytree('getTree');
return $tree.fancytree('getTree');
}
async function reload() {
@@ -645,6 +653,7 @@ const noteTree = (function() {
async function loadTree() {
const resp = await server.get('tree');
startNotePath = resp.start_note_path;
instanceName = resp.instanceName;
if (document.location.hash) {
startNotePath = getNotePathFromAddress();
@@ -663,7 +672,7 @@ const noteTree = (function() {
function collapseTree(node = null) {
if (!node) {
node = treeEl.fancytree("getRootNode");
node = $tree.fancytree("getRootNode");
}
node.setExpanded(false);
@@ -710,6 +719,9 @@ const noteTree = (function() {
titlePath = '';
}
// https://github.com/zadam/trilium/issues/46
// unfortunately not easy to implement because we don't have an easy access to note's isProtected property
const autocompleteItems = [];
for (const childNoteId of parentToChildren[parentNoteId]) {
@@ -744,7 +756,7 @@ const noteTree = (function() {
}
async function createNewTopLevelNote() {
const rootNode = treeEl.fancytree("getRootNode");
const rootNode = $tree.fancytree("getRootNode");
await createNote(rootNode, "root", "into");
}
@@ -820,6 +832,10 @@ const noteTree = (function() {
return !!childToParents[noteId];
}
function getInstanceName() {
return instanceName;
}
$(document).bind('keydown', 'ctrl+o', e => {
const node = getCurrentNode();
const parentNoteId = node.data.parentNoteId;
@@ -894,6 +910,7 @@ const noteTree = (function() {
setParentChildRelation,
getSelectedNodes,
sortAlphabetically,
noteExists
noteExists,
getInstanceName
};
})();

View File

@@ -1,7 +1,7 @@
"use strict";
const noteType = (function() {
const executeScriptButton = $("#execute-script-button");
const $executeScriptButton = $("#execute-script-button");
const noteTypeModel = new NoteTypeModel();
function NoteTypeModel() {
@@ -25,7 +25,8 @@ const noteType = (function() {
{ mime: 'text/html', title: 'HTML' },
{ mime: 'message/http', title: 'HTTP' },
{ mime: 'text/x-java', title: 'Java' },
{ mime: 'application/javascript', title: 'JavaScript' },
{ mime: 'application/javascript;env=frontend', title: 'JavaScript frontend' },
{ mime: 'application/javascript;env=backend', title: 'JavaScript backend' },
{ mime: 'application/json', title: 'JSON' },
{ mime: 'text/x-kotlin', title: 'Kotlin' },
{ mime: 'text/x-lua', title: 'Lua' },
@@ -65,11 +66,18 @@ const noteType = (function() {
else if (type === 'render') {
return 'Render HTML note';
}
else if (type === 'file') {
return 'Attachment';
}
else {
throwError('Unrecognized type: ' + type);
}
};
this.isDisabled = function() {
return self.type() === "file";
};
async function save() {
const note = noteEditor.getCurrentNote();
@@ -114,7 +122,7 @@ const noteType = (function() {
};
this.updateExecuteScriptButtonVisibility = function() {
executeScriptButton.toggle(self.mime() === 'application/javascript');
$executeScriptButton.toggle(self.mime().startsWith('application/javascript'));
}
}

View File

@@ -1,10 +1,10 @@
"use strict";
const protected_session = (function() {
const dialogEl = $("#protected-session-password-dialog");
const passwordFormEl = $("#protected-session-password-form");
const passwordEl = $("#protected-session-password");
const noteDetailWrapperEl = $("#note-detail-wrapper");
const $dialog = $("#protected-session-password-dialog");
const $passwordForm = $("#protected-session-password-form");
const $password = $("#protected-session-password");
const $noteDetailWrapper = $("#note-detail-wrapper");
let protectedSessionDeferred = null;
let lastProtectedSessionOperationDate = null;
@@ -25,9 +25,11 @@ const protected_session = (function() {
if (requireProtectedSession && !isProtectedSessionAvailable()) {
protectedSessionDeferred = dfd;
noteDetailWrapperEl.hide();
if (noteTree.getCurrentNode().data.isProtected) {
$noteDetailWrapper.hide();
}
dialogEl.dialog({
$dialog.dialog({
modal: modal,
width: 400,
open: () => {
@@ -46,8 +48,8 @@ const protected_session = (function() {
}
async function setupProtectedSession() {
const password = passwordEl.val();
passwordEl.val("");
const password = $password.val();
$password.val("");
const response = await enterProtectedSession(password);
@@ -58,15 +60,15 @@ const protected_session = (function() {
protectedSessionId = response.protectedSessionId;
dialogEl.dialog("close");
$dialog.dialog("close");
noteEditor.reload();
noteTree.reload();
if (protectedSessionDeferred !== null) {
ensureDialogIsClosed(dialogEl, passwordEl);
ensureDialogIsClosed($dialog, $password);
noteDetailWrapperEl.show();
$noteDetailWrapper.show();
protectedSessionDeferred.resolve();
@@ -77,11 +79,11 @@ const protected_session = (function() {
function ensureDialogIsClosed() {
// this may fal if the dialog has not been previously opened
try {
dialogEl.dialog('close');
$dialog.dialog('close');
}
catch (e) {}
passwordEl.val('');
$password.val('');
}
async function enterProtectedSession(password) {
@@ -155,7 +157,7 @@ const protected_session = (function() {
noteEditor.reload();
}
passwordFormEl.submit(() => {
$passwordForm.submit(() => {
setupProtectedSession();
return false;

View File

@@ -1,40 +1,40 @@
"use strict";
const searchTree = (function() {
const treeEl = $("#tree");
const searchInputEl = $("input[name='search-text']");
const resetSearchButton = $("button#reset-search-button");
const searchBoxEl = $("#search-box");
const $tree = $("#tree");
const $searchInput = $("input[name='search-text']");
const $resetSearchButton = $("button#reset-search-button");
const $searchBox = $("#search-box");
resetSearchButton.click(resetSearch);
$resetSearchButton.click(resetSearch);
function toggleSearch() {
if (searchBoxEl.is(":hidden")) {
searchBoxEl.show();
searchInputEl.focus();
if ($searchBox.is(":hidden")) {
$searchBox.show();
$searchInput.focus();
}
else {
resetSearch();
searchBoxEl.hide();
$searchBox.hide();
}
}
function resetSearch() {
searchInputEl.val("");
$searchInput.val("");
getTree().clearFilter();
}
function getTree() {
return treeEl.fancytree('getTree');
return $tree.fancytree('getTree');
}
searchInputEl.keyup(async e => {
const searchText = searchInputEl.val();
$searchInput.keyup(async e => {
const searchText = $searchInput.val();
if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") {
resetSearchButton.click();
$resetSearchButton.click();
return;
}

View File

@@ -31,16 +31,6 @@ const server = (function() {
return await call('DELETE', url);
}
async function exec(params, script) {
if (typeof script === "function") {
script = script.toString();
}
const ret = await post('script/exec', { script: script, params: params });
return ret.executionResult;
}
let i = 1;
const reqResolves = {};
@@ -104,7 +94,7 @@ const server = (function() {
post,
put,
remove,
exec,
ajax,
// don't remove, used from CKEditor image upload!
getHeaders
}

View File

@@ -1,14 +1,14 @@
"use strict";
const treeUtils = (function() {
const treeEl = $("#tree");
const $tree = $("#tree");
function getParentProtectedStatus(node) {
return isTopLevelNode(node) ? 0 : node.getParent().data.isProtected;
}
function getNodeByKey(key) {
return treeEl.fancytree('getNodeByKey', key);
return $tree.fancytree('getNodeByKey', key);
}
function getNoteIdFromNotePath(notePath) {

View File

@@ -115,9 +115,10 @@ async function stopWatch(what, func) {
return ret;
}
function executeScript(script) {
// last \r\n is necessary if script contains line comment on its last line
eval("(async function() {" + script + "\r\n})()");
async function executeBundle(bundle) {
const apiContext = ScriptContext(bundle.note, bundle.allNotes);
return await (function() { return eval(`const apiContext = this; (async function() { ${bundle.script}\r\n})()`); }.call(apiContext));
}
function formatValueWithWhitespace(val) {
@@ -132,4 +133,90 @@ function formatAttribute(attr) {
}
return str;
}
const CKEDITOR = { "js": ["libraries/ckeditor/ckeditor.js"] };
const CODE_MIRROR = {
js: [
"libraries/codemirror/codemirror.js",
"libraries/codemirror/addon/mode/loadmode.js",
"libraries/codemirror/addon/fold/xml-fold.js",
"libraries/codemirror/addon/edit/matchbrackets.js",
"libraries/codemirror/addon/edit/matchtags.js",
"libraries/codemirror/addon/search/match-highlighter.js",
"libraries/codemirror/mode/meta.js",
"libraries/codemirror/addon/lint/lint.js",
"libraries/codemirror/addon/lint/eslint.js"
],
css: [
"libraries/codemirror/codemirror.css",
"libraries/codemirror/addon/lint/lint.css"
]
};
const ESLINT = { js: [ "libraries/eslint.js" ] };
async function requireLibrary(library) {
if (library.css) {
library.css.map(cssUrl => requireCss(cssUrl));
}
if (library.js) {
for (const scriptUrl of library.js) {
await requireScript(scriptUrl);
}
}
}
const dynamicallyLoadedScripts = [];
async function requireScript(url) {
if (!dynamicallyLoadedScripts.includes(url)) {
dynamicallyLoadedScripts.push(url);
return await $.ajax({
url: url,
dataType: "script",
cache: true
})
}
}
async function requireCss(url) {
const css = Array
.from(document.querySelectorAll('link'))
.map(scr => scr.href);
if (!css.includes(url)) {
$('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url));
}
}
function getHost() {
const url = new URL(window.location.href);
return url.protocol + "//" + url.hostname + ":" + url.port;
}
function download(url) {
if (isElectron()) {
const remote = require('electron').remote;
remote.getCurrentWebContents().downloadURL(url);
}
else {
window.location.href = url;
}
}
function toObject(array, fn) {
const obj = {};
for (const item of array) {
const ret = fn(item);
obj[ret[0]] = ret[1];
}
return obj;
}

View File

@@ -102,18 +102,23 @@
}
}
var currentlyHighlighted = null;
function doMatchBrackets(cm) {
cm.operation(function() {
if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
if (cm.state.matchBrackets.currentlyHighlighted) {
cm.state.matchBrackets.currentlyHighlighted();
cm.state.matchBrackets.currentlyHighlighted = null;
}
cm.state.matchBrackets.currentlyHighlighted = matchBrackets(cm, false, cm.state.matchBrackets);
});
}
CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
cm.off("cursorActivity", doMatchBrackets);
if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;}
if (cm.state.matchBrackets && cm.state.matchBrackets.currentlyHighlighted) {
cm.state.matchBrackets.currentlyHighlighted();
cm.state.matchBrackets.currentlyHighlighted = null;
}
}
if (val) {
cm.state.matchBrackets = typeof val == "object" ? val : {};

View File

@@ -138,7 +138,7 @@
var iter = new Iter(cm, start.line, 0);
for (;;) {
var openTag = toNextTag(iter), end;
if (!openTag || iter.line != start.line || !(end = toTagEnd(iter))) return;
if (!openTag || !(end = toTagEnd(iter)) || iter.line != start.line) return;
if (!openTag[1] && end != "selfClose") {
var startPos = Pos(iter.line, iter.ch);
var endPos = findMatchingClose(iter, openTag[2]);

View File

@@ -0,0 +1,92 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/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";
async function validatorHtml(text, options) {
const result = /<script[^>]*>([\s\S]+)<\/script>/ig.exec(text);
if (result !== null) {
// preceding code is copied over but any (non-newline) character is replaced with space
// this will preserve line numbers etc.
const prefix = text.substr(0, result.index).replace(/./g, " ");
const js = prefix + result[1];
return await validatorJavaScript(js, options);
}
return [];
}
async function validatorJavaScript(text, options) {
if (noteEditor.getCurrentNote().detail.mime === 'application/json') {
// eslint doesn't seem to validate pure JSON well
return [];
}
await requireLibrary(ESLINT);
if (text.length > 20000) {
console.log("Skipping linting because of large size: ", text.length);
return [];
}
const errors = new eslint().verify(text, {
root: true,
parserOptions: {
ecmaVersion: 2017
},
extends: ['eslint:recommended', 'airbnb-base'],
env: {
'node': true
},
rules: {
'import/no-unresolved': 'off',
'func-names': 'off',
'comma-dangle': ['warn'],
'padded-blocks': 'off',
'linebreak-style': 'off',
'class-methods-use-this': 'off',
'no-unused-vars': ['warn', { vars: 'local', args: 'after-used' }],
'no-nested-ternary': 'off',
'no-underscore-dangle': ['error', {'allow': ['_super', '_lookupFactory']}]
}
});
const result = [];
if (errors) {
parseErrors(errors, result);
}
return result;
}
CodeMirror.registerHelper("lint", "javascript", validatorJavaScript);
CodeMirror.registerHelper("lint", "html", validatorHtml);
function parseErrors(errors, output) {
for (const error of errors) {
const startLine = error.line - 1;
const endLine = error.endLine !== undefined ? error.endLine - 1 : startLine;
const startCol = error.column - 1;
const endCol = error.endColumn !== undefined ? error.endColumn - 1 : startCol + 1;
output.push({
message: error.message,
severity: error.severity === 1 ? "warning" : "error",
from: CodeMirror.Pos(startLine, startCol),
to: CodeMirror.Pos(endLine, endCol)
});
}
}
});

View File

@@ -0,0 +1,73 @@
/* The lint marker gutter */
.CodeMirror-lint-markers {
width: 16px;
}
.CodeMirror-lint-tooltip {
background-color: #ffd;
border: 1px solid black;
border-radius: 4px 4px 4px 4px;
color: black;
font-family: monospace;
font-size: 10pt;
overflow: hidden;
padding: 2px 5px;
position: fixed;
white-space: pre;
white-space: pre-wrap;
z-index: 100;
max-width: 600px;
opacity: 0;
transition: opacity .4s;
-moz-transition: opacity .4s;
-webkit-transition: opacity .4s;
-o-transition: opacity .4s;
-ms-transition: opacity .4s;
}
.CodeMirror-lint-mark-error, .CodeMirror-lint-mark-warning {
background-position: left bottom;
background-repeat: repeat-x;
}
.CodeMirror-lint-mark-error {
background-image:
url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==")
;
}
.CodeMirror-lint-mark-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-error, .CodeMirror-lint-marker-warning {
background-position: center center;
background-repeat: no-repeat;
cursor: pointer;
display: inline-block;
height: 16px;
width: 16px;
vertical-align: middle;
position: relative;
}
.CodeMirror-lint-message-error, .CodeMirror-lint-message-warning {
padding-left: 18px;
background-position: top left;
background-repeat: no-repeat;
}
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
}
.CodeMirror-lint-marker-multiple {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
background-repeat: no-repeat;
background-position: right bottom;
width: 100%; height: 100%;
}

View File

@@ -0,0 +1,252 @@
// CodeMirror, copyright (c) by Marijn Haverbeke and others
// Distributed under an MIT license: http://codemirror.net/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";
var GUTTER_ID = "CodeMirror-lint-markers";
function showTooltip(e, content) {
var tt = document.createElement("div");
tt.className = "CodeMirror-lint-tooltip";
tt.appendChild(content.cloneNode(true));
document.body.appendChild(tt);
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";
}
CodeMirror.on(document, "mousemove", position);
position(e);
if (tt.style.opacity != null) tt.style.opacity = 1;
return tt;
}
function rm(elt) {
if (elt.parentNode) elt.parentNode.removeChild(elt);
}
function hideTooltip(tt) {
if (!tt.parentNode) return;
if (tt.style.opacity == null) rm(tt);
tt.style.opacity = 0;
setTimeout(function() { rm(tt); }, 600);
}
function showTooltipFor(e, content, node) {
var tooltip = showTooltip(e, content);
function hide() {
CodeMirror.off(node, "mouseout", hide);
if (tooltip) { hideTooltip(tooltip); tooltip = null; }
}
var poll = setInterval(function() {
if (tooltip) for (var n = node;; n = n.parentNode) {
if (n && n.nodeType == 11) n = n.host;
if (n == document.body) return;
if (!n) { hide(); break; }
}
if (!tooltip) return clearInterval(poll);
}, 400);
CodeMirror.on(node, "mouseout", hide);
}
function LintState(cm, options, hasGutter) {
this.marked = [];
this.options = options;
this.timeout = null;
this.hasGutter = hasGutter;
this.onMouseOver = function(e) { onMouseOver(cm, e); };
this.waitingFor = 0
}
function parseOptions(_cm, options) {
if (options instanceof Function) return {getAnnotations: options};
if (!options || options === true) options = {};
return options;
}
function clearMarks(cm) {
var state = cm.state.lint;
if (state.hasGutter) cm.clearGutter(GUTTER_ID);
for (var i = 0; i < state.marked.length; ++i)
state.marked[i].clear();
state.marked.length = 0;
}
function makeMarker(labels, severity, multiple, tooltips) {
var marker = document.createElement("div"), inner = marker;
marker.className = "CodeMirror-lint-marker-" + severity;
if (multiple) {
inner = marker.appendChild(document.createElement("div"));
inner.className = "CodeMirror-lint-marker-multiple";
}
if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) {
showTooltipFor(e, labels, inner);
});
return marker;
}
function getMaxSeverity(a, b) {
if (a == "error") return a;
else return b;
}
function groupByLine(annotations) {
var lines = [];
for (var i = 0; i < annotations.length; ++i) {
var ann = annotations[i], line = ann.from.line;
(lines[line] || (lines[line] = [])).push(ann);
}
return lines;
}
function annotationTooltip(ann) {
var severity = ann.severity;
if (!severity) severity = "error";
var tip = document.createElement("div");
tip.className = "CodeMirror-lint-message-" + severity;
if (typeof ann.messageHTML != 'undefined') {
tip.innerHTML = ann.messageHTML;
} else {
tip.appendChild(document.createTextNode(ann.message));
}
return tip;
}
function lintAsync(cm, getAnnotations, passOptions) {
var state = cm.state.lint
var id = ++state.waitingFor
function abort() {
id = -1
cm.off("change", abort)
}
cm.on("change", abort)
getAnnotations(cm.getValue(), function(annotations, arg2) {
cm.off("change", abort)
if (state.waitingFor != id) return
if (arg2 && annotations instanceof CodeMirror) annotations = arg2
cm.operation(function() {updateLinting(cm, annotations)})
}, passOptions, cm);
}
function startLinting(cm) {
var state = cm.state.lint, options = state.options;
/*
* Passing rules in `options` property prevents JSHint (and other linters) from complaining
* about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc.
*/
var passOptions = options.options || options;
var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint");
if (!getAnnotations) return;
if (options.async || getAnnotations.async) {
lintAsync(cm, getAnnotations, passOptions)
} else {
var annotations = getAnnotations(cm.getValue(), passOptions, cm);
if (!annotations) return;
if (annotations.then) annotations.then(function(issues) {
cm.operation(function() {updateLinting(cm, issues)})
});
else cm.operation(function() {updateLinting(cm, annotations)})
}
}
function updateLinting(cm, annotationsNotSorted) {
clearMarks(cm);
var state = cm.state.lint, options = state.options;
var annotations = groupByLine(annotationsNotSorted);
for (var line = 0; line < annotations.length; ++line) {
var anns = annotations[line];
if (!anns) continue;
var maxSeverity = null;
var tipLabel = state.hasGutter && document.createDocumentFragment();
for (var i = 0; i < anns.length; ++i) {
var ann = anns[i];
var severity = ann.severity;
if (!severity) severity = "error";
maxSeverity = getMaxSeverity(maxSeverity, severity);
if (options.formatAnnotation) ann = options.formatAnnotation(ann);
if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));
if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {
className: "CodeMirror-lint-mark-" + severity,
__annotation: ann
}));
}
if (state.hasGutter)
cm.setGutterMarker(line, GUTTER_ID, makeMarker(tipLabel, maxSeverity, anns.length > 1,
state.options.tooltips));
}
if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);
}
function onChange(cm) {
var state = cm.state.lint;
if (!state) return;
clearTimeout(state.timeout);
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay || 500);
}
function popupTooltips(annotations, e) {
var target = e.target || e.srcElement;
var tooltip = document.createDocumentFragment();
for (var i = 0; i < annotations.length; i++) {
var ann = annotations[i];
tooltip.appendChild(annotationTooltip(ann));
}
showTooltipFor(e, tooltip, target);
}
function onMouseOver(cm, e) {
var target = e.target || e.srcElement;
if (!/\bCodeMirror-lint-mark-/.test(target.className)) return;
var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2;
var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client"));
var annotations = [];
for (var i = 0; i < spans.length; ++i) {
var ann = spans[i].__annotation;
if (ann) annotations.push(ann);
}
if (annotations.length) popupTooltips(annotations, e);
}
CodeMirror.defineOption("lint", false, function(cm, val, old) {
if (old && old != CodeMirror.Init) {
clearMarks(cm);
if (cm.state.lint.options.lintOnChange !== false)
cm.off("change", onChange);
CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver);
clearTimeout(cm.state.lint.timeout);
delete cm.state.lint;
}
if (val) {
var gutters = cm.getOption("gutters"), hasLintGutter = false;
for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;
var state = cm.state.lint = new LintState(cm, parseOptions(cm, val), hasLintGutter);
if (state.options.lintOnChange !== false)
cm.on("change", onChange);
if (state.options.tooltips != false && state.options.tooltips != "gutter")
CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);
startLinting(cm);
}
});
CodeMirror.defineExtension("performLint", function() {
if (this.state.lint) startLinting(this);
});
});

View File

@@ -90,7 +90,7 @@
var state = cm.state.matchHighlighter;
cm.addOverlay(state.overlay = makeOverlay(query, hasBoundary, style));
if (state.options.annotateScrollbar && cm.showMatchesOnScrollbar) {
var searchFor = hasBoundary ? new RegExp("\\b" + query + "\\b") : query;
var searchFor = hasBoundary ? new RegExp("\\b" + query.replace(/[\\\[+*?(){|^$]/g, "\\$&") + "\\b") : query;
state.matchesonscroll = cm.showMatchesOnScrollbar(searchFor, false,
{className: "CodeMirror-selection-highlight-scrollbar"});
}

File diff suppressed because it is too large Load Diff

View File

@@ -846,6 +846,8 @@ CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
CodeMirror.defineMIME("text/javascript", "javascript");
CodeMirror.defineMIME("text/ecmascript", "javascript");
CodeMirror.defineMIME("application/javascript", "javascript");
CodeMirror.defineMIME("application/javascript;env=frontend", "javascript");
CodeMirror.defineMIME("application/javascript;env=backend", "javascript");
CodeMirror.defineMIME("application/x-javascript", "javascript");
CodeMirror.defineMIME("application/ecmascript", "javascript");
CodeMirror.defineMIME("application/json", {name: "javascript", json: true});

View File

@@ -70,7 +70,7 @@
{name: "Pug", mime: "text/x-pug", mode: "pug", ext: ["jade", "pug"], alias: ["jade"]},
{name: "Java", mime: "text/x-java", mode: "clike", ext: ["java"]},
{name: "Java Server Pages", mime: "application/x-jsp", mode: "htmlembedded", ext: ["jsp"], alias: ["jsp"]},
{name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/x-javascript", "application/ecmascript"],
{name: "JavaScript", mimes: ["text/javascript", "text/ecmascript", "application/javascript", "application/javascript;env=frontend", "application/javascript;env=backend", "application/x-javascript", "application/ecmascript"],
mode: "javascript", ext: ["js"], alias: ["ecmascript", "js", "node"]},
{name: "JSON", mimes: ["application/json", "application/x-json"], mode: "javascript", ext: ["json", "map"], alias: ["json5"]},
{name: "JSON-LD", mime: "application/ld+json", mode: "javascript", ext: ["jsonld"], alias: ["jsonld"]},

101349
src/public/libraries/eslint.js Normal file

File diff suppressed because one or more lines are too long

View File

@@ -72,6 +72,16 @@ span.fancytree-node.fancytree-folder.code > span.fancytree-icon {
background-image: url("../images/icons/code-folder.png");
}
span.fancytree-node.attachment > span.fancytree-icon {
background-position: 0 0;
background-image: url("../images/icons/paperclip.png");
}
span.fancytree-node.render > span.fancytree-icon {
background-position: 0 0;
background-image: url("../images/icons/play.png");
}
span.fancytree-node.protected > span.fancytree-icon {
filter: drop-shadow(2px 2px 2px black);
}
@@ -103,6 +113,9 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title {
.icon-action {
cursor: pointer;
display: block;
height: 24px;
width: 24px;
}
#protect-button, #unprotect-button {
@@ -268,4 +281,9 @@ div.ui-tooltip {
#attribute-list button {
padding: 2px;
margin-right: 5px;
}
#attachment-table th, #attachment-table td {
padding: 10px;
font-size: large;
}

View File

@@ -0,0 +1,74 @@
"use strict";
const express = require('express');
const router = express.Router();
const sql = require('../../services/sql');
const auth = require('../../services/auth');
const notes = require('../../services/notes');
const attributes = require('../../services/attributes');
const protected_session = require('../../services/protected_session');
const multer = require('multer')();
const wrap = require('express-promise-wrap').wrap;
router.post('/upload/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => {
const sourceId = req.headers.source_id;
const parentNoteId = req.params.parentNoteId;
const file = req.file;
const originalName = file.originalname;
const size = file.size;
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]);
if (!note) {
return res.status(404).send(`Note ${parentNoteId} doesn't exist.`);
}
await sql.doInTransaction(async () => {
const noteId = (await notes.createNewNote(parentNoteId, {
title: originalName,
content: file.buffer,
target: 'into',
isProtected: false,
type: 'file',
mime: file.mimetype
}, req, sourceId)).noteId;
await attributes.createAttribute(noteId, "original_file_name", originalName, sourceId);
await attributes.createAttribute(noteId, "file_size", size, sourceId);
res.send({
noteId: noteId
});
});
}));
router.get('/download/:noteId', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => {
const noteId = req.params.noteId;
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
const protectedSessionId = req.query.protectedSessionId;
if (!note) {
return res.status(404).send(`Note ${noteId} doesn't exist.`);
}
if (note.isProtected) {
const dataKey = protected_session.getDataKeyForProtectedSessionId(protectedSessionId);
if (!dataKey) {
res.status(401).send("Protected session not available");
return;
}
protected_session.decryptNote(dataKey, note);
}
const attributeMap = await attributes.getNoteAttributeMap(noteId);
const fileName = attributeMap.original_file_name ? attributeMap.original_file_name : note.title;
res.setHeader('Content-Disposition', 'attachment; filename=' + fileName);
res.setHeader('Content-Type', note.mime);
res.send(note.content);
}));
module.exports = router;

View File

@@ -2,56 +2,79 @@
const express = require('express');
const router = express.Router();
const rimraf = require('rimraf');
const fs = require('fs');
const sql = require('../../services/sql');
const data_dir = require('../../services/data_dir');
const html = require('html');
const auth = require('../../services/auth');
const wrap = require('express-promise-wrap').wrap;
const tar = require('tar-stream');
const sanitize = require("sanitize-filename");
const Repository = require("../../services/repository");
router.get('/:noteId/to/:directory', auth.checkApiAuth, wrap(async (req, res, next) => {
router.get('/:noteId/', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => {
const noteId = req.params.noteId;
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
if (!fs.existsSync(data_dir.EXPORT_DIR)) {
fs.mkdirSync(data_dir.EXPORT_DIR);
}
const completeExportDir = data_dir.EXPORT_DIR + '/' + directory;
if (fs.existsSync(completeExportDir)) {
rimraf.sync(completeExportDir);
}
fs.mkdirSync(completeExportDir);
const repo = new Repository(req);
const noteTreeId = await sql.getValue('SELECT noteTreeId FROM note_tree WHERE noteId = ?', [noteId]);
await exportNote(noteTreeId, completeExportDir);
const pack = tar.pack();
res.send({});
const name = await exportNote(noteTreeId, '', pack, repo);
pack.finalize();
res.setHeader('Content-Disposition', 'attachment; filename="' + name + '.tar"');
res.setHeader('Content-Type', 'application/tar');
pack.pipe(res);
}));
async function exportNote(noteTreeId, dir) {
async function exportNote(noteTreeId, directory, pack, repo) {
const noteTree = await sql.getRow("SELECT * FROM note_tree WHERE noteTreeId = ?", [noteTreeId]);
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteTree.noteId]);
const note = await repo.getEntity("SELECT notes.* FROM notes WHERE noteId = ?", [noteTree.noteId]);
const pos = (noteTree.notePosition + '').padStart(4, '0');
if (note.isProtected) {
return;
}
fs.writeFileSync(dir + '/' + pos + '-' + note.title + '.html', html.prettyPrint(note.content, {indent_size: 2}));
const metadata = await getMetadata(note);
if (metadata.attributes.find(attr => attr.name === 'exclude_from_export')) {
return;
}
const metadataJson = JSON.stringify(metadata, null, '\t');
const childFileName = directory + sanitize(note.title);
pack.entry({ name: childFileName + ".meta", size: metadataJson.length }, metadataJson);
const content = note.type === 'text' ? html.prettyPrint(note.content, {indent_size: 2}) : note.content;
pack.entry({ name: childFileName + ".dat", size: content.length }, content);
const children = await sql.getRows("SELECT * FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [note.noteId]);
if (children.length > 0) {
const childrenDir = dir + '/' + pos + '-' + note.title;
fs.mkdirSync(childrenDir);
for (const child of children) {
await exportNote(child.noteTreeId, childrenDir);
await exportNote(child.noteTreeId, childFileName + "/", pack, repo);
}
}
return childFileName;
}
async function getMetadata(note) {
return {
version: 1,
title: note.title,
type: note.type,
mime: note.mime,
attributes: (await note.getAttributes()).map(attr => {
return {
name: attr.name,
value: attr.value
};
})
};
}
module.exports = router;

View File

@@ -2,104 +2,136 @@
const express = require('express');
const router = express.Router();
const fs = require('fs');
const sql = require('../../services/sql');
const data_dir = require('../../services/data_dir');
const utils = require('../../services/utils');
const sync_table = require('../../services/sync_table');
const auth = require('../../services/auth');
const attributes = require('../../services/attributes');
const notes = require('../../services/notes');
const wrap = require('express-promise-wrap').wrap;
const tar = require('tar-stream');
const multer = require('multer')();
const stream = require('stream');
const path = require('path');
router.get('/:directory/to/:parentNoteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const directory = req.params.directory.replace(/[^0-9a-zA-Z_-]/gi, '');
function getFileName(name) {
let key;
if (name.endsWith(".dat")) {
key = "data";
name = name.substr(0, name.length - 4);
}
else if (name.endsWith((".meta"))) {
key = "meta";
name = name.substr(0, name.length - 5);
}
else {
throw new Error("Unknown file type in import archive: " + name);
}
return {name, key};
}
async function parseImportFile(file) {
const fileMap = {};
const files = [];
const extract = tar.extract();
extract.on('entry', function(header, stream, next) {
let {name, key} = getFileName(header.name);
let file = fileMap[name];
if (!file) {
file = fileMap[name] = {
children: []
};
let parentFileName = path.dirname(header.name);
if (parentFileName && parentFileName !== '.') {
fileMap[parentFileName].children.push(file);
}
else {
files.push(file);
}
}
const chunks = [];
stream.on("data", function (chunk) {
chunks.push(chunk);
});
// header is the tar header
// stream is the content body (might be an empty stream)
// call next when you are done with this entry
stream.on('end', function() {
file[key] = Buffer.concat(chunks);
if (key === "meta") {
file[key] = JSON.parse(file[key].toString("UTF-8"));
}
next(); // ready for next entry
});
stream.resume(); // just auto drain the stream
});
return new Promise(resolve => {
extract.on('finish', function() {
resolve(files);
});
const bufferStream = new stream.PassThrough();
bufferStream.end(file.buffer);
bufferStream.pipe(extract);
});
}
router.post('/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => {
const sourceId = req.headers.source_id;
const parentNoteId = req.params.parentNoteId;
const file = req.file;
const dir = data_dir.EXPORT_DIR + '/' + directory;
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]);
await sql.doInTransaction(async () => await importNotes(dir, parentNoteId));
if (!note) {
return res.status(404).send(`Note ${parentNoteId} doesn't exist.`);
}
const files = await parseImportFile(file);
await sql.doInTransaction(async () => {
await importNotes(files, parentNoteId, sourceId);
});
res.send({});
}));
async function importNotes(dir, parentNoteId) {
const parent = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]);
if (!parent) {
return;
}
const fileList = fs.readdirSync(dir);
for (const file of fileList) {
const path = dir + '/' + file;
if (fs.lstatSync(path).isDirectory()) {
continue;
async function importNotes(files, parentNoteId, sourceId) {
for (const file of files) {
if (file.meta.version !== 1) {
throw new Error("Can't read meta data version " + file.meta.version);
}
if (!file.endsWith('.html')) {
continue;
if (file.meta.type !== 'file') {
file.data = file.data.toString("UTF-8");
}
const fileNameWithoutExt = file.substr(0, file.length - 5);
let noteTitle;
let notePos;
const match = fileNameWithoutExt.match(/^([0-9]{4})-(.*)$/);
if (match) {
notePos = parseInt(match[1]);
noteTitle = match[2];
}
else {
let maxPos = await sql.getValue("SELECT MAX(notePosition) FROM note_tree WHERE parentNoteId = ? AND isDeleted = 0", [parentNoteId]);
if (maxPos) {
notePos = maxPos + 1;
}
else {
notePos = 0;
}
noteTitle = fileNameWithoutExt;
}
const noteText = fs.readFileSync(path, "utf8");
const noteId = utils.newNoteId();
const noteTreeId = utils.newNoteRevisionId();
const now = utils.nowDate();
await sql.insert('note_tree', {
noteTreeId: noteTreeId,
noteId: noteId,
parentNoteId: parentNoteId,
notePosition: notePos,
isExpanded: 0,
isDeleted: 0,
dateModified: now
const noteId = await notes.createNote(parentNoteId, file.meta.title, file.data, {
type: file.meta.type,
mime: file.meta.mime,
sourceId: sourceId
});
await sync_table.addNoteTreeSync(noteTreeId);
for (const attr of file.meta.attributes) {
await attributes.createAttribute(noteId, attr.name, attr.value);
}
await sql.insert('notes', {
noteId: noteId,
title: noteTitle,
content: noteText,
isDeleted: 0,
isProtected: 0,
type: 'text',
mime: 'text/html',
dateCreated: now,
dateModified: now
});
await sync_table.addNoteSync(noteId);
const noteDir = dir + '/' + fileNameWithoutExt;
if (fs.existsSync(noteDir) && fs.lstatSync(noteDir).isDirectory()) {
await importNotes(noteDir, noteId);
if (file.children.length > 0) {
await importNotes(file.children, noteId, sourceId);
}
}
}

View File

@@ -5,6 +5,7 @@ const router = express.Router();
const auth = require('../../services/auth');
const sql = require('../../services/sql');
const notes = require('../../services/notes');
const attributes = require('../../services/attributes');
const log = require('../../services/log');
const utils = require('../../services/utils');
const protected_session = require('../../services/protected_session');
@@ -25,8 +26,19 @@ router.get('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
protected_session.decryptNote(req, detail);
let attributeMap = null;
if (detail.type === 'file') {
// no need to transfer attachment payload for this request
detail.content = null;
// attributes contain important attachment metadata - filename and size
attributeMap = await attributes.getNoteAttributeMap(noteId);
}
res.send({
detail: detail
detail: detail,
attributes: attributeMap
});
}));

View File

@@ -4,13 +4,23 @@ const express = require('express');
const router = express.Router();
const auth = require('../../services/auth');
const wrap = require('express-promise-wrap').wrap;
const notes = require('../../services/notes');
const attributes = require('../../services/attributes');
const script = require('../../services/script');
const Repository = require('../../services/repository');
router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
const ret = await script.executeScript(req, req.body.script, req.body.params);
const ret = await script.executeScript(req, req.body.script, req.body.params, req.body.startNoteId, req.body.currentNoteId);
res.send({
executionResult: ret
});
}));
router.post('/run/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const repository = new Repository(req);
const note = await repository.getNote(req.params.noteId);
const ret = await script.executeNote(req, note);
res.send({
executionResult: ret
@@ -18,65 +28,28 @@ router.post('/exec', auth.checkApiAuth, wrap(async (req, res, next) => {
}));
router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteIds = await attributes.getNoteIdsWithAttribute("run_on_startup");
const repository = new Repository(req);
const notes = await attributes.getNotesWithAttribute(repository, "run", "frontend_startup");
const scripts = [];
for (const noteId of noteIds) {
scripts.push(await getNoteWithSubtreeScript(noteId, repository));
for (const note of notes) {
const bundle = await script.getScriptBundle(note);
if (bundle) {
scripts.push(bundle);
}
}
res.send(scripts);
}));
router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteId = req.params.noteId;
router.get('/bundle/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const repository = new Repository(req);
const note = await repository.getNote(req.params.noteId);
const bundle = await script.getScriptBundle(note);
const noteScript = (await repository.getNote(noteId)).content;
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository);
res.send(subTreeScripts + noteScript);
res.send(bundle);
}));
async function getNoteWithSubtreeScript(noteId, repository) {
const noteScript = (await repository.getNote(noteId)).content;
const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository);
return subTreeScripts + noteScript;
}
async function getSubTreeScripts(parentId, includedNoteIds, repository) {
const children = await repository.getEntities(`
SELECT notes.*
FROM notes JOIN note_tree USING(noteId)
WHERE note_tree.isDeleted = 0 AND notes.isDeleted = 0
AND note_tree.parentNoteId = ? AND notes.type = 'code'
AND (notes.mime = 'application/javascript' OR notes.mime = 'text/html')`, [parentId]);
let script = "\r\n";
for (const child of children) {
if (includedNoteIds.includes(child.noteId)) {
return;
}
includedNoteIds.push(child.noteId);
script += await getSubTreeScripts(child.noteId, includedNoteIds, repository);
if (child.mime === 'application/javascript') {
child.content = '<script>' + child.content + '</script>';
}
script += child.content + "\r\n";
}
return script;
}
module.exports = router;

View File

@@ -79,9 +79,12 @@ router.get('/changed', auth.checkApiAuth, wrap(async (req, res, next) => {
router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => {
const noteId = req.params.noteId;
const entity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
sync.serializeNoteContentBuffer(entity);
res.send({
entity: await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId])
entity: entity
});
}));

View File

@@ -6,6 +6,7 @@ const sql = require('../../services/sql');
const options = require('../../services/options');
const utils = require('../../services/utils');
const auth = require('../../services/auth');
const config = require('../../services/config');
const protected_session = require('../../services/protected_session');
const sync_table = require('../../services/sync_table');
const wrap = require('express-promise-wrap').wrap;
@@ -41,6 +42,7 @@ router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => {
AND notes.isDeleted = 0`);
res.send({
instanceName: config.General ? config.General.instanceName : null,
notes: notes,
hiddenInAutocomplete: hiddenInAutocomplete,
start_note_path: await options.getOption('start_note_path')

View File

@@ -5,13 +5,32 @@ const router = express.Router();
const auth = require('../services/auth');
const source_id = require('../services/source_id');
const sql = require('../services/sql');
const Repository = require('../services/repository');
const attributes = require('../services/attributes');
const wrap = require('express-promise-wrap').wrap;
router.get('', auth.checkAuth, wrap(async (req, res, next) => {
const repository = new Repository(req);
res.render('index', {
sourceId: await source_id.generateSourceId(),
maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync")
maxSyncIdAtLoad: await sql.getValue("SELECT MAX(id) FROM sync"),
appCss: await getAppCss(repository)
});
}));
async function getAppCss(repository) {
let css = '';
const notes = attributes.getNotesWithAttribute(repository, 'app_css');
for (const note of await notes) {
css += `/* ${note.noteId} */
${note.content}
`;
}
return css;
}
module.exports = router;

View File

@@ -29,6 +29,7 @@ const imageRoute = require('./api/image');
const attributesRoute = require('./api/attributes');
const scriptRoute = require('./api/script');
const senderRoute = require('./api/sender');
const attachmentsRoute = require('./api/attachments');
function register(app) {
app.use('/', indexRoute);
@@ -61,6 +62,7 @@ function register(app) {
app.use('/api/images', imageRoute);
app.use('/api/script', scriptRoute);
app.use('/api/sender', senderRoute);
app.use('/api/attachments', attachmentsRoute);
}
module.exports = {

Binary file not shown.

BIN
src/scripts/Today.tar Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -1,13 +0,0 @@
api.addButtonToToolbar('go-today', $('<button class="btn btn-xs" onclick="goToday();"><span class="ui-icon ui-icon-calendar"></span> Today</button>'));
window.goToday = async function() {
const todayDateStr = formatDateISO(new Date());
const todayNoteId = await server.exec([todayDateStr], async todayDateStr => {
return await this.getDateNoteId(todayDateStr);
});
api.activateNote(todayNoteId);
};
$(document).bind('keydown', "alt+t", window.goToday);

View File

@@ -1,99 +0,0 @@
<form id="weight-form" style="display: flex; width: 500px; justify-content: space-around; align-items: flex-end;">
<div>
<label for="weight-date">Date</label>
<input type="text" id="weight-date" class="form-control" style="width: 150px; text-align: center;" />
</div>
<div>
<label for="weight">Weight</label>
<input type="number" id="weight" value="80.0" step="0.1" class="form-control" style="text-align: center; width: 100px;" />
</div>
<button type="submit" class="btn btn-primary">Add</button>
</form>
<br/><br/>
<canvas id="canvas"></canvas>
<script>
(async function() {
const dateEl = $("#weight-date");
const weightEl = $("#weight");
dateEl.datepicker();
dateEl.datepicker('option', 'dateFormat', 'yy-mm-dd');
dateEl.datepicker('setDate', new Date());
async function saveWeight() {
await server.exec([dateEl.val(), weightEl.val()], async (date, weight) => {
const dataNote = await this.getNoteWithAttribute('date_data', date);
if (dataNote) {
dataNote.jsonContent.weight = weight;
await this.updateEntity(dataNote);
}
else {
const parentNoteId = await this.getDateNoteId(date);
const jsonContent = { weight: weight };
await this.createNote(parentNoteId, 'data', jsonContent, {
json: true,
attributes: {
date_data: date,
hide_in_autocomplete: null
}
});
}
});
showMessage("Weight has been saved");
drawChart();
}
async function drawChart() {
const data = await server.exec([], async () => {
const notes = await this.getNotesWithAttribute('date_data');
const data = [];
for (const note of notes) {
const dateAttr = await note.getAttribute('date_data');
data.push({
date: dateAttr.value,
weight: note.jsonContent.weight
});
}
data.sort((a, b) => a.date < b.date ? -1 : +1);
return data;
});
const ctx = $("#canvas")[0].getContext("2d");
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(row => row.date),
datasets: [{
label: "Weight",
backgroundColor: 'red',
borderColor: 'red',
data: data.map(row => row.weight),
fill: false
}]
}
});
}
$("#weight-form").submit(event => {
saveWeight();
event.preventDefault();
});
drawChart();
})();
</script>

View File

@@ -3,7 +3,7 @@
const build = require('./build');
const packageJson = require('../../package');
const APP_DB_VERSION = 76;
const APP_DB_VERSION = 78;
module.exports = {
app_version: packageJson.version,

View File

@@ -3,17 +3,22 @@
const sql = require('./sql');
const utils = require('./utils');
const sync_table = require('./sync_table');
const Repository = require('./repository');
const BUILTIN_ATTRIBUTES = [
'run_on_startup',
'frontend_startup',
'backend_startup',
'disable_versioning',
'calendar_root',
'hide_in_autocomplete'
'hide_in_autocomplete',
'exclude_from_export',
'run',
'manual_transaction_handling',
'disable_inclusion',
'app_css'
];
async function getNoteAttributeMap(noteId) {
return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]);
return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ? AND isDeleted = 0`, [noteId]);
}
async function getNoteIdWithAttribute(name, value) {
@@ -24,9 +29,7 @@ async function getNoteIdWithAttribute(name, value) {
AND attributes.value = ?`, [name, value]);
}
async function getNotesWithAttribute(dataKey, name, value) {
const repository = new Repository(dataKey);
async function getNotesWithAttribute(repository, name, value) {
let notes;
if (value !== undefined) {
@@ -41,8 +44,8 @@ async function getNotesWithAttribute(dataKey, name, value) {
return notes;
}
async function getNoteWithAttribute(dataKey, name, value) {
const notes = getNotesWithAttribute(dataKey, name, value);
async function getNoteWithAttribute(repository, name, value) {
const notes = getNotesWithAttribute(repository, name, value);
return notes.length > 0 ? notes[0] : null;
}
@@ -52,15 +55,21 @@ async function getNoteIdsWithAttribute(name) {
WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.isDeleted = 0`, [name]);
}
async function createAttribute(noteId, name, value = null, sourceId = null) {
async function createAttribute(noteId, name, value = "", sourceId = null) {
if (value === null || value === undefined) {
value = "";
}
const now = utils.nowDate();
const attributeId = utils.newAttributeId();
const position = 1 + await sql.getValue(`SELECT COALESCE(MAX(position), 0) FROM attributes WHERE noteId = ?`, [noteId]);
await sql.insert("attributes", {
attributeId: attributeId,
noteId: noteId,
name: name,
value: value,
position: position,
dateModified: now,
dateCreated: now,
isDeleted: false

View File

@@ -214,7 +214,7 @@ async function runAllChecks() {
FROM
notes
WHERE
type != 'text' AND type != 'code' AND type != 'render'`,
type != 'text' AND type != 'code' AND type != 'render' AND type != 'file'`,
"Note has invalid type", errorList);
await runSyncRowChecks("notes", "noteId", errorList);

View File

@@ -20,6 +20,5 @@ module.exports = {
DOCUMENT_PATH,
BACKUP_DIR,
LOG_DIR,
EXPORT_DIR,
ANONYMIZED_DB_DIR
};

View File

@@ -88,7 +88,7 @@ function noteTitleIv(iv) {
return "0" + iv;
}
function noteTextIv(iv) {
function noteContentIv(iv) {
return "1" + iv;
}
@@ -97,5 +97,5 @@ module.exports = {
decrypt,
decryptString,
noteTitleIv,
noteTextIv
noteContentIv
};

View File

@@ -29,7 +29,7 @@ async function getNoteStartingWith(parentNoteId, startsWith) {
AND note_tree.isDeleted = 0`, [parentNoteId]);
}
async function getRootNoteId() {
async function getRootCalendarNoteId() {
let rootNoteId = await sql.getValue(`SELECT notes.noteId FROM notes JOIN attributes USING(noteId)
WHERE attributes.name = '${CALENDAR_ROOT_ATTRIBUTE}' AND notes.isDeleted = 0`);
@@ -91,7 +91,7 @@ async function getMonthNoteId(dateTimeStr, rootNoteId) {
async function getDateNoteId(dateTimeStr, rootNoteId = null) {
if (!rootNoteId) {
rootNoteId = await getRootNoteId();
rootNoteId = await getRootCalendarNoteId();
}
const dateStr = dateTimeStr.substr(0, 10);
@@ -119,7 +119,7 @@ async function getDateNoteId(dateTimeStr, rootNoteId = null) {
}
module.exports = {
getRootNoteId,
getRootCalendarNoteId,
getYearNoteId,
getMonthNoteId,
getDateNoteId

View File

@@ -83,6 +83,40 @@ async function createNewNote(parentNoteId, noteOpts, dataKey, sourceId) {
};
}
async function createNote(parentNoteId, title, content = "", extraOptions = {}) {
if (!parentNoteId) throw new Error("Empty parentNoteId");
if (!title) throw new Error("Empty title");
const note = {
title: title,
content: extraOptions.json ? JSON.stringify(content, null, '\t') : content,
target: 'into',
isProtected: extraOptions.isProtected !== undefined ? extraOptions.isProtected : false,
type: extraOptions.type,
mime: extraOptions.mime
};
if (extraOptions.json) {
note.type = "code";
note.mime = "application/json";
}
if (!note.type) {
note.type = "text";
note.mime = "text/html";
}
const {noteId} = await createNewNote(parentNoteId, note, extraOptions.dataKey, extraOptions.sourceId);
if (extraOptions.attributes) {
for (const attrName in extraOptions.attributes) {
await attributes.createAttribute(noteId, attrName, extraOptions.attributes[attrName]);
}
}
return noteId;
}
async function protectNoteRecursively(noteId, dataKey, protect, sourceId) {
const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
@@ -148,10 +182,14 @@ async function protectNoteHistory(noteId, dataKey, protect, sourceId) {
async function saveNoteHistory(noteId, dataKey, sourceId, nowStr) {
const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (oldNote.type === 'file') {
return;
}
if (oldNote.isProtected) {
protected_session.decryptNote(dataKey, oldNote);
note.isProtected = false;
oldNote.isProtected = false;
}
const newNoteRevisionId = utils.newNoteRevisionId();
@@ -217,7 +255,21 @@ async function saveNoteImages(noteId, noteText, sourceId) {
}
}
async function loadFile(noteId, newNote, dataKey) {
const oldNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]);
if (oldNote.isProtected) {
await protected_session.decryptNote(dataKey, oldNote);
}
newNote.detail.content = oldNote.content;
}
async function updateNote(noteId, newNote, dataKey, sourceId) {
if (newNote.detail.type === 'file') {
await loadFile(noteId, newNote, dataKey);
}
if (newNote.detail.isProtected) {
await protected_session.encryptNote(dataKey, newNote.detail);
}
@@ -289,6 +341,7 @@ async function deleteNote(noteTreeId, sourceId) {
module.exports = {
createNewNote,
createNote,
updateNote,
deleteNote,
protectNoteRecursively

View File

@@ -26,6 +26,10 @@ function getDataKey(obj) {
const protectedSessionId = getProtectedSessionId(obj);
return getDataKeyForProtectedSessionId(protectedSessionId);
}
function getDataKeyForProtectedSessionId(protectedSessionId) {
if (protectedSessionId && session.protectedSessionId === protectedSessionId) {
return session.decryptedDataKey;
}
@@ -52,7 +56,14 @@ function decryptNote(dataKey, note) {
}
if (note.content) {
note.content = data_encryption.decryptString(dataKey, data_encryption.noteTextIv(note.noteId), note.content);
const contentIv = data_encryption.noteContentIv(note.noteId);
if (note.type === 'file') {
note.content = data_encryption.decrypt(dataKey, contentIv, note.content);
}
else {
note.content = data_encryption.decryptString(dataKey, contentIv, note.content);
}
}
}
@@ -76,7 +87,7 @@ function decryptNoteHistoryRow(dataKey, hist) {
}
if (hist.content) {
hist.content = data_encryption.decryptString(dataKey, data_encryption.noteTextIv(hist.noteRevisionId), hist.content);
hist.content = data_encryption.decryptString(dataKey, data_encryption.noteContentIv(hist.noteRevisionId), hist.content);
}
}
@@ -92,19 +103,20 @@ function encryptNote(dataKey, note) {
dataKey = getDataKey(dataKey);
note.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(note.noteId), note.title);
note.content = data_encryption.encrypt(dataKey, data_encryption.noteTextIv(note.noteId), note.content);
note.content = data_encryption.encrypt(dataKey, data_encryption.noteContentIv(note.noteId), note.content);
}
function encryptNoteHistoryRow(dataKey, history) {
dataKey = getDataKey(dataKey);
history.title = data_encryption.encrypt(dataKey, data_encryption.noteTitleIv(history.noteRevisionId), history.title);
history.content = data_encryption.encrypt(dataKey, data_encryption.noteTextIv(history.noteRevisionId), history.content);
history.content = data_encryption.encrypt(dataKey, data_encryption.noteContentIv(history.noteRevisionId), history.content);
}
module.exports = {
setDataKey,
getDataKey,
getDataKeyForProtectedSessionId,
isProtectedSessionAvailable,
decryptNote,
decryptNotes,

27
src/services/scheduler.js Normal file
View File

@@ -0,0 +1,27 @@
const script = require('./script');
const Repository = require('./repository');
const repo = new Repository();
async function runNotesWithAttribute(runAttrValue) {
const notes = await repo.getEntities(`
SELECT notes.*
FROM notes
JOIN attributes ON attributes.noteId = notes.noteId
AND attributes.isDeleted = 0
AND attributes.name = 'run'
AND attributes.value = ?
WHERE
notes.type = 'code'
AND notes.isDeleted = 0`, [runAttrValue]);
for (const note of notes) {
script.executeNote(null, note);
}
}
setTimeout(() => runNotesWithAttribute('backend_startup'), 10 * 1000);
setInterval(() => runNotesWithAttribute('hourly'), 3600 * 1000);
setInterval(() => runNotesWithAttribute('daily'), 24 * 3600 * 1000);

View File

@@ -1,27 +1,139 @@
const log = require('./log');
const sql = require('./sql');
const ScriptContext = require('./script_context');
const Repository = require('./repository');
async function executeScript(dataKey, script, params) {
log.info('Executing script: ' + script);
async function executeNote(dataKey, note) {
if (!note.isJavaScript()) {
return;
}
const ctx = new ScriptContext(dataKey);
const bundle = await getScriptBundle(note);
const paramsStr = getParams(params);
await executeBundle(dataKey, bundle);
}
let ret;
async function executeBundle(dataKey, bundle, startNote) {
if (!startNote) {
// this is the default case, the only exception is when we want to preserve frontend startNote
startNote = bundle.note;
}
await sql.doInTransaction(async () => {
ret = await (function() { return eval(`(${script})(${paramsStr})`); }.call(ctx));
});
// last \r\n is necessary if script contains line comment on its last line
const script = "async function() {\r\n" + bundle.script + "\r\n}";
return ret;
const ctx = new ScriptContext(dataKey, startNote, bundle.allNotes);
if (await bundle.note.hasAttribute('manual_transaction_handling')) {
return await execute(ctx, script, '');
}
else {
return await sql.doInTransaction(async () => execute(ctx, script, ''));
}
}
/**
* This method preserves frontend startNode - that's why we start execution from currentNote and override
* bundle's startNote.
*/
async function executeScript(dataKey, script, params, startNoteId, currentNoteId) {
const repository = new Repository(dataKey);
const startNote = await repository.getNote(startNoteId);
const currentNote = await repository.getNote(currentNoteId);
currentNote.content = `return await (${script}\r\n)(${getParams(params)})`;
currentNote.type = 'code';
currentNote.mime = 'application/javascript;env=backend';
const bundle = await getScriptBundle(currentNote);
return await executeBundle(dataKey, bundle, startNote);
}
async function execute(ctx, script, paramsStr) {
return await (function() { return eval(`const apiContext = this;\r\n(${script}\r\n)(${paramsStr})`); }.call(ctx));
}
function getParams(params) {
return params.map(p => JSON.stringify(p)).join(",");
if (!params) {
return params;
}
return params.map(p => {
if (typeof p === "string" && p.startsWith("!@#Function: ")) {
return p.substr(13);
}
else {
return JSON.stringify(p);
}
}).join(",");
}
async function getScriptBundle(note, root = true, scriptEnv = null, includedNoteIds = []) {
if (!note.isJavaScript() && !note.isHtml() && note.type !== 'render') {
return;
}
if (!root && await note.hasAttribute('disable_inclusion')) {
return;
}
if (root) {
scriptEnv = note.getScriptEnv();
}
if (note.type !== 'file' && scriptEnv !== note.getScriptEnv()) {
return;
}
const bundle = {
note: note,
script: '',
html: '',
allNotes: [note]
};
if (includedNoteIds.includes(note.noteId)) {
return bundle;
}
includedNoteIds.push(note.noteId);
const modules = [];
for (const child of await note.getChildren()) {
const childBundle = await getScriptBundle(child, false, scriptEnv, includedNoteIds);
if (childBundle) {
modules.push(childBundle.note);
bundle.script += childBundle.script;
bundle.html += childBundle.html;
bundle.allNotes = bundle.allNotes.concat(childBundle.allNotes);
}
}
if (note.isJavaScript()) {
bundle.script += `
apiContext.modules['${note.noteId}'] = {};
${root ? 'return ' : ''}await (async function(exports, module, api` + (modules.length > 0 ? ', ' : '') +
modules.map(child => sanitizeVariableName(child.title)).join(', ') + `) {
${note.content}
})({}, apiContext.modules['${note.noteId}'], apiContext.apis['${note.noteId}']` + (modules.length > 0 ? ', ' : '') +
modules.map(mod => `apiContext.modules['${mod.noteId}'].exports`).join(', ') + `);
`;
}
else if (note.isHtml()) {
bundle.html += note.content;
}
return bundle;
}
function sanitizeVariableName(str) {
return str.replace(/[^a-z0-9_]/gim, "");
}
module.exports = {
executeScript
executeNote,
executeScript,
getScriptBundle
};

View File

@@ -1,20 +1,42 @@
const log = require('./log');
const protected_session = require('./protected_session');
const notes = require('./notes');
const sql = require('./sql');
const utils = require('./utils');
const attributes = require('./attributes');
const date_notes = require('./date_notes');
const config = require('./config');
const Repository = require('./repository');
const axios = require('axios');
function ScriptContext(noteId, dataKey) {
this.dataKey = protected_session.getDataKey(dataKey);
this.repository = new Repository(dataKey);
function ScriptContext(dataKey, startNote, allNotes) {
dataKey = protected_session.getDataKey(dataKey);
this.modules = {};
this.notes = utils.toObject(allNotes, note => [note.noteId, note]);
this.apis = utils.toObject(allNotes, note => [note.noteId, new ScriptApi(dataKey, startNote, note)]);
}
function ScriptApi(dataKey, startNote, currentNote) {
const repository = new Repository(dataKey);
this.startNote = startNote;
this.currentNote = currentNote;
this.axios = axios;
this.utils = {
unescapeHtml: utils.unescapeHtml,
isoDateTimeStr: utils.dateStr
};
this.getInstanceName = () => config.General ? config.General.instanceName : null;
this.getNoteById = async function(noteId) {
return this.repository.getNote(noteId);
return repository.getNote(noteId);
};
this.getNotesWithAttribute = async function (attrName, attrValue) {
return await attributes.getNotesWithAttribute(this.dataKey, attrName, attrValue);
return await attributes.getNotesWithAttribute(repository, attrName, attrValue);
};
this.getNoteWithAttribute = async function (attrName, attrValue) {
@@ -23,46 +45,22 @@ function ScriptContext(noteId, dataKey) {
return notes.length > 0 ? notes[0] : null;
};
this.createNote = async function (parentNoteId, name, jsonContent, extraOptions = {}) {
const note = {
title: name,
content: extraOptions.json ? JSON.stringify(jsonContent, null, '\t') : jsonContent,
target: 'into',
isProtected: extraOptions.isProtected !== undefined ? extraOptions.isProtected : false,
type: extraOptions.type,
mime: extraOptions.mime
};
this.createNote = async function(parentNoteId, title, content = "", extraOptions = {}) {
extraOptions.dataKey = dataKey;
if (extraOptions.json) {
note.type = "code";
note.mime = "application/json";
}
if (!note.type) {
note.type = "text";
note.mime = "text/html";
}
const noteId = (await notes.createNewNote(parentNoteId, note, this.dataKey)).noteId;
if (extraOptions.attributes) {
for (const attrName in extraOptions.attributes) {
await attributes.createAttribute(noteId, attrName, extraOptions.attributes[attrName]);
}
}
return noteId;
return await notes.createNote(parentNoteId, title, content, extraOptions);
};
this.createAttribute = attributes.createAttribute;
this.updateEntity = this.repository.updateEntity;
this.updateEntity = repository.updateEntity;
this.log = function(message) {
log.info(`Script: ${message}`);
};
this.log = message => log.info(`Script ${currentNote.noteId}: ${message}`);
this.getRootCalendarNoteId = date_notes.getRootCalendarNoteId;
this.getDateNoteId = date_notes.getDateNoteId;
this.transaction = sql.doInTransaction;
}
module.exports = ScriptContext;

View File

@@ -195,6 +195,7 @@ async function doInTransaction(func) {
await transactionPromise;
}
let ret = null;
const error = new Error(); // to capture correct stack trace in case of exception
transactionActive = true;
@@ -202,7 +203,7 @@ async function doInTransaction(func) {
try {
await beginTransaction();
await func();
ret = await func();
await commit();
@@ -223,6 +224,8 @@ async function doInTransaction(func) {
if (transactionActive) {
await transactionPromise;
}
return ret;
}
async function isDbUpToDate() {

View File

@@ -204,6 +204,8 @@ async function pushEntity(sync, syncContext) {
if (sync.entityName === 'notes') {
entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]);
serializeNoteContentBuffer(entity);
}
else if (sync.entityName === 'note_tree') {
entity = await sql.getRow('SELECT * FROM note_tree WHERE noteTreeId = ?', [sync.entityId]);
@@ -258,6 +260,12 @@ async function pushEntity(sync, syncContext) {
await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload);
}
function serializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = note.content.toString("binary");
}
}
async function checkContentHash(syncContext) {
const resp = await syncRequest(syncContext, 'GET', '/api/sync/check');
@@ -350,5 +358,6 @@ sql.dbReady.then(() => {
});
module.exports = {
sync
sync,
serializeNoteContentBuffer
};

View File

@@ -1,10 +1,17 @@
const sql = require('./sql');
const log = require('./log');
const eventLog = require('./event_log');
const notes = require('./notes');
const sync_table = require('./sync_table');
function deserializeNoteContentBuffer(note) {
if (note.type === 'file') {
note.content = new Buffer(note.content, 'binary');
}
}
async function updateNote(entity, sourceId) {
deserializeNoteContentBuffer(entity);
const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]);
if (!origNote || origNote.dateModified <= entity.dateModified) {

View File

@@ -2,6 +2,7 @@
const crypto = require('crypto');
const randtoken = require('rand-token').generator({source: 'crypto'});
const unescape = require('unescape');
function newNoteId() {
return randomString(12);
@@ -129,6 +130,22 @@ async function stopWatch(what, func) {
return ret;
}
function unescapeHtml(str) {
return unescape(str);
}
function toObject(array, fn) {
const obj = {};
for (const item of array) {
const ret = fn(item);
obj[ret[0]] = ret[1];
}
return obj;
}
module.exports = {
randomSecureToken,
randomString,
@@ -153,5 +170,7 @@ module.exports = {
getDateTimeForFile,
sanitizeSql,
assertArguments,
stopWatch
stopWatch,
unescapeHtml,
toObject
};

View File

@@ -40,22 +40,20 @@
<div class="hide-toggle" style="grid-area: tree-actions;">
<div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;">
<a onclick="noteTree.createNewTopLevelNote()" title="Create new top level note" class="icon-action">
<img src="images/icons/file-plus.png" alt="Create new top level note"/>
</a>
<a onclick="noteTree.createNewTopLevelNote()" title="Create new top level note" class="icon-action"
style="background: url('images/icons/file-plus.png')"></a>
<a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action">
<img src="images/icons/list.png" alt="Collapse note tree"/>
</a>
<a onclick="noteTree.collapseTree()" title="Collapse note tree" class="icon-action"
style="background: url('images/icons/list.png')"></a>
<a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action">
<img src="images/icons/crosshair.png" alt="Scroll to current note"/>
</a>
<a onclick="noteTree.scrollToCurrentNote()" title="Scroll to current note. Shortcut CTRL+." class="icon-action"
style="background: url('images/icons/crosshair.png')"></a>
<a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action">
<img src="images/icons/search.png" alt="Search in notes"/>
</a>
<a onclick="searchTree.toggleSearch()" title="Search in notes" class="icon-action"
style="background: url('images/icons/search.png')"></a>
</div>
<input type="file" id="import-upload" style="display: none" />
</div>
<div id="search-box" class="hide-toggle" style="grid-area: search; display: none; padding: 10px; margin-top: 10px;">
@@ -81,17 +79,13 @@
title="Protect the note so that password will be required to view the note"
class="icon-action"
id="protect-button"
style="display: none;">
<img src="images/icons/lock.png" alt="Protect note"/>
</a>
style="display: none; background: url('images/icons/lock.png')"></a>
<a onclick="protected_session.unprotectNoteAndSendToServer()"
title="Unprotect note so that password will not be required to access this note in the future"
class="icon-action"
id="unprotect-button"
style="display: none;">
<img src="images/icons/unlock.png" alt="Unprotect note"/>
</a>
style="display: none; background: url('images/icons/unlock.png')"></a>
&nbsp;
@@ -105,7 +99,7 @@
onclick="noteEditor.executeCurrentNote()">Execute <kbd>Ctrl+Enter</kbd></button>
<div class="dropdown" id="note-type">
<button id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm">
<button data-bind="disable: isDisabled()" id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm">
Type: <span data-bind="text: typeString()"></span>
<span class="caret"></span>
</button>
@@ -130,6 +124,7 @@
<li><a onclick="noteHistory.showCurrentNoteHistory();"><kbd>Alt+H</kbd> History</a></li>
<li><a onclick="attributesDialog.showDialog();"><kbd>Alt+A</kbd> Attributes</a></li>
<li><a onclick="noteSource.showDialog();"><kbd>Ctrl+U</kbd> HTML source</a></li>
<li><a onclick="uploadAttachment();">Upload attachment</a></li>
</ul>
</div>
</div>
@@ -141,6 +136,32 @@
<div id="note-detail-code"></div>
<div id="note-detail-render"></div>
<div id="note-detail-attachment">
<table id="attachment-table">
<tr>
<th>File name:</th>
<td id="attachment-filename"></td>
</tr>
<tr>
<th>File type:</th>
<td id="attachment-filetype"></td>
</tr>
<tr>
<th>File size:</th>
<td id="attachment-filesize"></td>
</tr>
<tr>
<td>
<button id="attachment-download" class="btn btn-primary" type="button">Download</button>
&nbsp;
<button id="attachment-open" class="btn btn-primary" type="button">Open</button>
</td>
</tr>
</table>
</div>
<input type="file" id="attachment-upload" style="display: none" />
</div>
<div id="attribute-list">
@@ -449,8 +470,6 @@
<link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet">
<script src="libraries/fancytree/jquery.fancytree-all.min.js"></script>
<script src="libraries/ckeditor/ckeditor.js"></script>
<script src="libraries/jquery.hotkeys.js"></script>
<script src="libraries/jquery.fancytree.hotkeys.js"></script>
@@ -458,15 +477,6 @@
<script src="libraries/knockout.min.js"></script>
<script src="libraries/codemirror/codemirror.js"></script>
<link rel="stylesheet" href="libraries/codemirror/codemirror.css">
<script src="libraries/codemirror/addon/mode/loadmode.js"></script>
<script src="libraries/codemirror/addon/fold/xml-fold.js"></script>
<script src="libraries/codemirror/addon/edit/matchbrackets.js"></script>
<script src="libraries/codemirror/addon/edit/matchtags.js"></script>
<script src="libraries/codemirror/addon/search/match-highlighter.js"></script>
<script src="libraries/codemirror/mode/meta.js"></script>
<link href="stylesheets/style.css" rel="stylesheet">
<script src="javascripts/utils.js"></script>
@@ -481,6 +491,7 @@
<script src="javascripts/drag_and_drop.js"></script>
<script src="javascripts/context_menu.js"></script>
<script src="javascripts/search_tree.js"></script>
<script src="javascripts/export.js"></script>
<!-- Note detail -->
<script src="javascripts/note_editor.js"></script>
@@ -510,5 +521,9 @@
// final form which is pretty ugly.
$("#container").show();
</script>
<style type="text/css">
<%= appCss %>
</style>
</body>
</html>