mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 02:16:05 +01:00 
			
		
		
		
	Compare commits
	
		
			58 Commits
		
	
	
		
			v0.6.2
			...
			v0.9.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | a5c9180533 | ||
|  | e86f1e0d05 | ||
|  | b6277049f3 | ||
|  | c831221cc4 | ||
|  | 577a168714 | ||
|  | b0bd27321a | ||
|  | 90c5348ca7 | ||
|  | 8e95b080da | ||
|  | 766a567a32 | ||
|  | 6d0218cb36 | ||
|  | d26170762b | ||
|  | b3209a9bbf | ||
|  | 61c2456cf6 | ||
|  | 1c6fc9029f | ||
|  | 5c91e38dfe | ||
|  | 07bf075894 | ||
|  | ddce5c959e | ||
|  | 3b9d1df05c | ||
|  | d239ef2956 | ||
|  | 7a865a9081 | ||
|  | 83d6c2970f | ||
|  | 8c7d159012 | ||
|  | d169f67901 | ||
|  | 982b723647 | ||
|  | 31d5ac05ff | ||
|  | 72d91d1571 | ||
|  | f4b57f4c57 | ||
|  | ee0833390a | ||
|  | 2acff07368 | ||
|  | bea1d24f07 | ||
|  | adc270c59f | ||
|  | 66064f7a94 | ||
|  | 1501fa8dbf | ||
|  | 60bba46d80 | ||
|  | 12c06ae97e | ||
|  | f0bea9cf71 | ||
|  | a555b6319c | ||
|  | 5dd93e4cdc | ||
|  | 3b4509d833 | ||
|  | 19308bbfbd | ||
|  | 4acc5432c3 | ||
|  | 08b8141fdf | ||
|  | e1200aa308 | ||
|  | 89666eb078 | ||
|  | d5605aa64d | ||
|  | 2582b016f9 | ||
|  | e8c52e25f0 | ||
|  | a149c6a105 | ||
|  | 131af9ab12 | ||
|  | aa2bbc6575 | ||
|  | 78e8c15786 | ||
|  | fda4146150 | ||
|  | ddc885066e | ||
|  | 08bc2afb49 | ||
|  | 1d0220b03d | ||
|  | 3033f7cc08 | ||
|  | 6b9ff47c88 | ||
|  | cdde6a4d8e | 
| @@ -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). | ||||
|   | ||||
							
								
								
									
										23
									
								
								db/migrations/0077__non_null_attribute_value.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								db/migrations/0077__non_null_attribute_value.sql
									
									
									
									
									
										Normal 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); | ||||
							
								
								
									
										1
									
								
								db/migrations/0078__javascript_type.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0078__javascript_type.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| UPDATE notes SET mime = 'application/javascript;env=frontend' WHERE type = 'code' AND mime = 'application/javascript'; | ||||
| @@ -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
									
									
									
								
							
							
						
						
									
										72
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -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", | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|   "name": "trilium", | ||||
|   "description": "Trilium Notes", | ||||
|   "version": "0.6.2", | ||||
|   "version": "0.9.0-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" | ||||
|   }, | ||||
|   | ||||
| @@ -73,7 +73,7 @@ require('./services/backup'); | ||||
| // trigger consistency checks timer | ||||
| require('./services/consistency_checks'); | ||||
|  | ||||
| require('./plugins/reddit'); | ||||
| require('./services/scheduler'); | ||||
|  | ||||
| module.exports = { | ||||
|     app, | ||||
|   | ||||
| @@ -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'); | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
| }); | ||||
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/paperclip.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/public/images/icons/paperclip.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 358 B | 
							
								
								
									
										
											BIN
										
									
								
								src/public/images/icons/play.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/public/images/icons/play.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 252 B | 
| @@ -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 | ||||
|     } | ||||
| })(); | ||||
| } | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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", { | ||||
|   | ||||
							
								
								
									
										32
									
								
								src/public/javascripts/export.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/public/javascripts/export.js
									
									
									
									
									
										Normal 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(); | ||||
| }); | ||||
| @@ -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); | ||||
| }); | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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> </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); | ||||
|   | ||||
| @@ -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 | ||||
|     }; | ||||
| })(); | ||||
| @@ -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')); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
|   | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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; | ||||
| } | ||||
| @@ -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 : {}; | ||||
|   | ||||
| @@ -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]); | ||||
|   | ||||
							
								
								
									
										92
									
								
								src/public/libraries/codemirror/addon/lint/eslint.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/public/libraries/codemirror/addon/lint/eslint.js
									
									
									
									
										vendored
									
									
										Normal 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) | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| }); | ||||
							
								
								
									
										73
									
								
								src/public/libraries/codemirror/addon/lint/lint.css
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/public/libraries/codemirror/addon/lint/lint.css
									
									
									
									
										vendored
									
									
										Normal 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%; | ||||
| } | ||||
							
								
								
									
										252
									
								
								src/public/libraries/codemirror/addon/lint/lint.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								src/public/libraries/codemirror/addon/lint/lint.js
									
									
									
									
										vendored
									
									
										Normal 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); | ||||
|   }); | ||||
| }); | ||||
| @@ -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
											
										
									
								
							| @@ -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}); | ||||
|   | ||||
							
								
								
									
										2
									
								
								src/public/libraries/codemirror/mode/meta.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								src/public/libraries/codemirror/mode/meta.js
									
									
									
									
										vendored
									
									
								
							| @@ -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
									
								
							
							
						
						
									
										101349
									
								
								src/public/libraries/eslint.js
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -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; | ||||
| } | ||||
							
								
								
									
										74
									
								
								src/routes/api/attachments.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/routes/api/attachments.js
									
									
									
									
									
										Normal 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; | ||||
| @@ -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 ('exclude_from_export' in metadata.attributes) { | ||||
|         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; | ||||
| @@ -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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|     }); | ||||
| })); | ||||
|  | ||||
|   | ||||
| @@ -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,26 @@ 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); | ||||
|  | ||||
|         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; | ||||
| @@ -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 | ||||
|     }); | ||||
| })); | ||||
|  | ||||
|   | ||||
| @@ -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') | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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 = { | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								src/scripts/Reddit Importer.tar
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/scripts/Reddit Importer.tar
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -1,4 +1,4 @@ | ||||
| <form id="weight-form" style="display: flex; width: 500px; justify-content: space-around; align-items: flex-end;"> | ||||
| <form id="weight-form" style="display: flex; width: 700px; 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;" /> | ||||
| @@ -7,6 +7,10 @@ | ||||
|         <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> | ||||
|     <div> | ||||
|         <label for="comment">Comment</label> | ||||
|         <input type="text" id="comment" class="form-control" style="width: 200px;" /> | ||||
|     </div> | ||||
|  | ||||
|     <button type="submit" class="btn btn-primary">Add</button> | ||||
| </form> | ||||
| @@ -16,84 +20,127 @@ | ||||
| <canvas id="canvas"></canvas> | ||||
|  | ||||
| <script> | ||||
| (async function() { | ||||
|     const dateEl = $("#weight-date"); | ||||
|     const weightEl = $("#weight"); | ||||
|     (async function() { | ||||
|         const $form = $("#weight-form"); | ||||
|         const $date = $("#weight-date"); | ||||
|         const $weight = $("#weight"); | ||||
|         const $comment = $("#comment"); | ||||
|         let chart; | ||||
|  | ||||
|     dateEl.datepicker(); | ||||
|     dateEl.datepicker('option', 'dateFormat', 'yy-mm-dd'); | ||||
|     dateEl.datepicker('setDate', new Date()); | ||||
|         $date.datepicker(); | ||||
|         $date.datepicker('option', 'dateFormat', 'yy-mm-dd'); | ||||
|         $date.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); | ||||
|         async function saveWeight() { | ||||
|             await server.exec([$date.val(), parseFloat($weight.val()), $comment.val()], async (date, weight, comment) => { | ||||
|                 const dataNote = await this.getNoteWithAttribute('date_data', date); | ||||
|  | ||||
|             if (dataNote) { | ||||
|                 dataNote.jsonContent.weight = weight; | ||||
|                 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 | ||||
|                     if (comment) { | ||||
|                         dataNote.jsonContent.weight_comment = comment; | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         showMessage("Weight has been saved"); | ||||
|                     await this.updateEntity(dataNote); | ||||
|                 } | ||||
|                 else { | ||||
|                     const parentNoteId = await this.getDateNoteId(date); | ||||
|                     const jsonContent = { weight: weight }; | ||||
|  | ||||
|                     if (comment) { | ||||
|                         jsonContent.weight_comment = comment; | ||||
|                     } | ||||
|  | ||||
|                     await this.createNote(parentNoteId, 'data', jsonContent, { | ||||
|                         json: true, | ||||
|                         attributes: { | ||||
|                             date_data: date, | ||||
|                             hide_in_autocomplete: null | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             showMessage("Weight has been saved"); | ||||
|  | ||||
|             chart.data = await getData(); | ||||
|             chart.update(); | ||||
|         } | ||||
|  | ||||
|         async function drawChart() { | ||||
|             const data = await getData(); | ||||
|  | ||||
|             const ctx = $("#canvas")[0].getContext("2d"); | ||||
|  | ||||
|             chart = new Chart(ctx, { | ||||
|                 type: 'line', | ||||
|                 data: data, | ||||
|                 options: { | ||||
|                     tooltips: { | ||||
|                         enabled: true, | ||||
|                         mode: 'single', | ||||
|                         callbacks: { | ||||
|                             label: function (tooltipItem, data) { | ||||
|                                 const multistringText = [tooltipItem.yLabel]; | ||||
|                                 const comment = data.comments[tooltipItem['index']]; | ||||
|  | ||||
|                                 if (comment) { | ||||
|                                     multistringText.push(comment); | ||||
|                                 } | ||||
|  | ||||
|                                 return multistringText; | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         async function getData() { | ||||
|             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, | ||||
|                         comment: note.jsonContent.weight_comment | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 data.sort((a, b) => a.date < b.date ? -1 : +1); | ||||
|  | ||||
|                 return data; | ||||
|             }); | ||||
|  | ||||
|             const datasets = [{ | ||||
|                 label: "Weight", | ||||
|                 backgroundColor: 'red', | ||||
|                 borderColor: 'red', | ||||
|                 data: data.map(row => row.weight), | ||||
|                 fill: false | ||||
|             }]; | ||||
|  | ||||
|             const labels = data.map(row => row.date); | ||||
|             const comments = data.map(row => row.comment); | ||||
|  | ||||
|             return { | ||||
|                 labels: labels, | ||||
|                 datasets: datasets, | ||||
|                 comments: comments | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         $form.submit(event => { | ||||
|             saveWeight(); | ||||
|  | ||||
|             event.preventDefault(); | ||||
|         }); | ||||
|  | ||||
|         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> | ||||
| @@ -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, | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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); | ||||
|   | ||||
| @@ -20,6 +20,5 @@ module.exports = { | ||||
|     DOCUMENT_PATH, | ||||
|     BACKUP_DIR, | ||||
|     LOG_DIR, | ||||
|     EXPORT_DIR, | ||||
|     ANONYMIZED_DB_DIR | ||||
| }; | ||||
| @@ -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 | ||||
| }; | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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
									
								
							
							
						
						
									
										27
									
								
								src/services/scheduler.js
									
									
									
									
									
										Normal 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); | ||||
| @@ -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 (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 | ||||
| }; | ||||
| @@ -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; | ||||
| @@ -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() { | ||||
|   | ||||
| @@ -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 | ||||
| }; | ||||
| @@ -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) { | ||||
|   | ||||
| @@ -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 | ||||
| }; | ||||
| @@ -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> | ||||
|  | ||||
|             | ||||
|  | ||||
| @@ -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> | ||||
|                   | ||||
|                 <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> | ||||
		Reference in New Issue
	
	Block a user